diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d003a478750..4adcdb1029e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,5 @@ +IMPORTANT: Please disable plugins prior to posting a bug report. If you have a problem with a plugin please post on the plugin repository. Thanks! + --- name: Bug report about: Create a report to help us improve @@ -28,6 +30,7 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g., Ubuntu 20.04] - Node.js version (`node --version`): - npm version (`npm --version`): + - Is the server free of plugins: **Desktop (please complete the following information):** - OS: [e.g. iOS] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d745034b460..d9f660907db 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,29 +1,13 @@ - -> Please provide enough information so that others can review your pull request: - - - -> Explain the **details** for making this change. What existing problem does the pull request solve? - - - -> Screenshots/GIFs - - diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index e88b3273144..9e33cbe10a9 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -116,9 +116,7 @@ jobs: node-version: 12 - name: Install all dependencies and symlink for ep_etherpad-lite - run: | - cd src - npm ci --no-optional + run: src/bin/installOnWindows.bat - name: Fix up the settings.json run: | @@ -172,9 +170,7 @@ jobs: # if npm correctly hoists the dependencies, the hoisting seems to confuse # tools such as `npm outdated`, `npm update`, and some ESLint rules. - name: Install all dependencies and symlink for ep_etherpad-lite - run: | - cd src - npm ci --no-optional + run: src/bin/installOnWindows.bat - name: Fix up the settings.json run: | diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 44cb697f2dc..5fb0f39c2bb 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -57,6 +57,9 @@ jobs: - name: Write custom settings.json that enables the Admin UI tests run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json" + - name: increase maxHttpBufferSize + run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json" + - name: Remove standard frontend test files, so only admin tests are run run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs diff --git a/.gitignore b/.gitignore index 09618cc8326..60638c50a00 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ var/dirty.db *.patch npm-debug.log *.DS_Store -.ep_initialized *.crt *.key credentials.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d61e1ffee..d5d4bd07dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +# 1.8.15 + +### Security fixes + +* Fixed leak of the writable pad ID when exporting from the pad's read-only ID. + This only matters if you treat the writeable pad IDs as secret (e.g., you are + not using [ep_padlist2](https://www.npmjs.com/package/ep_padlist2)) and you + share the pad's read-only ID with untrusted users. Instead of treating + writeable pad IDs as secret, you are encouraged to take advantage of + Etherpad's authentication and authorization mechanisms (e.g., use + [ep_openid_connect](https://www.npmjs.com/package/ep_openid_connect) with + [ep_readonly_guest](https://www.npmjs.com/package/ep_readonly_guest), or write + your own + [authentication](https://etherpad.org/doc/v1.8.14/#index_authenticate) and + [authorization](https://etherpad.org/doc/v1.8.14/#index_authorize) plugins). + +### Compatibility changes + +* The `logconfig` setting is deprecated. +* For plugin authors: + * Etherpad now uses [jsdom](https://github.com/jsdom/jsdom) instead of + [cheerio](https://cheerio.js.org/) for processing HTML imports. There are + two consequences of this change: + * `require('ep_etherpad-lite/node_modules/cheerio')` no longer works. To + fix, your plugin should directly depend on `cheerio` and do + `require('cheerio')`. + * The `node` context argument passed to the `collectContentImage` hook is + now an + [`HTMLImageElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement) + object rather than a Cheerio Node-like object, so the API is slightly + different. See + [citizenos/ep_image_upload#49](https://github.com/citizenos/ep_image_upload/pull/49) + for an example fix. + * The `clientReady` server-side hook is deprecated; use the new `userJoin` + hook instead. + * The `init_` server-side hooks are now run every time Etherpad + starts up, not just the first time after the named plugin is installed. + * The `userLeave` server-side hook's context properties have changed: + * `auth`: Deprecated. + * `author`: Deprecated; use the new `authorId` property instead. + * `readonly`: Deprecated; use the new `readOnly` property instead. + * `rev`: Deprecated. + * Changes to the `src/static/js/Changeset.js` library: + * `opIterator()`: The unused start index parameter has been removed, as has + the unused `lastIndex()` method on the returned object. + * `smartOpAssembler()`: The returned object's `appendOpWithText()` method is + deprecated without a replacement available to plugins (if you need one, + let us know and we can make the private `opsFromText()` function public). + * Several functions that should have never been public are no longer + exported: `applyZip()`, `assert()`, `clearOp()`, `cloneOp()`, `copyOp()`, + `error()`, `followAttributes()`, `opString()`, `stringOp()`, + `textLinesMutator()`, `toBaseTen()`, `toSplices()`. + +### Notable enhancements + +* Simplified pad reload after importing an `.etherpad` file. +* For plugin authors: + * `clientVars` was added to the context for the `postAceInit` client-side + hook. Plugins should use this instead of the `clientVars` global variable. + * New `userJoin` server-side hook. + * The `userLeave` server-side hook has a new `socket` context property. + * The `helper.aNewPad()` function (accessible to client-side tests) now + accepts hook functions to inject when opening a pad. This can be used to + test any new client-side hooks your plugin provides. + * Chat improvements: + * The `chatNewMessage` client-side hook context has new properties: + * `message`: Provides access to the raw message object so that plugins can + see the original unprocessed message text and any added metadata. + * `rendered`: Allows plugins to completely override how the message is + rendered in the UI. + * New `chatSendMessage` client-side hook that enables plugins to process the + text before sending it to the server or augment the message object with + custom metadata. + * New `chatNewMessage` server-side hook to process new chat messages before + they are saved to the database and relayed to users. + # 1.8.14 ### Security fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 724e02ac021..15dfae99df7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,17 @@ number of the issue that is being fixed, in the form: Fixes #someIssueNumber ``` * if the PR is a **bug fix**: - * the first commit in the series must be a test that shows the failure - * subsequent commits will fix the bug and make the test pass - * the final commit message should include the text `Fixes: #xxx` to link it to its bug report + * The commit that fixes the bug should **include a regression test** that + would fail if the bug fix was reverted. Adding the regression test in the + same commit as the bug fix makes it easier for a reviewer to verify that the + test is appropriate for the bug fix. + * If there is a bug report, **the pull request description should include the + text "`Fixes #xxx`"** so that the bug report is auto-closed when the PR is + merged. It is less useful to say the same thing in a commit message because + GitHub will spam the bug report every time the commit is rebased, and + because a bug number alone becomes meaningless in forks. (A full URL would + be better, but ideally each commit is readable on its own without the need + to examine an external reference to understand motivation or context.) * think about stability: code has to be backwards compatible as much as possible. Always **assume your code will be run with an older version of the DB/config file** * if you want to remove a feature, **deprecate it instead**: * write an issue with your deprecation plan diff --git a/Dockerfile b/Dockerfile index 85b673fd67d..c6339ae72e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,6 +63,7 @@ RUN export DEBIAN_FRONTEND=noninteractive; \ apt-get -qq --no-install-recommends install \ ca-certificates \ git \ + curl \ ${INSTALL_ABIWORD:+abiword} \ ${INSTALL_SOFFICE:+libreoffice} \ && \ @@ -94,5 +95,7 @@ COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json # Fix group permissions RUN chmod -R g=u . +HEALTHCHECK --interval=20s --timeout=3s CMD curl -f http://localhost:9001 || exit 1 + EXPOSE 9001 CMD ["node", "src/node/server.js"] diff --git a/doc/api/changeset_library.md b/doc/api/changeset_library.md index c776c17c56c..7929aa48b92 100644 --- a/doc/api/changeset_library.md +++ b/doc/api/changeset_library.md @@ -1,156 +1,44 @@ # Changeset Library -``` -"Z:z>1|2=m=b*0|1+1$\n" -``` - -This is a Changeset. It's just a string and it's very difficult to read in this form. But the Changeset Library gives us some tools to read it. - -A changeset describes the diff between two revisions of the document. The Browser sends changesets to the server and the server sends them to the clients to update them. These Changesets also get saved into the history of a pad. This allows us to go back to every revision from the past. +The [changeset +library](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/Changeset.js) +provides tools to create, read, and apply changesets. -## Changeset.unpack(changeset) - - * `changeset` {String} - -This function returns an object representation of the changeset, similar to this: +## Changeset +```javascript +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); ``` -{ oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' } -``` - - * `oldLen` {Number} the original length of the document. - * `newLen` {Number} the length of the document after the changeset is applied. - * `ops` {String} the actual changes, introduced by this changeset. - * `charBank` {String} All characters that are added by this changeset. -## Changeset.opIterator(ops) +A changeset describes the difference between two revisions of a document. When a +user edits a pad, the browser generates and sends a changeset to the server, +which relays it to the other users and saves a copy (so that every past revision +is accessible). - * `ops` {String} The operators, returned by `Changeset.unpack()` +A transmitted changeset looks like this: -Returns an operator iterator. This iterator allows us to iterate over all operators that are in the changeset. - -You can iterate with an opIterator using its `next()` and `hasNext()` methods. Next returns the `next()` operator object and `hasNext()` indicates, whether there are any operators left. - -## The Operator object -There are 3 types of operators: `+`,`-` and `=`. These operators describe different changes to the document, beginning with the first character of the document. A `=` operator doesn't change the text, but it may add or remove text attributes. A `-` operator removes text. And a `+` Operator adds text and optionally adds some attributes to it. - - * `opcode` {String} the operator type - * `chars` {Number} the length of the text changed by this operator. - * `lines` {Number} the number of lines changed by this operator. - * `attribs` {attribs} attributes set on this text. - -### Example ``` -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '*0' } +'Z:z>1|2=m=b*0|1+1$\n' ``` -## APool +## Attribute Pool +```javascript +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); ``` -> var AttributePoolFactory = require("./utils/AttributePoolFactory"); -> var apool = AttributePoolFactory.createAttributePool(); -> console.log(apool) -{ numToAttrib: {}, - attribToNum: {}, - nextNum: 0, - putAttrib: [Function], - getAttrib: [Function], - getAttribKey: [Function], - getAttribValue: [Function], - eachAttrib: [Function], - toJsonable: [Function], - fromJsonable: [Function] } -``` - -This creates an empty apool. An apool saves which attributes were used during the history of a pad. There is one apool for each pad. It only saves the attributes that were really used, it doesn't save unused attributes. Let's fill this apool with some values -``` -> apool.fromJsonable({"numToAttrib":{"0":["author","a.kVnWeomPADAT2pn9"],"1":["bold","true"],"2":["italic","true"]},"nextNum":3}); -> console.log(apool) -{ numToAttrib: - { '0': [ 'author', 'a.kVnWeomPADAT2pn9' ], - '1': [ 'bold', 'true' ], - '2': [ 'italic', 'true' ] }, - attribToNum: - { 'author,a.kVnWeomPADAT2pn9': 0, - 'bold,true': 1, - 'italic,true': 2 }, - nextNum: 3, - putAttrib: [Function], - getAttrib: [Function], - getAttribKey: [Function], - getAttribValue: [Function], - eachAttrib: [Function], - toJsonable: [Function], - fromJsonable: [Function] } -``` - -We used the fromJsonable function to fill the empty apool with values. the fromJsonable and toJsonable functions are used to serialize and deserialize an apool. You can see that it stores the relation between numbers and attributes. So for example the attribute 1 is the attribute bold and vise versa. An attribute is always a key value pair. For stuff like bold and italic it's just 'italic':'true'. For authors it's author:$AUTHORID. So a character can be bold and italic. But it can't belong to multiple authors - -``` -> apool.getAttrib(1) -[ 'bold', 'true' ] -``` - -Simple example of how to get the key value pair for the attribute 1 - -## AText - -``` -> var atext = {"text":"bold text\nitalic text\nnormal text\n\n","attribs":"*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2"}; -> console.log(atext) -{ text: 'bold text\nitalic text\nnormal text\n\n', - attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2' } -``` - -This is an atext. An atext has two parts: text and attribs. The text is just the text of the pad as a string. We will look closer at the attribs at the next steps - -``` -> var opiterator = Changeset.opIterator(atext.attribs) -> console.log(opiterator) -{ next: [Function: next], - hasNext: [Function: hasNext], - lastIndex: [Function: lastIndex] } -> opiterator.next() -{ opcode: '+', - chars: 9, - lines: 0, - attribs: '*0*1' } -> opiterator.next() -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '*0' } -> opiterator.next() -{ opcode: '+', - chars: 11, - lines: 0, - attribs: '*0*1*2' } -> opiterator.next() -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '' } -> opiterator.next() -{ opcode: '+', - chars: 11, - lines: 0, - attribs: '*0' } -> opiterator.next() -{ opcode: '+', - chars: 2, - lines: 2, - attribs: '' } -``` +Changesets do not include any attribute key–value pairs. Instead, they use +numeric identifiers that reference attributes kept in an [attribute +pool](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/AttributePool.js). +This attribute interning reduces the transmission overhead of attributes that +are used many times. -The attribs are again a bunch of operators like .ops in the changeset was. But these operators are only + operators. They describe which part of the text has which attributes +There is one attribute pool per pad, and it includes every current and +historical attribute used in the pad. -## Resources / further reading +## Further Reading Detailed information about the changesets & Easysync protocol: -* Easysync Protocol - [/doc/easysync/easysync-notes.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf) -* Etherpad and EasySync Technical Manual - [/doc/easysync/easysync-full-description.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf) +* [Easysync Protocol](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf) +* [Etherpad and EasySync Technical Manual](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf) diff --git a/doc/api/editbar.md b/doc/api/editbar.md index d297eb255df..f448a218a5b 100644 --- a/doc/api/editbar.md +++ b/doc/api/editbar.md @@ -5,7 +5,7 @@ src/static/js/pad_editbar.js ## disable() -## toggleDropDown(dropdown, callback) +## toggleDropDown(dropdown) Shows the dropdown `div.popup` whose `id` equals `dropdown`. ## registerCommand(cmd, callback) diff --git a/doc/api/editorInfo.md b/doc/api/editorInfo.md index 7cbe3fcd008..834f5ac3cfc 100644 --- a/doc/api/editorInfo.md +++ b/doc/api/editorInfo.md @@ -18,7 +18,6 @@ Returns the `rep` object. ## editorInfo.ace_setOnKeyDown(?) ## editorInfo.ace_setNotifyDirty(?) ## editorInfo.ace_dispose(?) -## editorInfo.ace_getFormattedCode(?) ## editorInfo.ace_setEditable(bool) ## editorInfo.ace_execCommand(?) ## editorInfo.ace_callWithAce(fn, callStack, normalize) @@ -30,9 +29,6 @@ Returns the `rep` object. ## editorInfo.ace_applyPreparedChangesetToBase() ## editorInfo.ace_setUserChangeNotificationCallback(f) ## editorInfo.ace_setAuthorInfo(author, info) -## editorInfo.ace_setAuthorSelectionRange(author, start, end) -## editorInfo.ace_getUnhandledErrors() -## editorInfo.ace_getDebugProperty(prop) ## editorInfo.ace_fastIncorp(?) ## editorInfo.ace_isCaret(?) ## editorInfo.ace_getLineAndCharForPoint(?) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 2559a4e0862..45ef18a011a 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -229,7 +229,10 @@ Called from: src/static/js/pad.js Things in context: 1. ace - the ace object that is applied to this editor. -2. pad - the pad object of the current pad. +2. clientVars - Object containing client-side configuration such as author ID + and plugin settings. Your plugin can manipulate this object via the + `clientVars` server-side hook. +3. pad - the pad object of the current pad. ## postToolbarInit @@ -276,29 +279,53 @@ Things in context: This hook is called on the client side whenever a user joins or changes. This can be used to create notifications or an alternate user list. -## chatNewMessage - -Called from: src/static/js/chat.js - -Things in context: - -1. authorName - The user that wrote this message -2. author - The authorID of the user that wrote the message -3. text - the message text -4. sticky (boolean) - if you want the gritter notification bubble to fade out on - its own or just sit there -5. timestamp - the timestamp of the chat message -6. timeStr - the timestamp as a formatted string -7. duration - for how long in milliseconds should the gritter notification - appear (0 to disable) - -This hook is called on the client side whenever a chat message is received from -the server. It can be used to create different notifications for chat messages. -Hoook functions can modify the `author`, `authorName`, `duration`, `sticky`, -`text`, and `timeStr` context properties to change how the message is processed. -The `text` and `timeStr` properties may contain HTML, but plugins should be -careful to sanitize any added user input to avoid introducing an XSS -vulnerability. +## `chatNewMessage` + +Called from: `src/static/js/chat.js` + +This hook runs on the client side whenever a chat message is received from the +server. It can be used to create different notifications for chat messages. Hook +functions can modify the `author`, `authorName`, `duration`, `rendered`, +`sticky`, `text`, and `timeStr` context properties to change how the message is +processed. The `text` and `timeStr` properties may contain HTML and come +pre-sanitized; plugins should be careful to sanitize any added user input to +avoid introducing an XSS vulnerability. + +Context properties: + +* `authorName`: The display name of the user that wrote the message. +* `author`: The author ID of the user that wrote the message. +* `text`: Sanitized message HTML, with URLs wrapped like `url`. (Note that `message.text` is not sanitized or processed + in any way.) +* `message`: The raw message object as received from the server, except with + time correction and a default `authorId` property if missing. Plugins must not + modify this object. Warning: Unlike `text`, `message.text` is not + pre-sanitized or processed in any way. +* `rendered` - Used to override the default message rendering. Initially set to + `null`. If the hook function sets this to a DOM element object or a jQuery + object, then that object will be used as the rendered message UI. Otherwise, + if this is set to `null`, then Etherpad will render a default UI for the + message using the other context properties. +* `sticky` (boolean): Whether the gritter notification should fade out on its + own or just sit there until manually closed. +* `timestamp`: When the chat message was sent (milliseconds since epoch), + corrected using the difference between the local clock and the server's clock. +* `timeStr`: The message timestamp as a formatted string. +* `duration`: How long (in milliseconds) to display the gritter notification (0 + to disable). + +## `chatSendMessage` + +Called from: `src/static/js/chat.js` + +This hook runs on the client side whenever the user sends a new chat message. +Plugins can mutate the message object to change the message text or add metadata +to control how the message will be rendered by the `chatNewMessage` hook. + +Context properties: + +* `message`: The message object that will be sent to the Etherpad server. ## collectContentPre diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index c46da350ce3..5e8832fd488 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -50,12 +50,13 @@ Things in context: If this hook returns an error, the callback to the install function gets an error, too. This seems useful for adding in features when a particular plugin is installed. -## init_`` -Called from: src/static/js/pluginfw/plugins.js +## `init_` -Things in context: None +Called from: `src/static/js/pluginfw/plugins.js` + +Run during startup after the named plugin is initialized. -This function is called after a specific plugin is initialized. This would probably be more useful than the previous two functions if you only wanted to add in features to one specific plugin. +Context properties: None ## expressConfigure Called from: src/node/hooks/express.js @@ -579,9 +580,9 @@ Things in context: This hook allows plugins to grant temporary write access to a pad. It is called for each incoming message from a client. If write access is granted, it applies to the current message and all future messages from the same socket.io -connection until the next `CLIENT_READY` or `SWITCH_TO_PAD` message. Read-only -access is reset **after** each `CLIENT_READY` or `SWITCH_TO_PAD` message, so -granting write access has no effect for those message types. +connection until the next `CLIENT_READY` message. Read-only access is reset +**after** each `CLIENT_READY` message, so granting write access has no effect +for those message types. The handleMessageSecurity function must return a Promise. If the Promise resolves to `true`, write access is granted as described above. Returning @@ -807,36 +808,82 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` -## userLeave -Called from src/node/handler/PadMessageHandler.js +## `import` + +Called from: `src/node/handler/ImportHandler.js` + +Called when a user submits a document for import, before the document is +converted to HTML. The hook function should return a truthy value if the hook +function elected to convert the document to HTML. + +Context properties: + +* `destFile`: The destination HTML filename. +* `fileEnding`: The lower-cased filename extension from `srcFile` **with leading + period** (examples: `'.docx'`, `'.html'`, `'.etherpad'`). +* `padId`: The identifier of the destination pad. +* `srcFile`: The document to convert. + +## `userJoin` + +Called from: `src/node/handler/PadMessageHandler.js` -This in context: +Called after users have been notified that a new user has joined the pad. -1. session (including the pad id and author id) +Context properties: -This hook gets called when an author leaves a pad. This is useful if you want to perform certain actions after a pad has been edited +* `authorId`: The user's author identifier. +* `displayName`: The user's display name. +* `padId`: The real (not read-only) identifier of the pad the user joined. This + MUST NOT be shared with any users that are connected with read-only access. +* `readOnly`: Whether the user only has read-only access. +* `readOnlyPadId`: The read-only identifier of the pad the user joined. +* `socket`: The socket.io Socket object. Example: -``` -exports.userLeave = function(hook, session, callback) { - console.log('%s left pad %s', session.author, session.padId); +```javascript +exports.userJoin = async (hookName, {authorId, displayName, padId}) => { + console.log(`${authorId} (${displayName}) joined pad ${padId}); }; ``` -### clientReady -Called from src/node/handler/PadMessageHandler.js +## `userLeave` + +Called from: `src/node/handler/PadMessageHandler.js` -This in context: +Called when a user disconnects from a pad. This is useful if you want to perform +certain actions after a pad has been edited. -1. message +Context properties: -This hook gets called when handling a CLIENT_READY which is the first message from the client to the server. +* `authorId`: The user's author ID. +* `padId`: The pad's real (not read-only) identifier. +* `readOnly`: If truthy, the user only has read-only access. +* `readOnlyPadId`: The pad's read-only identifier. +* `socket`: The socket.io Socket object. Example: -``` -exports.clientReady = function(hook, message) { - console.log('Client has entered the pad' + message.padId); +```javascript +exports.userLeave = async (hookName, {author, padId}) => { + console.log(`${author} left pad ${padId}`); }; ``` + +## `chatNewMessage` + +Called from: `src/node/handler/PadMessageHandler.js` + +Called when a user (or plugin) generates a new chat message, just before it is +saved to the pad and relayed to all connected users. + +Context properties: + +* `message`: The chat message object. Plugins can mutate this object to change + the message text or add custom metadata to control how the message will be + rendered by the `chatNewMessage` client-side hook. The message's `authorId` + property can be trusted (the server overwrites any client-provided author ID + value with the user's actual author ID before this hook runs). +* `padId`: The pad's real (not read-only) identifier. +* `pad`: The pad's Pad object. diff --git a/doc/docker.md b/doc/docker.md index d7fb76b374c..91caab97e55 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -19,7 +19,7 @@ All of the following instructions are as a member of the `docker` group. By default, the Etherpad Docker image is built and run in `production` mode: no development dependencies are installed, and asset bundling speeds up page load time. ### Rebuilding with custom settings -Edit `/settings.json.docker` at your will. When rebuilding the image, this file will be copied inside your image and renamed to `setting.json`. +Edit `/settings.json.docker` at your will. When rebuilding the image, this file will be copied inside your image and renamed to `settings.json`. **Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. @@ -211,7 +211,9 @@ For the editor container, you can also make it full width by adding `full-width- | `FOCUS_LINE_PERCENTAGE_ARROW_UP` | Percentage of viewport height to be additionally scrolled when user presses arrow up in the line of the top of the viewport. Set to 0 to let the scroll to be handled as default by Etherpad | `0` | | `FOCUS_LINE_DURATION` | Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation | `0` | | `FOCUS_LINE_CARET_SCROLL` | Flag to control if it should scroll when user places the caret in the last line of the viewport | `false` | +| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. | `10000` | | `LOAD_TEST` | Allow Load Testing tools to hit the Etherpad Instance. WARNING: this will disable security on the instance. | `false` | +| `DUMP_ON_UNCLEAN_EXIT` | Enable dumping objects preventing a clean exit of Node.js. WARNING: this has a significant performance impact. | `false` | | `EXPOSE_VERSION` | Expose Etherpad version in the web interface and in the Server http header. Do not enable on production machines. | `false` | diff --git a/settings.json.docker b/settings.json.docker index d9373f5eb70..ed1be901d96 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -213,7 +213,9 @@ "user": "${DB_USER:undefined}", "password": "${DB_PASS:undefined}", "charset": "${DB_CHARSET:undefined}", - "filename": "${DB_FILENAME:var/dirty.db}" + "filename": "${DB_FILENAME:var/dirty.db}", + "collection": "${DB_COLLECTION:undefined}", + "url": "${DB_URL:undefined}" }, /* @@ -480,7 +482,7 @@ * value to work properly, but increasing the value increases susceptibility * to denial of service attacks (malicious clients can exhaust memory). */ - "maxHttpBufferSize": 10000 + "maxHttpBufferSize": "${SOCKETIO_MAX_HTTP_BUFFER_SIZE:10000}" }, /* @@ -493,7 +495,7 @@ /** * Disable dump of objects preventing a clean exit */ - "dumpOnUncleanExit": false, + "dumpOnUncleanExit": "${DUMP_ON_UNCLEAN_EXIT:false}", /* * Disable indentation on new line when previous line ends with some special @@ -584,58 +586,6 @@ */ "loglevel": "${LOGLEVEL:INFO}", - /* - * Logging configuration. See log4js documentation for further information: - * https://github.com/nomiddlename/log4js-node - * - * You can add as many appenders as you want here. - */ - "logconfig" : - { "appenders": [ - { "type": "console" - //, "category": "access"// only logs pad access - } - - /* - , { "type": "file" - , "filename": "your-log-file-here.log" - , "maxLogSize": 1024 - , "backups": 3 // how many log files there're gonna be at max - //, "category": "test" // only log a specific category - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "warn" // filters out all log messages that have a lower level than "error" - , "appender": - { Use whatever appender you want here } - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "error" // filters out all log messages that have a lower level than "error" - , "appender": - { "type": "smtp" - , "subject": "An error occurred in your EPL instance!" - , "recipients": "bar@blurdybloop.com, baz@blurdybloop.com" - , "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message - , "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods - "host": "smtp.example.com", "port": 465, - "secureConnection": true, - "auth": { - "user": "foo@example.com", - "pass": "bar_foo" - } - } - } - } - */ - - ] - }, // logconfig - /* Override any strings found in locale directories */ "customLocaleStrings": {} } diff --git a/settings.json.template b/settings.json.template index 2d7f119a227..8b8766be8e8 100644 --- a/settings.json.template +++ b/settings.json.template @@ -590,58 +590,6 @@ */ "loglevel": "INFO", - /* - * Logging configuration. See log4js documentation for further information: - * https://github.com/nomiddlename/log4js-node - * - * You can add as many appenders as you want here. - */ - "logconfig" : - { "appenders": [ - { "type": "console" - //, "category": "access"// only logs pad access - } - - /* - , { "type": "file" - , "filename": "your-log-file-here.log" - , "maxLogSize": 1024 - , "backups": 3 // how many log files there're gonna be at max - //, "category": "test" // only log a specific category - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "warn" // filters out all log messages that have a lower level than "error" - , "appender": - { Use whatever appender you want here } - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "error" // filters out all log messages that have a lower level than "error" - , "appender": - { "type": "smtp" - , "subject": "An error occurred in your EPL instance!" - , "recipients": "bar@blurdybloop.com, baz@blurdybloop.com" - , "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message - , "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods - "host": "smtp.example.com", "port": 465, - "secureConnection": true, - "auth": { - "user": "foo@example.com", - "pass": "bar_foo" - } - } - } - } - */ - - ] - }, // logconfig - /* Override any strings found in locale directories */ "customLocaleStrings": {}, diff --git a/src/bin/doc/package-lock.json b/src/bin/doc/package-lock.json new file mode 100644 index 00000000000..40fe13e45de --- /dev/null +++ b/src/bin/doc/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "node-doc-generator", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "marked": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", + "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==" + } + } +} diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index 94d8630cccc..66ad895c25f 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -15,10 +15,12 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )" is_cmd npm || fatal "Please install npm ( https://npmjs.org )" # Check npm version -require_minimal_version "npm" $(get_program_version "npm") "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" +require_minimal_version "npm" "$(get_program_version "npm")" \ + "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" # Check node version -require_minimal_version "nodejs" $(get_program_version "node") "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" +require_minimal_version "nodejs" "$(get_program_version "node")" \ + "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" # Get the name of the settings file settings="settings.json" @@ -34,17 +36,14 @@ if [ ! -f "$settings" ]; then cp settings.json.template "$settings" || exit 1 fi -log "Ensure that all dependencies are up to date... If this is the first time you have run Etherpad please be patient." +log "Installing dependencies..." ( - mkdir -p node_modules - cd node_modules - [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite - cd ep_etherpad-lite + mkdir -p node_modules && + cd node_modules && + { [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite; } && + cd ep_etherpad-lite && npm ci --no-optional -) || { - rm -rf src/node_modules - exit 1 -} +) || exit 1 # Remove all minified data to force node creating it new log "Clearing minified cache..." diff --git a/src/bin/installOnWindows.bat b/src/bin/installOnWindows.bat index 3c6bf58c01c..971335c20ad 100644 --- a/src/bin/installOnWindows.bat +++ b/src/bin/installOnWindows.bat @@ -1,7 +1,7 @@ @echo off :: Change directory to etherpad-lite root -cd /D "%~dp0\.." +cd /D "%~dp0\..\.." :: Is node installed? cmd /C node -e "" || ( echo "Please install node.js ( https://nodejs.org )" && exit /B 1 ) @@ -16,7 +16,7 @@ mklink /D "ep_etherpad-lite" "..\src" cd /D "ep_etherpad-lite" cmd /C npm ci || exit /B 1 -cd /D "%~dp0\.." +cd /D "%~dp0\..\.." echo _ echo Clearing cache... diff --git a/src/bin/plugins/lib/gitignore b/src/bin/plugins/lib/gitignore index 0719a85c1bd..153216eb946 100755 --- a/src/bin/plugins/lib/gitignore +++ b/src/bin/plugins/lib/gitignore @@ -1,5 +1,3 @@ -.ep_initialized .DS_Store node_modules/ -node_modules npm-debug.log diff --git a/src/ep.json b/src/ep.json index 5642f8c12dd..b917aa1f304 100644 --- a/src/ep.json +++ b/src/ep.json @@ -50,12 +50,6 @@ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize" } }, - { - "name": "padreadonly", - "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly" - } - }, { "name": "webaccess", "hooks": { diff --git a/src/locales/bn.json b/src/locales/bn.json index e30734c2d26..b46ed8bdbc2 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -5,23 +5,38 @@ "Aftabuzzaman", "Al Riaz Uddin Ripon", "Bellayet", + "Greatder", "Nasir8891", "Sankarshan", "Sibabrata Banerjee", "আফতাবুজ্জামান" ] }, + "admin.page-title": "প্রশাসক কেন্দ্র - ইথারপ্যাড", + "admin_plugins": "প্লাগিন ব্যবস্থাপক", + "admin_plugins.available": "বিদ্যমান প্লাগিন", + "admin_plugins.available_not-found": "প্লাগিন পাওয়া যায়নি।", "admin_plugins.available_fetching": "আনা হচ্ছে...", "admin_plugins.available_install.value": "ইনস্টল করুন", + "admin_plugins.available_search.placeholder": "ইনস্টল করার জন্য প্লাগইন অনুসন্ধান করুন", "admin_plugins.description": "বিবরণ", "admin_plugins.installed": "ইন্সটল হওয়া প্লাগিনসমূহ", "admin_plugins.installed_fetching": "ইন্সটলকৃত প্লাগিন আনা হচ্ছে", "admin_plugins.installed_uninstall.value": "আনইনস্টল করুন", "admin_plugins.last-update": "সর্বশেষ হালনাগাদ", "admin_plugins.name": "নাম", + "admin_plugins.page-title": "প্লাগিন ব্যবস্থাপনা - ইথারপ্যাড", "admin_plugins.version": "সংস্করণ", + "admin_plugins_info": "সমস্যা সমাধানের তথ্য", + "admin_plugins_info.hooks": "ইন্সটলকৃত হুক", + "admin_plugins_info.hooks_client": "গ্রাহক পার্শ্বের হুক", + "admin_plugins_info.hooks_server": "সার্ভার পার্শ্বের হুক", + "admin_plugins_info.parts": "ইন্সটলকৃত অংশ", + "admin_plugins_info.plugins": "ইন্সটলকৃত প্লাগিন", + "admin_plugins_info.page-title": "প্লাগিন তথ্য - ইথারপ্যাড", "admin_plugins_info.version": "ইথারপ্যাড সংস্করণ", "admin_plugins_info.version_latest": "সাম্প্রতিক উপলব্ধ সংস্করণ", + "admin_plugins_info.version_number": "সংস্করণ সংখ্যা", "admin_settings": "সেটিংসমূহ", "admin_settings.current": "বর্তমান কনফিগারেশন", "admin_settings.current_restart.value": "ইথারপ্যাড পুনরায় চালু করুন", diff --git a/src/locales/fi.json b/src/locales/fi.json index 9f569d44b6b..c0c827e78d2 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -5,8 +5,10 @@ "Espeox", "Jl", "Lliehu", + "MITO", "Maantietäjä", "Macofe", + "Markus Mikkonen", "MrTapsa", "Nedergard", "Nike", @@ -18,8 +20,11 @@ "VezonThunder" ] }, + "admin.page-title": "Ylläpitäjän kojelauta - Etherpad", + "admin_plugins": "Lisäosien hallinta", "admin_plugins.available": "Saatavilla olevat liitännäiset", - "admin_plugins.available_install.value": "Lataa", + "admin_plugins.available_not-found": "Lisäosia ei löytynyt.", + "admin_plugins.available_install.value": "Asenna", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.description": "Kuvaus", "admin_plugins.installed": "Asennetut laajennukset", @@ -43,7 +48,7 @@ "admin_settings": "Asetukset", "admin_settings.current": "Nykyinen kokoonpano", "admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista", - "admin_settings.current_save.value": "Tallenna Asetukset", + "admin_settings.current_save.value": "Tallenna asetukset", "admin_settings.page-title": "asetukset - Etherpad", "index.newPad": "Uusi muistio", "index.createOpenPad": "tai luo tai avaa muistio nimellä:", @@ -67,7 +72,7 @@ "pad.colorpicker.save": "Tallenna", "pad.colorpicker.cancel": "Peru", "pad.loading": "Ladataan…", - "pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi!", + "pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi! Istuntoasi ja asetuksiasi ei tulla tallentamaan vierailujen välillä. Tämä voi johtua siitä, että Etherpad on sisällytetty iFrameen joissain selaimissa. Varmistathan että Etherpad on samalla subdomainilla/domainilla kuin ylätason iFrame", "pad.permissionDenied": "Käyttöoikeutesi eivät riitä tämän muistion käyttämiseen.", "pad.settings.padSettings": "Muistion asetukset", "pad.settings.myView": "Oma näkymä", diff --git a/src/locales/ia.json b/src/locales/ia.json index d78ce31627f..ea5db836b46 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -21,7 +21,7 @@ "pad.toolbar.timeslider.title": "Glissa-tempore", "pad.toolbar.savedRevision.title": "Version salveguardate", "pad.toolbar.settings.title": "Configuration", - "pad.toolbar.embed.title": "Divider e incorporar iste pad", + "pad.toolbar.embed.title": "Condivider e incorporar iste pad", "pad.toolbar.showusers.title": "Monstrar le usatores de iste pad", "pad.colorpicker.save": "Salveguardar", "pad.colorpicker.cancel": "Cancellar", diff --git a/src/locales/ko.json b/src/locales/ko.json index 04c279a05f5..3fae86e93e2 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -9,6 +9,7 @@ "Revi", "SeoJeongHo", "Ykhwong", + "그냥기여자", "아라" ] }, @@ -132,6 +133,7 @@ "pad.chat.loadmessages": "더 많은 메시지 불러오기", "pad.chat.stick.title": "채팅을 화면에 고정", "pad.chat.writeMessage.placeholder": "여기에 메시지를 적으십시오", + "timeslider.followContents": "패드 콘텐츠의 갱신 주시하기", "timeslider.pageTitle": "{{appTitle}} 시간슬라이더", "timeslider.toolbar.returnbutton": "패드로 돌아가기", "timeslider.toolbar.authors": "저자:", diff --git a/src/locales/mk.json b/src/locales/mk.json index a5a10606008..68ba2f1cdef 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -77,7 +77,7 @@ "pad.settings.about": "За додатоков", "pad.settings.poweredBy": "Овозможено од", "pad.importExport.import_export": "Увоз/Извоз", - "pad.importExport.import": "Подигање на било каква текстуална податотека или документ", + "pad.importExport.import": "Подигање на било каква било текстуална податотека или документ", "pad.importExport.importSuccessful": "Успешно!", "pad.importExport.export": "Извези ја тековната тетратка како", "pad.importExport.exportetherpad": "Etherpad", diff --git a/src/locales/my.json b/src/locales/my.json new file mode 100644 index 00000000000..d427bbcb5c5 --- /dev/null +++ b/src/locales/my.json @@ -0,0 +1,169 @@ +{ + "@metadata": { + "authors": [ + "Andibecker", + "Dr Lotus Black" + ] + }, + "admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad", + "admin_plugins": "ပလပ်အင်မန်နေဂျာ", + "admin_plugins.available": "ရနိုင်သော plugins များ", + "admin_plugins.available_not-found": "ပလပ်အင်များမတွေ့ပါ။", + "admin_plugins.available_fetching": "ရယူနေသည်…", + "admin_plugins.available_install.value": "အင်စတော လုပ်ပါ", + "admin_plugins.available_search.placeholder": "အင်စတောလုပ်ဖို့ plugins များကိုရှာပါ", + "admin_plugins.description": "ဖော်ပြချက်", + "admin_plugins.installed": "plugins များထည့်သွင်းထားသည်", + "admin_plugins.installed_fetching": "ထည့်သွင်းထားသောပလပ်အင်များကိုရယူနေသည်…", + "admin_plugins.installed_nothing": "သင်မည်သည့် plugins ကိုမျှမထည့်သွင်းရသေးပါ။", + "admin_plugins.installed_uninstall.value": "ဖြုတ်ပါ", + "admin_plugins.last-update": "နောက်ဆုံးအပ်ဒိတ်", + "admin_plugins.name": "နာမည်", + "admin_plugins.page-title": "ပလပ်အင်မန်နေဂျာ - Etherpad", + "admin_plugins.version": "ဗားရှင်း", + "admin_plugins_info": "သတင်းအချက်အလက်ပြဿနာဖြေရှင်းခြင်း", + "admin_plugins_info.hooks": "ချိတ်များတပ်ဆင်ထားသည်", + "admin_plugins_info.hooks_client": "Client-side ချိတ်", + "admin_plugins_info.hooks_server": "Server-side ချိတ်", + "admin_plugins_info.parts": "တပ်ဆင်ထားသော အစိတ်အပိုင်းများ", + "admin_plugins_info.plugins": "plugins များထည့်သွင်းထားသည်", + "admin_plugins_info.page-title": "ပလပ်အင်အချက်အလက် - Etherpad", + "admin_plugins_info.version": "Etherpad ဗားရှင်း", + "admin_plugins_info.version_latest": "နောက်ဆုံးရနိုင်သောဗားရှင်း", + "admin_plugins_info.version_number": "ဗားရှင်းနံပါတ်", + "admin_settings": "အပြင်အဆင်များ", + "admin_settings.current": "လက်ရှိဖွဲ့စည်းမှု", + "admin_settings.current_example-devel": "နမူနာဖွံ့ဖြိုးတိုးတက်မှုဆက်တင်နမူနာ", + "admin_settings.current_example-prod": "နမူနာထုတ်လုပ်မှုဆက်တင်ပုံစံ", + "admin_settings.current_restart.value": "Etherpad ကိုပြန်လည်စတင်ပါ", + "admin_settings.current_save.value": "ဆက်တင်များကိုသိမ်းပါ", + "admin_settings.page-title": "ဆက်တင်များ - Etherpad", + "index.newPad": "Pad အသစ်", + "index.createOpenPad": "သို့မဟုတ် Pad နှင့်နာမည်ဖွင့်ပါ။", + "index.openPad": "ရှိပြီးသား Pad ကိုနာမည်နှင့်ဖွင့်ပါ။", + "pad.toolbar.bold.title": "စာလုံးအကြီး (Ctrl+B)", + "pad.toolbar.italic.title": "စာလုံးစောင်း (Ctrl+I)", + "pad.toolbar.underline.title": "မျဉ်းသားရန် (Ctrl+U)", + "pad.toolbar.strikethrough.title": "ဖြတ်တောက်ခြင်း (Ctrl+5)", + "pad.toolbar.ol.title": "အမှာစာစာရင်း (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Unordered စာရင်း (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "အင်တင်း (TAB)", + "pad.toolbar.unindent.title": "အပြင် (Shift+TAB)", + "pad.toolbar.undo.title": "ပြန်လုပ်ရန် (Ctrl+Z)", + "pad.toolbar.redo.title": "ပြန်လုပ်ရန် (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "စာရေးသူအရောင်များကိုရှင်းလင်းပါ (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "ကွဲပြားခြားနားသောဖိုင်အမျိုးအစားများမှ/သွင်းကုန်/တင်ပို့ပါ", + "pad.toolbar.timeslider.title": "Timeslider", + "pad.toolbar.savedRevision.title": "ပြန်လည်တည်းဖြတ်ပါ", + "pad.toolbar.settings.title": "အပြင်အဆင်များ", + "pad.toolbar.embed.title": "ဒီ pad ကို Share လုပ်ပြီးမြှုပ်လိုက်ပါ", + "pad.toolbar.showusers.title": "ဤ pad ပေါ်တွင်အသုံးပြုသူများကိုပြပါ", + "pad.colorpicker.save": "သိမ်းရန်", + "pad.colorpicker.cancel": "မလုပ်တော့ပါ", + "pad.loading": "ဝန်ဆွဲတင်နေသည်...", + "pad.noCookie": "ကွတ်ကီးကိုရှာမတွေ့ပါ။ ကျေးဇူးပြု၍ သင်၏ browser တွင် cookies များကိုခွင့်ပြုပါ။ လည်ပတ်မှုများအကြားသင်၏အစည်းအဝေးနှင့်ဆက်တင်များကိုသိမ်းဆည်းမည်မဟုတ်ပါ။ ၎င်းသည်အချို့သောဘရောင်ဇာများတွင် iFrame တွင် iFrame တွင်ထည့်သွင်းခံရခြင်းကြောင့်ဖြစ်နိုင်သည်။ Etherpad သည် parent iFrame ကဲ့သို့တူညီသော subdomain/domain ပေါ်တွင်သေချာပါစေ", + "pad.permissionDenied": "သင်ဤ pad ကိုသုံးခွင့်မရှိပါ", + "pad.settings.padSettings": "Pad ဆက်တင်များ", + "pad.settings.myView": "ငါ့အမြင်", + "pad.settings.stickychat": "ဖန်သားပြင်ပေါ်တွင်အမြဲစကားပြောပါ", + "pad.settings.chatandusers": "ချတ်နှင့်အသုံးပြုသူများကိုပြပါ", + "pad.settings.colorcheck": "စာရေးသူအရောင်များ", + "pad.settings.linenocheck": "လိုင်းနံပါတ်များ", + "pad.settings.rtlcheck": "အကြောင်းအရာကိုညာမှဘယ်သို့ဖတ်ပါ။", + "pad.settings.fontType": "ဖောင့်အမျိုးအစား", + "pad.settings.fontType.normal": "သာမန်", + "pad.settings.language": "ဘာသာစကား:", + "pad.settings.about": "အကြောင်း", + "pad.settings.poweredBy": "မှပံ့ပိုးသည်", + "pad.importExport.import_export": "သွင်းကုန်/ပို့ကုန်", + "pad.importExport.import": "မည်သည့်စာသားဖိုင်သို့မဆိုစာရွက်စာတမ်းတင်ပါ", + "pad.importExport.importSuccessful": "အောင်မြင်သည်။", + "pad.importExport.export": "လက်ရှိ pad ကိုအောက်ပါအတိုင်းတင်ပို့ပါ။", + "pad.importExport.exportetherpad": "Etherpad ပါ", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "ရိုးရိုးစာသား", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "ပီဒီအက်ဖ်", + "pad.importExport.exportopen": "ODF (စာရွက်စာတမ်းဖွင့်ပုံစံ)", + "pad.importExport.abiword.innerHTML": "သင်ရိုးရိုးစာသားများ (သို့) HTML ပုံစံများဖြင့်သာတင်သွင်းနိုင်သည်။ ပိုမိုအဆင့်မြင့်သောသွင်းကုန်အင်္ဂါရပ်များအတွက် ကျေးဇူးပြု၍ ကျေးဇူးပြု၍ AbiWord သို့မဟုတ် LibreOffice ကို install လုပ်ပါ။", + "pad.modals.connected": "ချိတ်ဆက်ထားသည်။", + "pad.modals.reconnecting": "သင်၏ pad သို့ပြန်လည်ချိတ်ဆက်နေသည်…", + "pad.modals.forcereconnect": "ပြန်လည်ချိတ်ဆက်ခိုင်းပါ", + "pad.modals.reconnecttimer": "ပြန်လည်ချိတ်ဆက်ရန်ကြိုးစားနေသည်", + "pad.modals.cancel": "မလုပ်တော့ပါ", + "pad.modals.userdup": "ပယ်ဖျက်", + "pad.modals.userdup.explanation": "ဤ pad ကိုဤကွန်ပျူတာရှိ browser window တစ်ခုထက်ပိုဖွင့်ထားပုံရသည်။", + "pad.modals.userdup.advice": "၎င်းအစားဤဝင်းဒိုးကိုသုံးရန်ပြန်လည်ချိတ်ဆက်ပါ။", + "pad.modals.unauth": "လုပ်ပိုင်ခွင့်မရှိပါ", + "pad.modals.unauth.explanation": "ဤစာမျက်နှာကိုကြည့်နေစဉ်သင်၏ခွင့်ပြုချက်များပြောင်းသွားသည်။ ပြန်လည်ချိတ်ဆက်ရန်ကြိုးစားပါ။", + "pad.modals.looping.explanation": "synchronization server နှင့်ဆက်သွယ်မှုပြဿနာများရှိသည်။", + "pad.modals.looping.cause": "သဟဇာတမဖြစ်သည့် firewall (သို့) proxy မှတဆင့်သင်ဆက်သွယ်နိုင်သည်။", + "pad.modals.initsocketfail": "ဆာဗာကို ဆက်သွယ်၍ မရပါ။", + "pad.modals.initsocketfail.explanation": "ထပ်တူပြုခြင်းဆာဗာသို့မချိတ်ဆက်နိုင်ခဲ့ပါ။", + "pad.modals.initsocketfail.cause": "၎င်းသည်သင်၏ browser (သို့) သင်၏အင်တာနက်ဆက်သွယ်မှုပြဿနာကြောင့်ဖြစ်နိုင်သည်။", + "pad.modals.slowcommit.explanation": "ဆာဗာကမတုံ့ပြန်ပါ။", + "pad.modals.slowcommit.cause": "၎င်းသည်ကွန်ယက်ချိတ်ဆက်မှုဆိုင်ရာပြဿနာများကြောင့်ဖြစ်နိုင်သည်။", + "pad.modals.badChangeset.explanation": "သင်ပြုလုပ်သောတည်းဖြတ်မှုကို synchronization server မှတရားမ ၀ င်ခွဲခြားခဲ့သည်။", + "pad.modals.badChangeset.cause": "၎င်းသည်မှားယွင်းသော server ဖွဲ့စည်းမှုပုံစံ (သို့) အခြားမမျှော်လင့်သောအပြုအမူများကြောင့်ဖြစ်နိုင်သည်။ ဤအရာသည်မှားယွင်းမှုတစ်ခုဟုသင်ခံစားရပါက ၀ န်ဆောင်မှုစီမံခန့်ခွဲသူအားဆက်သွယ်ပါ။ တည်းဖြတ်မှုကိုဆက်လက်လုပ်ဆောင်နိုင်ရန်ပြန်လည်ချိတ်ဆက်ကြည့်ပါ။", + "pad.modals.corruptPad.explanation": "သင်ရယူရန်ကြိုးစားနေသော pad သည်ယိုယွင်းနေသည်။", + "pad.modals.corruptPad.cause": "၎င်းသည်မှားယွင်းသော server ဖွဲ့စည်းမှုပုံစံ (သို့) အခြားမမျှော်လင့်သောအပြုအမူများကြောင့်ဖြစ်နိုင်သည်။ ကျေးဇူးပြု၍ ၀ န်ဆောင်မှုစီမံခန့်ခွဲသူကိုဆက်သွယ်ပါ။", + "pad.modals.deleted": "ဖျက်လိုက်သည်။", + "pad.modals.deleted.explanation": "ဒီအကွက်ကိုဖယ်ရှားပြီးပါပြီ။", + "pad.modals.rateLimited": "နှုန်းကန့်သတ်။", + "pad.modals.rateLimited.explanation": "မင်းဒီအဆက်အသွယ်ကိုဒီ pad မှာအရမ်းများတဲ့မက်ဆေ့ဂျ်တွေပို့ခဲ့တယ်။", + "pad.modals.rejected.explanation": "ဆာဗာသည်သင်၏ဘရောင်ဇာမှပေးပို့သောစာကိုငြင်းပယ်ခဲ့သည်။", + "pad.modals.rejected.cause": "သင် pad ကိုကြည့်နေစဉ်ဆာဗာကိုမွမ်းမံခဲ့ပေမည်၊ သို့မဟုတ် Etherpad တွင်ချို့ယွင်းချက်တစ်ခုရှိနေနိုင်သည်။ စာမျက်နှာကိုပြန်တင်ကြည့်ပါ။", + "pad.modals.disconnected": "မင်းအဆက်အသွယ်ဖြတ်လိုက်ပြီ။", + "pad.modals.disconnected.explanation": "ဆာဗာနှင့်ချိတ်ဆက်မှုပြတ်တောက်သွားသည်", + "pad.modals.disconnected.cause": "ဆာဗာမရနိုင်ပါ။ ဤသို့ဆက်ဖြစ်နေပါက ၀န်ဆောင်မှုစီမံခန့်ခွဲသူအား အကြောင်းကြားပါ။", + "pad.share": "ဒီစာရွက်ကိုမျှဝေပါ", + "pad.share.readonly": "ဖတ်သာကြည့်ပါ", + "pad.share.link": "လင့်", + "pad.share.emebdcode": "URL ထည့်ပါ", + "pad.chat": "စကားပြောမယ်", + "pad.chat.title": "ဒီ pad အတွက်စကားပြောခန်းကိုဖွင့်ပါ။", + "pad.chat.loadmessages": "နောက်ထပ်မက်ဆေ့ခ်ျများတင်ပါ", + "pad.chat.stick.title": "ချတ်ကိုမျက်နှာပြင်သို့ကပ်ပါ", + "pad.chat.writeMessage.placeholder": "မင်းရဲ့စာကိုဒီမှာရေးပါ", + "timeslider.followContents": "pad အကြောင်းအရာနောက်ဆုံးသတင်းများကိုလိုက်နာပါ", + "timeslider.pageTitle": "{{appTitle}} Timeslider", + "timeslider.toolbar.returnbutton": "ပလက်ဖောင်းသို့ပြန်သွားရန်", + "timeslider.toolbar.authors": "ရေးသားသူ -", + "timeslider.toolbar.authorsList": "စာရေးသူမရှိပါ", + "timeslider.toolbar.exportlink.title": "တင်ပို့သည်", + "timeslider.exportCurrent": "လက်ရှိဗားရှင်းအဖြစ်", + "timeslider.version": "ဗားရှင်း {{version}}", + "timeslider.saved": "သိမ်းထားသည် {{month}} {{day}}, {{year}}", + "timeslider.playPause": "Pad အကြောင်းအရာများပြန်ဖွင့်ခြင်း / ခဏရပ်ခြင်း", + "timeslider.backRevision": "ဤ Pad ရှိပြန်လည်သုံးသပ်ခြင်းကိုပြန်သွားပါ", + "timeslider.forwardRevision": "ဤ Pad ၌တည်းဖြတ်မှုတစ်ခုကိုရှေ့ဆက်ပါ", + "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}: {{minutes}}: {{seconds}}", + "timeslider.month.january": "ဇန်နဝါရီ", + "timeslider.month.february": "ဖေဖော်ဝါရီ", + "timeslider.month.march": "မတ်", + "timeslider.month.april": "ဧပြီ", + "timeslider.month.may": "မေ", + "timeslider.month.june": "ဇွန်", + "timeslider.month.july": "ဇူလိုင်", + "timeslider.month.august": "ဩဂုတ်", + "timeslider.month.september": "စက်တင်ဘာ", + "timeslider.month.october": "အောက်တိုဘာ", + "timeslider.month.november": "နို​ဝင်​ဘာ​", + "timeslider.month.december": "ဒီဇင်ဘာ", + "timeslider.unnamedauthors": "{{num}} အမည်မဖော်လိုသူ {[အများကိန်း (num) one: author၊ အခြား: author]}", + "pad.savedrevs.marked": "ယခုပြန်လည်တည်းဖြတ်မှုအားသိမ်းဆည်းထားသောတည်းဖြတ်မှုတစ်ခုအဖြစ်အမှတ်အသားပြုထားသည်", + "pad.savedrevs.timeslider": "timeslider ကိုသွားခြင်းဖြင့်သိမ်းဆည်းထားသောပြန်လည်တည်းဖြတ်ချက်များကိုသင်မြင်နိုင်သည်", + "pad.userlist.entername": "မင်းနာမည်ထည့်ပါ", + "pad.userlist.unnamed": "အမည်မဲ့", + "pad.editbar.clearcolors": "စာရွက်စာတမ်းတစ်ခုလုံးတွင်စာရေးသူအရောင်များကိုရှင်းလိုပါသလား။ ဒါကိုပြန် ပြင်၍ မရပါ", + "pad.impexp.importbutton": "ယခုတင်သွင်းပါ", + "pad.impexp.importing": "တင်သွင်းနေသည် ...", + "pad.impexp.confirmimport": "ဖိုင်တစ်ခုတင်သွင်းခြင်းသည် pad ၏လက်ရှိစာသားကိုထပ်ရေးလိမ့်မည်။ သင်ရှေ့ဆက်လိုသည်မှာသေချာသလား။", + "pad.impexp.convertFailed": "ဤဖိုင်ကိုကျွန်ုပ်တို့မတင်သွင်းနိုင်ခဲ့ပါ။ ကျေးဇူးပြု၍ အခြားစာရွက်စာတမ်းပုံစံတစ်ခုကိုသုံးပါသို့မဟုတ်ကိုယ်တိုင်ကူးယူပါ", + "pad.impexp.padHasData": "ဤ Pad သည်အပြောင်းအလဲများရှိနေပြီးဖြစ်သောကြောင့် ကျေးဇူးပြု၍ ဤဖိုင်ကိုတင်သွင်းနိုင်ခဲ့ခြင်းမရှိပါ၊ ကျေးဇူးပြု၍ pad အသစ်သို့တင်သွင်းပါ", + "pad.impexp.uploadFailed": "အပ်လုဒ်တင်ခြင်းမအောင်မြင်ပါ၊ ကျေးဇူးပြု၍ ထပ်ကြိုးစားပါ", + "pad.impexp.importfailed": "တင်သွင်းမှုမအောင်မြင်ပါ", + "pad.impexp.copypaste": "ကျေးဇူးပြု၍ ကူးထည့်ပါ", + "pad.impexp.exportdisabled": "{{type}} ပုံစံအဖြစ်ထုတ်ယူခြင်းကိုပိတ်ထားသည်။ အသေးစိတ်အတွက် ကျေးဇူးပြု၍ သင်၏စနစ်စီမံခန့်ခွဲသူကိုဆက်သွယ်ပါ။", + "pad.impexp.maxFileSize": "ဖိုင်ဆိုဒ်အရမ်းကြီးတယ်။ သွင်းကုန်အတွက်ခွင့်ပြုထားသောဖိုင်အရွယ်အစားကိုမြှင့်ရန်သင်၏ site စီမံခန့်ခွဲသူနှင့်ဆက်သွယ်ပါ" +} diff --git a/src/locales/nl.json b/src/locales/nl.json index 99da9420a31..303bded94b8 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -7,6 +7,7 @@ "Macofe", "Mainframe98", "Marcelhospers", + "McDutchie", "PonkoSasuke", "Rickvl", "Robin van der Vliet", @@ -57,7 +58,7 @@ "pad.toolbar.ol.title": "Geordende lijst (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Ongeordende lijst (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Inspringen (Tab)", - "pad.toolbar.unindent.title": "Inspringing verkleinen (Shift+Tab)", + "pad.toolbar.unindent.title": "Uitspringen (Shift+Tab)", "pad.toolbar.undo.title": "Ongedaan maken (Ctrl-Z)", "pad.toolbar.redo.title": "Opnieuw uitvoeren (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "Kleuren auteurs wissen (Ctrl+Shift+C)", @@ -70,7 +71,7 @@ "pad.colorpicker.save": "Opslaan", "pad.colorpicker.cancel": "Annuleren", "pad.loading": "Bezig met laden…", - "pad.noCookie": "Er kon geen cookie gevonden worden. Zorg ervoor dat uw browser cookies accepteert.", + "pad.noCookie": "Er kon geen cookie gevonden worden. Zorg ervoor dat uw browser cookies accepteert. Uw sessie en instellingen worden tussen bezoeken niet opgeslagen. Dit kan te wijten zijn aan het feit dat Etherpad in sommige browsers wordt opgenomen in een iFrame. Zorg ervoor dat Etherpad zich op hetzelfde subdomein/domein bevindt als het bovenliggende iFrame.", "pad.permissionDenied": "U hebt geen rechten om deze pad te bekijken", "pad.settings.padSettings": "Padinstellingen", "pad.settings.myView": "Mijn overzicht", @@ -93,9 +94,9 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "Pdf", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "U kunt alleen importeren vanuit Tekst zonder opmaak of een HTML-opmaak. Installeer AbiWord om meer geavanceerde importmogelijkheden te krijgen.", + "pad.importExport.abiword.innerHTML": "U kunt alleen importeren vanuit tekst zonder opmaak of met HTML-opmaak. Installeer AbiWord of LibreOffice om meer geavanceerde importmogelijkheden te krijgen.", "pad.modals.connected": "Verbonden.", - "pad.modals.reconnecting": "Opnieuw verbinding maken met uw pad...", + "pad.modals.reconnecting": "Opnieuw verbinding maken met uw pad…", "pad.modals.forcereconnect": "Opnieuw verbinden", "pad.modals.reconnecttimer": "Proberen te verbinden over", "pad.modals.cancel": "Annuleren", diff --git a/src/locales/sd.json b/src/locales/sd.json index 2d2d4bcd3e4..a167196f91a 100644 --- a/src/locales/sd.json +++ b/src/locales/sd.json @@ -49,7 +49,7 @@ "pad.modals.cancel": "رد", "pad.modals.userdup": "هڪ ٻي دري ۾ کليل", "pad.modals.unauth": "اختيار نه آهي", - "pad.modals.initsocketfail": "سَروَرَ کي پڄي نٿو سگھجي.", + "pad.modals.initsocketfail": "سَروَرَ تائين پُڄي نٿو سگهجي.", "pad.modals.slowcommit.explanation": "سَروَر جواب نٿو ڏي.", "pad.modals.corruptPad.explanation": "جيڪا پٽي توهان حاصل ڪرڻ چاهيو ٿا اها بدعنوان آهي.", "pad.modals.deleted": "ختم ڪيل.", diff --git a/src/locales/sw.json b/src/locales/sw.json new file mode 100644 index 00000000000..33d24add2e6 --- /dev/null +++ b/src/locales/sw.json @@ -0,0 +1,169 @@ +{ + "@metadata": { + "authors": [ + "Andibecker", + "Edwingudfriend", + "Muddyb" + ] + }, + "admin.page-title": "Dashibodi ya Usimamizi - Etherpad", + "admin_plugins": "Meneja wa programu-jalizi", + "admin_plugins.available": "Programu-jalizi zinazopatikana", + "admin_plugins.available_not-found": "Hakuna programu-jalizi zilizopatikana.", + "admin_plugins.available_fetching": "Inaleta…", + "admin_plugins.available_install.value": "Sakinisha", + "admin_plugins.available_search.placeholder": "Tafuta programu-jalizi ili usakinishe", + "admin_plugins.description": "Maelezo", + "admin_plugins.installed": "Programu-jalizi zilizosanikishwa", + "admin_plugins.installed_fetching": "Inaleta programu-jalizi zilizosakinishwa…", + "admin_plugins.installed_nothing": "Bado hujasakinisha programu-jalizi yoyote.", + "admin_plugins.installed_uninstall.value": "Ondoa", + "admin_plugins.last-update": "Sasisho la mwisho", + "admin_plugins.name": "Jina", + "admin_plugins.page-title": "Meneja wa programu-jalizi - Etherpad", + "admin_plugins.version": "Toleo", + "admin_plugins_info": "Maelezo ya utatuzi", + "admin_plugins_info.hooks": "Ndoano zilizowekwa", + "admin_plugins_info.hooks_client": "Kulabu za mteja", + "admin_plugins_info.hooks_server": "Kulabu za upande wa seva", + "admin_plugins_info.parts": "Sehemu zilizowekwa", + "admin_plugins_info.plugins": "Programu-jalizi zilizosanikishwa", + "admin_plugins_info.page-title": "Habari ya programu-jalizi - Etherpad", + "admin_plugins_info.version": "Toleo la Etherpad", + "admin_plugins_info.version_latest": "Toleo la hivi karibuni linalopatikana", + "admin_plugins_info.version_number": "Nambari ya toleo", + "admin_settings": "Mipangilio", + "admin_settings.current": "Usanidi wa sasa", + "admin_settings.current_example-devel": "Mfano mipangilio ya mipangilio ya maendeleo", + "admin_settings.current_example-prod": "Mfano mipangilio ya mipangilio ya uzalishaji", + "admin_settings.current_restart.value": "Anzisha upya Etherpad", + "admin_settings.current_save.value": "Hifadhi Mipangilio", + "admin_settings.page-title": "Mipangilio - Etherpad", + "index.newPad": "Pad Mpya", + "index.createOpenPad": "au tunga/fungua Pad yenye jina:", + "index.openPad": "fungua Pad iliyopo na jina:", + "pad.toolbar.bold.title": "Koozesha (Ctrl+B)", + "pad.toolbar.italic.title": "Mlalo (Ctrl+I)", + "pad.toolbar.underline.title": "Pigia mstari (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Kata (Ctrl+5)", + "pad.toolbar.ol.title": "Orodha iliyopangliwa (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Orodha isiyopangiliwa (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Jongeza (TAB)", + "pad.toolbar.unindent.title": "Punguza (Shift+TAB)", + "pad.toolbar.undo.title": "Tengua (Ctrl+Z)", + "pad.toolbar.redo.title": "Fanyaupya (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Futa Rangi za Uandishi (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Ingiza/Toa kutoka/kwa muundo wa faili tofauti", + "pad.toolbar.timeslider.title": "Kiburuzawakati", + "pad.toolbar.savedRevision.title": "Hifadhi Mapitio", + "pad.toolbar.settings.title": "Marekebisho", + "pad.toolbar.embed.title": "Shiriki na Pachika pedi hii", + "pad.toolbar.showusers.title": "Onyesha watumiaji kwenye pedi hii", + "pad.colorpicker.save": "Okoa", + "pad.colorpicker.cancel": "Ghairi", + "pad.loading": "Inapakiwa...", + "pad.noCookie": "Kuki haikuweza kupatikana. Tafadhali ruhusu kuki katika kivinjari chako! Kikao na mipangilio yako haitahifadhiwa kati ya ziara. Hii inaweza kuwa ni kutokana na Etherpad kuingizwa katika iFrame katika Vivinjari vingine. Tafadhali hakikisha Etherpad iko kwenye kikoa / kikoa sawa na iFrame ya mzazi", + "pad.permissionDenied": "Huna ruhusa ya kufikia pedi hii", + "pad.settings.padSettings": "Mipangilio ya pedi", + "pad.settings.myView": "Mtazamo Wangu", + "pad.settings.stickychat": "Ongea kila wakati kwenye skrini", + "pad.settings.chatandusers": "Onyesha Gumzo na Watumiaji", + "pad.settings.colorcheck": "Rangi za uandishi", + "pad.settings.linenocheck": "Nambari za laini", + "pad.settings.rtlcheck": "Soma yaliyomo kutoka kulia kwenda kushoto?", + "pad.settings.fontType": "Aina ya herufi:", + "pad.settings.language": "Lugha:", + "pad.settings.about": "Kuhusu", + "pad.settings.poweredBy": "Kinatumia", + "pad.importExport.import_export": "Ingiza / Hamisha", + "pad.importExport.import": "Pakia faili yoyote ya maandishi au hati", + "pad.importExport.importSuccessful": "Imefanikiwa!", + "pad.importExport.export": "Hamisha pedi ya sasa kama:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Maandishi wazi", + "pad.importExport.exportword": "Neno la Microsoft", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Fungua Fomati ya Hati)", + "pad.importExport.abiword.innerHTML": "Unaweza kuagiza tu kutoka kwa maandishi wazi au fomati za HTML. Kwa vipengee vya hali ya juu zaidi tafadhali weka AbiWord au LibreOffice .", + "pad.modals.connected": "Imeunganishwa", + "pad.modals.reconnecting": "Inaunganisha tena pedi yako…", + "pad.modals.forcereconnect": "Lazimisha kuunganisha tena", + "pad.modals.reconnecttimer": "Kujaribu kuungana tena", + "pad.modals.cancel": "Ghairi", + "pad.modals.userdup": "Imefunguliwa kwenye dirisha lingine", + "pad.modals.userdup.explanation": "Pedi hii inaonekana kufunguliwa katika zaidi ya dirisha moja la kivinjari kwenye kompyuta hii.", + "pad.modals.userdup.advice": "Unganisha tena ili utumie dirisha hili badala yake.", + "pad.modals.unauth": "Haijaidhinishwa", + "pad.modals.unauth.explanation": "Ruhusa zako zimebadilika wakati wa kutazama ukurasa huu. Jaribu kuunganisha tena.", + "pad.modals.looping.explanation": "Kuna shida za mawasiliano na seva ya maingiliano.", + "pad.modals.looping.cause": "Labda uliunganisha kupitia firewall isiyokubaliana au wakala.", + "pad.modals.initsocketfail": "Seva haipatikani.", + "pad.modals.initsocketfail.explanation": "Imeshindwa kuunganisha kwenye seva ya usawazishaji.", + "pad.modals.initsocketfail.cause": "Labda hii ni kwa sababu ya shida na kivinjari chako au muunganisho wako wa mtandao.", + "pad.modals.slowcommit.explanation": "Seva haijibu.", + "pad.modals.slowcommit.cause": "Hii inaweza kuwa ni kwa sababu ya shida na muunganisho wa mtandao.", + "pad.modals.badChangeset.explanation": "Hariri uliyoifanya iliainishwa kuwa haramu na seva ya maingiliano.", + "pad.modals.badChangeset.cause": "Hii inaweza kuwa ni kwa sababu ya usanidi mbaya wa seva au tabia zingine zisizotarajiwa. Tafadhali wasiliana na msimamizi wa huduma, ikiwa unahisi hii ni kosa. Jaribu kuunganisha tena ili uendelee kuhariri.", + "pad.modals.corruptPad.explanation": "Pedi unayojaribu kufikia ni mbovu.", + "pad.modals.corruptPad.cause": "Hii inaweza kuwa ni kwa sababu ya usanidi mbaya wa seva au tabia zingine zisizotarajiwa. Tafadhali wasiliana na msimamizi wa huduma.", + "pad.modals.deleted": "Imefutwa.", + "pad.modals.deleted.explanation": "Pedi hii imeondolewa.", + "pad.modals.rateLimited": "Kiwango kidogo.", + "pad.modals.rateLimited.explanation": "Ulituma ujumbe mwingi kwenye pedi hii kwa hivyo ikakukata.", + "pad.modals.rejected.explanation": "Seva ilikataa ujumbe ambao ulitumwa na kivinjari chako.", + "pad.modals.rejected.cause": "Seva inaweza kuwa imesasishwa wakati unatazama pedi, au labda kuna mdudu katika Etherpad. Jaribu kupakia upya ukurasa.", + "pad.modals.disconnected": "Umetenganishwa", + "pad.modals.disconnected.explanation": "Muunganisho wa seva ulipotea", + "pad.modals.disconnected.cause": "Huenda seva haipatikani. Tafadhali mjulishe msimamizi wa huduma ikiwa hii itaendelea kutokea.", + "pad.share": "Shiriki pedi hii", + "pad.share.readonly": "Soma tu", + "pad.share.link": "Kiungo", + "pad.share.emebdcode": "Pachika URL", + "pad.chat": "Ongea", + "pad.chat.title": "Fungua gumzo kwa pedi hii.", + "pad.chat.loadmessages": "Pakia ujumbe zaidi", + "pad.chat.stick.title": "Funga mazungumzo kwenye skrini", + "pad.chat.writeMessage.placeholder": "Andika ujumbe wako hapa", + "timeslider.followContents": "Fuata sasisho za yaliyomo kwenye pedi", + "timeslider.pageTitle": "{{appTitle}} Mpangaji Nyakati", + "timeslider.toolbar.returnbutton": "Rudi kwenye pedi", + "timeslider.toolbar.authors": "Waandishi", + "timeslider.toolbar.authorsList": "Hakuna Waandishi", + "timeslider.toolbar.exportlink.title": "Hamisha", + "timeslider.exportCurrent": "Hamisha toleo la sasa kama:", + "timeslider.version": "Toleo {{version}}", + "timeslider.saved": "Imehifadhiwa {{month}} {{day}}, {{year}}", + "timeslider.playPause": "Uchezaji / Sitisha Yaliyomo ya Pad", + "timeslider.backRevision": "Rudi nyuma kwenye toleo hili", + "timeslider.forwardRevision": "Nenda mbele kwa marekebisho katika Pad hii", + "timeslider.dateformat": "{{month}} / {{day}} / {{year}} {{hours}}: {{minutes}}: {{seconds}}", + "timeslider.month.january": "Januari", + "timeslider.month.february": "Februari", + "timeslider.month.march": "Machi", + "timeslider.month.april": "Aprili", + "timeslider.month.may": "Mei", + "timeslider.month.june": "Juni", + "timeslider.month.july": "Julai", + "timeslider.month.august": "Agosti", + "timeslider.month.september": "Septemba", + "timeslider.month.october": "Oktoba", + "timeslider.month.november": "Novemba", + "timeslider.month.december": "Desemba", + "timeslider.unnamedauthors": "{{num}} haijatajwa jina {[wingi (num) moja: mwandishi, mwingine: waandishi]}", + "pad.savedrevs.marked": "Marekebisho haya sasa yamewekwa alama kama marekebisho yaliyohifadhiwa", + "pad.savedrevs.timeslider": "Unaweza kuona marekebisho yaliyohifadhiwa kwa kutembelea mpangilio wa nyakati", + "pad.userlist.entername": "Ingiza jina lako", + "pad.userlist.unnamed": "bila jina", + "pad.editbar.clearcolors": "Futa rangi za uandishi kwenye hati nzima? Hii haiwezi kutenduliwa", + "pad.impexp.importbutton": "Ingiza Sasa", + "pad.impexp.importing": "Inaleta ...", + "pad.impexp.confirmimport": "Kuingiza faili kutaondoa maandishi ya sasa ya pedi. Je! Una uhakika unataka kuendelea?", + "pad.impexp.convertFailed": "Hatukuweza kuleta faili hii. Tafadhali tumia fomati ya hati tofauti au nakili ubandike mwenyewe", + "pad.impexp.padHasData": "Hatukuweza kuagiza faili hii kwa sababu pedi hii tayari imekuwa na mabadiliko, tafadhali ingiza kwa pedi mpya", + "pad.impexp.uploadFailed": "Upakiaji umeshindwa, tafadhali jaribu tena", + "pad.impexp.importfailed": "Uingizaji haukufaulu", + "pad.impexp.copypaste": "Tafadhali nakili kuweka", + "pad.impexp.exportdisabled": "Kuhamisha kama muundo wa {{type}} kumezimwa. Tafadhali wasiliana na msimamizi wako wa mfumo kwa maelezo.", + "pad.impexp.maxFileSize": "Faili kubwa sana. Wasiliana na msimamizi wa wavuti yako ili kuongeza saizi iliyoruhusiwa ya kuagiza" +} diff --git a/src/locales/th.json b/src/locales/th.json index 3905b8f4088..573c7a76102 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -2,9 +2,43 @@ "@metadata": { "authors": [ "Aefgh39622", + "Andibecker", "Patsagorn Y." ] }, + "admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad", + "admin_plugins": "ตัวจัดการปลั๊กอิน", + "admin_plugins.available": "ปลั๊กอินที่มีอยู่", + "admin_plugins.available_not-found": "ไม่พบปลั๊กอิน", + "admin_plugins.available_fetching": "กำลังเรียก…", + "admin_plugins.available_install.value": "ติดตั้ง", + "admin_plugins.available_search.placeholder": "ค้นหาปลั๊กอินที่จะติดตั้ง", + "admin_plugins.description": "คำอธิบาย", + "admin_plugins.installed": "ปลั๊กอินที่ติดตั้ง", + "admin_plugins.installed_fetching": "กำลังเรียกปลั๊กอินที่ติดตั้ง…", + "admin_plugins.installed_nothing": "คุณยังไม่ได้ติดตั้งปลั๊กอินใดๆ", + "admin_plugins.installed_uninstall.value": "ถอนการติดตั้ง", + "admin_plugins.last-update": "การปรับปรุงครั้งล่าสุด", + "admin_plugins.name": "ชื่อ", + "admin_plugins.page-title": "ตัวจัดการปลั๊กอิน - Etherpad", + "admin_plugins.version": "เวอร์ชั่น", + "admin_plugins_info": "ข้อมูลการแก้ไขปัญหา", + "admin_plugins_info.hooks": "ติดตั้งตะขอ", + "admin_plugins_info.hooks_client": "ตะขอฝั่งไคลเอ็นต์", + "admin_plugins_info.hooks_server": "ตะขอฝั่งเซิร์ฟเวอร์", + "admin_plugins_info.parts": "ชิ้นส่วนที่ติดตั้ง", + "admin_plugins_info.plugins": "ปลั๊กอินที่ติดตั้ง", + "admin_plugins_info.page-title": "ข้อมูลปลั๊กอิน - Etherpad", + "admin_plugins_info.version": "รุ่น Etherpad", + "admin_plugins_info.version_latest": "เวอร์ชันล่าสุดที่มีอยู่", + "admin_plugins_info.version_number": "หมายเลขเวอร์ชัน", + "admin_settings": "การตั้งค่า", + "admin_settings.current": "การกำหนดค่าปัจจุบัน", + "admin_settings.current_example-devel": "ตัวอย่างเทมเพลตการตั้งค่าการพัฒนา", + "admin_settings.current_example-prod": "ตัวอย่างเทมเพลตการตั้งค่าการผลิต", + "admin_settings.current_restart.value": "รีสตาร์ท Etherpad", + "admin_settings.current_save.value": "บันทึกการตั้งค่า", + "admin_settings.page-title": "การตั้งค่า - Etherpad", "index.newPad": "สร้างแผ่นจดบันทึกใหม่", "index.createOpenPad": "หรือสร้าง/เปิดแผ่นจดบันทึกที่มีชื่อ:", "index.openPad": "เปิดแพดที่มีอยู่แล้วด้วยชื่อ:", @@ -77,6 +111,8 @@ "pad.modals.deleted.explanation": "แผ่นจดบันทึกนี้ได้ถูกลบออกแล้ว", "pad.modals.rateLimited": "ถึงขีดจำกัด", "pad.modals.rateLimited.explanation": "คณส่งข้อความถึงแพดนี้มากเกินไปจึงถูกตัดการเชื่อมโดยโปรแกรมอัตโนมัติ", + "pad.modals.rejected.explanation": "เซิร์ฟเวอร์ปฏิเสธข้อความที่ส่งโดยเบราว์เซอร์ของคุณ", + "pad.modals.rejected.cause": "เซิร์ฟเวอร์อาจได้รับการอัปเดตในขณะที่คุณกำลังดูแพด หรืออาจมีข้อบกพร่องใน Etherpad ลองโหลดหน้านี้ใหม่", "pad.modals.disconnected": "คุณได้ตัดการเชื่อมต่อแล้ว", "pad.modals.disconnected.explanation": "การเชื่อมต่อกับเซิร์ฟเวอร์ถูกตัด", "pad.modals.disconnected.cause": "เซิร์ฟเวอร์อาจใช้ไม่ได้ชั่วคราว โปรดแจ้งให้ผู้ดูแลการให้บริการทราบถ้าปัญหานี้ยังคงเกิดขึ้น", diff --git a/src/locales/tr.json b/src/locales/tr.json index f9a9602f283..f3d7ae33fb6 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -44,12 +44,12 @@ "admin_settings.current": "Geçerli yapılandırma", "admin_settings.current_example-devel": "Örnek geliştirme ayarları şablonu", "admin_settings.current_example-prod": "Örnek üretim ayarları şablonu", - "admin_settings.current_restart.value": "Etherpad'ı sıfırla", + "admin_settings.current_restart.value": "Etherpad'i yeniden başlatın", "admin_settings.current_save.value": "Ayarları Kaydet", "admin_settings.page-title": "Ayarlar - Etherpad", "index.newPad": "Yeni Bloknot", "index.createOpenPad": "veya şu adla bir Bloknot oluşturun/açın:", - "index.openPad": "şu adla varolan bir Pad'i açın:", + "index.openPad": "şu adla varolan bir Bloknot'u açın:", "pad.toolbar.bold.title": "Kalın (Ctrl+B)", "pad.toolbar.italic.title": "Eğik (Ctrl+I)", "pad.toolbar.underline.title": "Altı Çizili (Ctrl+U)", @@ -65,12 +65,12 @@ "pad.toolbar.timeslider.title": "Zaman Çizelgesi", "pad.toolbar.savedRevision.title": "Düzeltmeyi Kaydet", "pad.toolbar.settings.title": "Ayarlar", - "pad.toolbar.embed.title": "Bu bloknotu Paylaş ve Göm", + "pad.toolbar.embed.title": "Bu Bloknot'u Paylaş ve Göm", "pad.toolbar.showusers.title": "Kullanıcıları bu bloknotta göster", "pad.colorpicker.save": "Kaydet", "pad.colorpicker.cancel": "İptal", "pad.loading": "Yükleniyor...", - "pad.noCookie": "Çerez bulunamadı. Lütfen tarayıcınızda çerezlere izin veriniz! Lütfen tarayıcınızda çerezlere izin verin! Oturumunuz ve ayarlarınız ziyaretler arasında kaydedilmez. Bunun nedeni, Etherpad'in bazı Tarayıcılarda bir iFrame'e dahil edilmiş olması olabilir. Lütfen Etherpad'in üst iFrame ile aynı alt alanda/alanda olduğundan emin olun", + "pad.noCookie": "Çerez bulunamadı. Lütfen tarayıcınızda çerezlere izin verin! Oturumunuz ve ayarlarınız ziyaretler arasında kaydedilmez. Bunun nedeni, bazı Tarayıcılarda Etherpad'in bir iFrame'e dahil edilmesi olabilir. Lütfen Etherpad'in üst iFrame ile aynı alt etki alanında/etki alanında olduğundan emin olun.", "pad.permissionDenied": "Bu bloknota erişmeye izniniz yok", "pad.settings.padSettings": "Bloknot Ayarları", "pad.settings.myView": "Görünümüm", @@ -83,7 +83,7 @@ "pad.settings.fontType.normal": "Olağan", "pad.settings.language": "Dil:", "pad.settings.about": "Hakkında", - "pad.settings.poweredBy": "Destekleyen", + "pad.settings.poweredBy": "Destekleyen:", "pad.importExport.import_export": "İçe/Dışa aktar", "pad.importExport.import": "Herhangi bir metin dosyası ya da belgesi yükle", "pad.importExport.importSuccessful": "Başarılı!", @@ -94,37 +94,37 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Açık Doküman Biçimi)", - "pad.importExport.abiword.innerHTML": "Yalnızca düz metin ya da HTML biçimlerini içe aktarabilirsiniz. Daha fazla gelişmiş içe aktarım özellikleri için lütfen AbiWord veya LibreOffice yükleyin.", + "pad.importExport.abiword.innerHTML": "Yalnızca düz metin veya HTML biçimlerinden içe aktarabilirsiniz. Daha gelişmiş içe aktarma özellikleri için lütfen AbiWord veya LibreOffice yükleyin .", "pad.modals.connected": "Bağlandı.", - "pad.modals.reconnecting": "Pedinize tekrar bağlanılıyor…", + "pad.modals.reconnecting": "Bloknotuza tekrar bağlanılıyor…", "pad.modals.forcereconnect": "Yeniden bağlanmaya zorla", "pad.modals.reconnecttimer": "Yeniden bağlanmaya çalışılıyor", "pad.modals.cancel": "İptal", "pad.modals.userdup": "Başka pencerede açıldı", "pad.modals.userdup.explanation": "Bu bloknot bu bilgisayarda birden fazla tarayıcı penceresinde açılmış gibi görünüyor.", "pad.modals.userdup.advice": "Bu pencereden kullanmak için yeniden bağlanın.", - "pad.modals.unauth": "Yetkili değil", + "pad.modals.unauth": "Yetkilendirilmemiş", "pad.modals.unauth.explanation": "Bu sayfayı görüntülerken izinleriniz değiştirildi. Tekrar bağlanmayı deneyin.", - "pad.modals.looping.explanation": "Eşitleme sunucusu ile iletişim sorunları yaşanıyor.", + "pad.modals.looping.explanation": "Senkronizasyon sunucusuyla iletişim sorunları yaşanıyor.", "pad.modals.looping.cause": "Belki de uygun olmayan güvenlik duvarı ya da vekil sunucu (proxy) ile bağlanmaya çalışıyorsunuz.", - "pad.modals.initsocketfail": "Sunucuya erişilemiyor.", - "pad.modals.initsocketfail.explanation": "Eşitleme sunucusuna bağlantı kurulamıyor.", + "pad.modals.initsocketfail": "Sunucuya ulaşılamıyor.", + "pad.modals.initsocketfail.explanation": "Senkronizasyon sunucusuna bağlanılamadı.", "pad.modals.initsocketfail.cause": "Bu sorun muhtemelen, tarayıcınızdan ya da internet bağlantınızdan kaynaklanıyor.", "pad.modals.slowcommit.explanation": "Sunucu yanıt vermiyor.", - "pad.modals.slowcommit.cause": "Bu hata ağ bağlantısı sebebiyle olabilir.", - "pad.modals.badChangeset.explanation": "Yaptığınız bir düzenleme eşitleme sunucusu tarafından kullanışsız/kural dışı olarak sınıflandırıldı.", - "pad.modals.badChangeset.cause": "Bunun nedeni, yanlış bir sunucu yapılandırması veya beklenmeyen başka bir davranış olabilir. Bunun bir hata olduğunu düşünüyorsanız lütfen servis yöneticisine başvurun. Düzenlemeye devam etmek için yeniden bağlanmayı deneyin.", + "pad.modals.slowcommit.cause": "Bu durum, ağ bağlantısıyla ilgili sorunlardan kaynaklanıyor olabilir.", + "pad.modals.badChangeset.explanation": "Yaptığınız bir düzenleme, senkronizasyon sunucusu tarafından yasa dışı olarak sınıflandırıldı.", + "pad.modals.badChangeset.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya başka bir beklenmeyen davranış olabilir. Bunun bir hata olduğunu düşünüyorsanız lütfen servis yöneticisi ile iletişime geçin. Düzenlemeye devam etmek için yeniden bağlanmayı deneyin.", "pad.modals.corruptPad.explanation": "Erişmeye çalıştığınız bloknot bozuk.", - "pad.modals.corruptPad.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya beklenmeyen başka bir davranış olabilir. Lütfen servis yöneticisine başvurun.", + "pad.modals.corruptPad.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya başka bir beklenmeyen davranış olabilir. Lütfen servis yöneticisine başvurun.", "pad.modals.deleted": "Silindi.", "pad.modals.deleted.explanation": "Bu bloknot kaldırılmış.", "pad.modals.rateLimited": "Oran Sınırlı.", - "pad.modals.rateLimited.explanation": "Bu pad'e çok fazla mesaj gönderdiniz, böylece bağlantı kesildi.", + "pad.modals.rateLimited.explanation": "Bu bloknota çok fazla mesaj gönderdiğiniz için bağlantı kesildi.", "pad.modals.rejected.explanation": "Sunucu, tarayıcınız tarafından gönderilen bir mesajı reddetti.", - "pad.modals.rejected.cause": "Siz pedi görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.", - "pad.modals.disconnected": "Bağlantınız koptu.", - "pad.modals.disconnected.explanation": "Sunucu bağlantısı kaybedildi", - "pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Bunun devam etmesi durumunda servis yöneticisine bildirin.", + "pad.modals.rejected.cause": "Bloknotu görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.", + "pad.modals.disconnected": "Bağlantınız kesildi.", + "pad.modals.disconnected.explanation": "Sunucuyla bağlantı kesildi", + "pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Böyle devam ederse lütfen hizmet yöneticisine bildirin.", "pad.share": "Bu bloknotu paylaş", "pad.share.readonly": "Yalnızca oku", "pad.share.link": "Bağlantı", @@ -133,20 +133,20 @@ "pad.chat.title": "Bu bloknot için sohbeti açın.", "pad.chat.loadmessages": "Daha fazla mesaj yükle", "pad.chat.stick.title": "Sohbeti ekrana yapıştır", - "pad.chat.writeMessage.placeholder": "Mesajını buraya yaz", - "timeslider.followContents": "Pad içerik güncellemelerini takip edin", + "pad.chat.writeMessage.placeholder": "Mesajınızı buraya yazın", + "timeslider.followContents": "Bloknot içerik güncellemelerini takip edin", "timeslider.pageTitle": "{{appTitle}} Zaman Çizelgesi", "timeslider.toolbar.returnbutton": "Bloknota geri dön", "timeslider.toolbar.authors": "Yazarlar:", "timeslider.toolbar.authorsList": "Yazar Yok", "timeslider.toolbar.exportlink.title": "Dışa aktar", - "timeslider.exportCurrent": "Mevcut sürümü şu olarak dışa aktar:", - "timeslider.version": "{{version}} sürümü", + "timeslider.exportCurrent": "Geçerli sürümü şu şekilde dışa aktar:", + "timeslider.version": "Sürüm {{version}}", "timeslider.saved": "{{day}} {{month}} {{year}} tarihinde kaydedildi", "timeslider.playPause": "Bloknot İçeriğini Oynat / Durdur", - "timeslider.backRevision": "Bu bloknottaki bir revizyona geri git", - "timeslider.forwardRevision": "Bu bloknatta sonraki revizyona git", - "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.backRevision": "Bu bloknottaki bir sürüme geri dön", + "timeslider.forwardRevision": "Bu bloknatta sonraki sürüme git", + "timeslider.dateformat": "{{day}}.{{month}}.{{year}} {{hours}}.{{minutes}}.{{seconds}}", "timeslider.month.january": "Ocak", "timeslider.month.february": "Şubat", "timeslider.month.march": "Mart", @@ -160,19 +160,19 @@ "timeslider.month.november": "Kasım", "timeslider.month.december": "Aralık", "timeslider.unnamedauthors": "{{num}} isimsiz {[plural(num) one: yazar, other: yazar ]}", - "pad.savedrevs.marked": "Bu düzenleme artık kayıtlı bir düzeltme olarak işaretlendi", - "pad.savedrevs.timeslider": "Zaman kaydırıcısını ziyaret ederek kaydedilen revizyonları görebilirsiniz", + "pad.savedrevs.marked": "Bu sürüm, artık kaydedilmiş bir sürüm olarak işaretlendi.", + "pad.savedrevs.timeslider": "Kaydedilmiş sürümleri, zaman kaydırıcısını ziyaret ederek görebilirsiniz.", "pad.userlist.entername": "Adınızı girin", "pad.userlist.unnamed": "isimsiz", - "pad.editbar.clearcolors": "Bütün belgedeki yazarlık renkleri silinsin mi? Bu işlem geri alınamaz", + "pad.editbar.clearcolors": "Bütün belgedeki yazarlık renkleri silinsin mi? Bu işlem geri alınamaz.", "pad.impexp.importbutton": "Şimdi İçe Aktar", - "pad.impexp.importing": "İçe aktarıyor...", - "pad.impexp.confirmimport": "Bir dosya içe aktarılırken bloknotun mevcut metninin üzerine yazdırılır. Devam etmek istediğinizden emin misiniz?", - "pad.impexp.convertFailed": "Bu dosyayı içe aktarmak mümkün değil. Lütfen farklı bir belge biçimi kullanın ya da elle kopyala yapıştır yapın", - "pad.impexp.padHasData": "Bu Pad'in zaten değişiklikleri olduğu için bu dosyayı içe aktaramadık, lütfen yeni bir bloknota aktarın", - "pad.impexp.uploadFailed": "Yükleme başarısız, lütfen tekrar deneyin", - "pad.impexp.importfailed": "İçe aktarım başarısız oldu", + "pad.impexp.importing": "İçe aktarılıyor...", + "pad.impexp.confirmimport": "Bir dosyanın içe aktarılması, bloknotun mevcut metninin üzerine yazacaktır. Devam etmek istediğinizden emin misiniz?", + "pad.impexp.convertFailed": "Bu dosyayı içe aktaramadık. Lütfen farklı bir belge biçimi kullanın veya elle kopyalayıp yapıştırın", + "pad.impexp.padHasData": "Bu bloknotta zaten değişiklikler olduğu için bu dosyayı içe aktaramadık, lütfen yeni bir bloknota aktarın.", + "pad.impexp.uploadFailed": "Yükleme başarısız oldu, lütfen tekrar deneyin.", + "pad.impexp.importfailed": "İçe aktarılamadı", "pad.impexp.copypaste": "Lütfen kopyala yapıştır yapın", - "pad.impexp.exportdisabled": "{{type}} biçimiyle dışa aktarma devre dışı bırakıldı. Ayrıntılar için sistem yöneticinizle iletişime geçiniz.", + "pad.impexp.exportdisabled": "{{type}} biçiminde dışa aktarma devre dışı bırakıldı. Ayrıntılar için lütfen sistem yöneticinize başvurun.", "pad.impexp.maxFileSize": "Dosya çok büyük. İçe aktarma için izin verilen dosya boyutunu artırmak için site yöneticinize başvurun" } diff --git a/src/locales/uk.json b/src/locales/uk.json index 3604e38b961..994c32ba33f 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -4,6 +4,7 @@ "Andriykopanytsia", "Base", "Bunyk", + "DDPAT", "Lxlalexlxl", "Movses", "Olvin", @@ -31,6 +32,10 @@ "admin_plugins.page-title": "Менеджер плагінів — Etherpad", "admin_plugins.version": "Версія", "admin_plugins_info": "Інформація щодо виправлення неполадок", + "admin_plugins_info.hooks": "Встановлені гачки", + "admin_plugins_info.hooks_client": "Гачки на стороні клієнта", + "admin_plugins_info.hooks_server": "Серверні гачки", + "admin_plugins_info.parts": "Встановлені деталі", "admin_plugins_info.plugins": "Встановлені плагіни", "admin_plugins_info.page-title": "Інформація про плагіни — Etherpad", "admin_plugins_info.version": "Версія Etherpad", @@ -38,6 +43,8 @@ "admin_plugins_info.version_number": "Номер версії", "admin_settings": "Налаштування", "admin_settings.current": "Поточна конфігурація", + "admin_settings.current_example-devel": "Приклад шаблону налаштувань розробки", + "admin_settings.current_example-prod": "Приклад шаблону налаштувань виробництва", "admin_settings.current_restart.value": "Перезапустити Etherpad", "admin_settings.current_save.value": "Зберегти налаштування", "admin_settings.page-title": "Налаштування — Etherpad", @@ -64,10 +71,10 @@ "pad.colorpicker.save": "Зберегти", "pad.colorpicker.cancel": "Скасувати", "pad.loading": "Завантаження…", - "pad.noCookie": "Реп'яшки не знайдено. Будь ласка, увімкніть реп'яшки у вашому браузері! Ваша сесія та налаштування не зберігатимуться між візитами. Це може спричинятися тим, що Etherpad у деяких браузерах включений через iFrame. Будь ласка, переконайтеся, що iFrame міститься на тому ж піддомені/домені, що й батьківський iFrame", + "pad.noCookie": "Cookie не знайдено. Будь ласка, увімкніть cookie у вашому браузері! Ваша сесія та налаштування не зберігатимуться між візитами. Це може спричинятися тим, що Etherpad у деяких браузерах включений через iFrame. Будь ласка, переконайтеся, що iFrame міститься на тому ж піддомені/домені, що й батьківський iFrame", "pad.permissionDenied": "У Вас немає дозволу для доступу до цього документа", "pad.settings.padSettings": "Налаштування документа", - "pad.settings.myView": "Мій Вигляд", + "pad.settings.myView": "Мій погляд", "pad.settings.stickychat": "Завжди відображувати чат", "pad.settings.chatandusers": "Показати чат і користувачів", "pad.settings.colorcheck": "Кольори авторства", @@ -98,7 +105,7 @@ "pad.modals.userdup.explanation": "Документ, можливо, відкрито більш ніж в одному вікні браузера на цьому комп'ютері.", "pad.modals.userdup.advice": "Перепідключитись використовуючи це вікно.", "pad.modals.unauth": "Не авторизовано", - "pad.modals.unauth.explanation": "Ваші права було змінено під час перегляду цієї сторінк. Спробуйте перепідключитись.", + "pad.modals.unauth.explanation": "Ваші права було змінено під час перегляду цієї сторінки. Спробуйте відновити зв’язок.", "pad.modals.looping.explanation": "Проблеми зв'єзку з сервером синхронізації.", "pad.modals.looping.cause": "Можливо, підключились через несумісний брандмауер або проксі-сервер.", "pad.modals.initsocketfail": "Сервер недоступний.", @@ -115,7 +122,7 @@ "pad.modals.rateLimited": "Швидкість обмежено.", "pad.modals.rateLimited.explanation": "Ви надіслали надто багато повідомлень у цей документ, тому він вас від'єднав.", "pad.modals.rejected.explanation": "Сервер відхилив повідомлення, надіслане вашим браузером.", - "pad.modals.rejected.cause": "Сервер міг оновитися, поки ви переглядали документ, а може це баг в Etherpad'і. Спробуйте перезавантажити сторінку.", + "pad.modals.rejected.cause": "Сервер міг оновитися, поки ви переглядали документ, а може це помилка у Etherpad'і. Спробуйте перезавантажити сторінку.", "pad.modals.disconnected": "Вас було від'єднано.", "pad.modals.disconnected.explanation": "З'єднання з сервером втрачено", "pad.modals.disconnected.cause": "Сервер, можливо, недоступний. Будь ласка, повідомте адміністратора служби, якщо це повторюватиметься.", @@ -137,7 +144,7 @@ "timeslider.exportCurrent": "Експортувати поточну версію як:", "timeslider.version": "Версія {{version}}", "timeslider.saved": "Збережено {{month}} {{day}}, {{year}}", - "timeslider.playPause": "Відтворення / Пауза Панель Зміст", + "timeslider.playPause": "Вміст панелі відтворення/паузи", "timeslider.backRevision": "Переглянути попередню ревізію цієї панелі", "timeslider.forwardRevision": "Переглянути наступну ревізію цієї панелі", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", @@ -155,8 +162,8 @@ "timeslider.month.december": "Грудень", "timeslider.unnamedauthors": "{{num}} {[plural(num) one: безіменний автор, few: безіменні автори, many: безіменних авторів, other: безіменних авторів]}", "pad.savedrevs.marked": "Цю версію помічено збереженою версією", - "pad.savedrevs.timeslider": "Ви можете побачити збережені ревізії, відвідавши \"Слайдер Змін Ревізій\"", - "pad.userlist.entername": "Введіть Ваше ім'я", + "pad.savedrevs.timeslider": "Ви можете побачити збережені ревізії, відвідавши «Слайдер Змін Ревізій»", + "pad.userlist.entername": "Введіть ваше ім'я", "pad.userlist.unnamed": "безіменний", "pad.editbar.clearcolors": "Очистити кольори у всьому документі? Це не можна буде відкотити", "pad.impexp.importbutton": "Імпортувати зараз", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index 7a5d46e39de..e2dca09a8c2 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -3,6 +3,7 @@ "authors": [ "94rain", "Dimension", + "GuoPC", "Hydra", "Hzy980512", "JuneAugust", @@ -67,7 +68,7 @@ "pad.importExport.exportopen": "ODF(开放文档格式)", "pad.importExport.abiword.innerHTML": "您只可以导入纯文本或HTML格式。要获取更高级的导入功能,请安装 AbiWord 或是 LibreOffice。", "pad.modals.connected": "已连接。", - "pad.modals.reconnecting": "重新连接到您的记事本...", + "pad.modals.reconnecting": "重新连接到您的记事本…", "pad.modals.forcereconnect": "强制重新连接", "pad.modals.reconnecttimer": "尝试重新连入", "pad.modals.cancel": "取消", diff --git a/src/node/db/API.js b/src/node/db/API.js index f119f58479b..d4886755871 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -20,6 +20,7 @@ */ const Changeset = require('../../static/js/Changeset'); +const ChatMessage = require('../../static/js/ChatMessage'); const CustomError = require('../utils/customError'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); @@ -364,7 +365,7 @@ exports.appendChatMessage = async (padID, text, authorID, time) => { // @TODO - missing getPadSafe() call ? // save chat message to database and send message to all connected clients - await padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); + await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID); }; /* *************** diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index db35daf19e3..677e9c01441 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -5,6 +5,7 @@ const Changeset = require('../../static/js/Changeset'); +const ChatMessage = require('../../static/js/ChatMessage'); const AttributePool = require('../../static/js/AttributePool'); const db = require('./DB'); const settings = require('../utils/Settings'); @@ -258,7 +259,7 @@ Pad.prototype.setText = async function (newText) { } // append the changeset - await this.appendRevision(changeset); + if (newText !== oldText) await this.appendRevision(changeset); }; Pad.prototype.appendText = async function (newText) { @@ -274,53 +275,61 @@ Pad.prototype.appendText = async function (newText) { await this.appendRevision(changeset); }; -Pad.prototype.appendChatMessage = async function (text, userId, time) { +/** + * Adds a chat message to the pad, including saving it to the database. + * + * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a string + * containing the raw text of the user's chat message (deprecated). + * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId` instead. + * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use + * `msgOrText.time` instead. + */ +Pad.prototype.appendChatMessage = async function (msgOrText, authorId = null, time = null) { + const msg = + msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); this.chatHead++; - // save the chat entry in the database await Promise.all([ - db.set(`pad:${this.id}:chat:${this.chatHead}`, {text, userId, time}), + // Don't save the display name in the database because the user can change it at any time. The + // `displayName` property will be populated with the current value when the message is read from + // the database. + db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), this.saveToDatabase(), ]); }; +/** + * @param {number} entryNum - ID of the desired chat message. + * @returns {?ChatMessage} + */ Pad.prototype.getChatMessage = async function (entryNum) { - // get the chat entry const entry = await db.get(`pad:${this.id}:chat:${entryNum}`); - - // get the authorName if the entry exists - if (entry != null) { - entry.userName = await authorManager.getAuthorName(entry.userId); - } - - return entry; + if (entry == null) return null; + const message = ChatMessage.fromObject(entry); + message.displayName = await authorManager.getAuthorName(message.authorId); + return message; }; +/** + * @param {number} start - ID of the first desired chat message. + * @param {number} end - ID of the last desired chat message. + * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end` + * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open + * interval as is typical in code. + */ Pad.prototype.getChatMessages = async function (start, end) { - // collect the numbers of chat entries and in which order we need them - const neededEntries = []; - for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) { - neededEntries.push({entryNum, order}); - } - - // get all entries out of the database - const entries = []; - await Promise.all( - neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => { - entries[entryObject.order] = entry; - }))); + const entries = await Promise.all( + [...Array(end + 1 - start).keys()].map((i) => this.getChatMessage(start + i))); // sort out broken chat entries // it looks like in happened in the past that the chat head was // incremented, but the chat message wasn't added - const cleanedEntries = entries.filter((entry) => { + return entries.filter((entry) => { const pass = (entry != null); if (!pass) { console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); } return pass; }); - - return cleanedEntries; }; Pad.prototype.init = async function (text) { @@ -490,7 +499,6 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) { // based on Changeset.makeSplice const assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', ''); Changeset.appendATextToAssembler(oldAText, assem); assem.endDocument(); diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 9a222e2fb01..b5f93094d0f 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -255,7 +255,7 @@ const listSessionsWithDBKey = async (dbkey) => { const sessionInfo = await exports.getSessionInfo(sessionID); sessions[sessionID] = sessionInfo; } catch (err) { - if (err === 'apierror: sessionID does not exist') { + if (err.name === 'apierror') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); sessions[sessionID] = null; } else { diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 0bf75a17f11..f3fde047cd5 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -56,14 +56,14 @@ exports.doExport = async (req, res, padId, readOnlyId, type) => { // if this is a plain text export, we can do this directly // We have to over engineer this because tabs are stored as attributes and not plain text if (type === 'etherpad') { - const pad = await exportEtherpad.getPadRaw(padId); + const pad = await exportEtherpad.getPadRaw(padId, readOnlyId); res.send(pad); } else if (type === 'txt') { const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); res.send(txt); } else { // render the html document - let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev); + let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId); // decide what to do with the html export diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index acaaf092707..c865dcf9836 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -142,11 +142,8 @@ const doImport = async (req, res, padId) => { } const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); - - // Logic for allowing external Import Plugins - const result = await hooks.aCallAll('import', {srcFile, destFile, fileEnding}); - const importHandledByPlugin = (result.length > 0); // This feels hacky and wrong.. - + const importHandledByPlugin = + (await hooks.aCallAll('import', {srcFile, destFile, fileEnding, padId})).some((x) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); @@ -235,8 +232,8 @@ const doImport = async (req, res, padId) => { pad = await padManager.getPad(padId); padManager.unloadPad(padId); - // direct Database Access means a pad user should perform a switchToPad - // and not attempt to receive updated pad data + // Direct database access means a pad user should reload the pad and not attempt to receive + // updated pad data. if (directDatabaseAccess) return true; // tell clients to update diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a1194ea9192..0735ce97fc5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -21,6 +21,7 @@ const padManager = require('../db/PadManager'); const Changeset = require('../../static/js/Changeset'); +const ChatMessage = require('../../static/js/ChatMessage'); const AttributePool = require('../../static/js/AttributePool'); const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); @@ -33,14 +34,15 @@ const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); const _ = require('underscore'); const hooks = require('../../static/js/pluginfw/hooks.js'); -const channels = require('channels'); const stats = require('../stats'); const assert = require('assert').strict; -const nodeify = require('nodeify'); 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'; exports.socketio = () => { // The rate limiter is created in this hook so that restarting the server resets the limiter. The @@ -63,14 +65,14 @@ exports.socketio = () => { * - token: User-supplied token. * - author: The user's author ID. * - padId: The real (not read-only) ID of the pad. - * - readonlyPadId: The read-only ID of the pad. + * - readOnlyPadId: The read-only ID of the pad. * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ const sessioninfos = {}; exports.sessioninfos = sessioninfos; -stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); +stats.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0); stats.gauge('activePads', () => { const padIds = new Set(); for (const {padId} of Object.values(sessioninfos)) { @@ -81,16 +83,43 @@ stats.gauge('activePads', () => { }); /** - * A changeset queue per pad that is processed by handleUserChanges() + * Processes one task at a time per channel. */ -const padChannels = new channels.channels( - ({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback) -); +class Channels { + /** + * @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be + * functions that will be executed with the channel as the only argument. + */ + constructor(exec = (ch, task) => task(ch)) { + this._exec = exec; + this._promiseChains = new Map(); + } + + /** + * Schedules a task for execution. The task will be executed once all previously enqueued tasks + * for the named channel have completed. + * + * @param {any} ch - Identifies the channel. + * @param {any} task - The task to give to the executor. + * @returns {Promise} The value returned by the executor. + */ + async enqueue(ch, task) { + const p = (this._promiseChains.get(ch) || Promise.resolve()).then(() => this._exec(ch, task)); + const pc = p + .catch(() => {}) // Prevent rejections from halting the queue. + .then(() => { + // Clean up this._promiseChains if there are no more tasks for the channel. + if (this._promiseChains.get(ch) === pc) this._promiseChains.delete(ch); + }); + this._promiseChains.set(ch, pc); + return await p; + } +} /** - * Saves the Socket class we need to send and receive data from the client + * A changeset queue per pad that is processed by handleUserChanges() */ -let socketio; +const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(socket, message)); /** * This Method is called by server.js to tell the message handler on which socket it should send @@ -130,45 +159,35 @@ exports.kickSessionsFromPad = (padID) => { */ exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); - - // save the padname of this session const session = sessioninfos[socket.id]; - - // if this connection was already etablished with a handshake, - // send a disconnect message to the others - if (session && session.author) { - const {session: {user} = {}} = socket.client.request; - accessLogger.info(`${'[LEAVE]' + - ` pad:${session.padId}` + - ` socket:${socket.id}` + - ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${session.author}`}${ - (user && user.username) ? ` username:${user.username}` : ''}`); - - // get the author color out of the db - const color = await authorManager.getAuthorColorId(session.author); - - // prepare the notification for the other users on the pad, that this user left - const messageToTheOtherUsers = { - type: 'COLLABROOM', - data: { - type: 'USER_LEAVE', - userInfo: { - colorId: color, - userId: session.author, - }, - }, - }; - - // Go through all user that are still on the pad, and send them the USER_LEAVE message - socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); - - // Allow plugins to hook into users leaving the pad - hooks.callAll('userLeave', session); - } - - // Delete the sessioninfos entrys of this session delete sessioninfos[socket.id]; + // session.padId can be nullish if the user disconnects before sending CLIENT_READY. + if (!session || !session.author || !session.padId) return; + const {session: {user} = {}} = socket.client.request; + /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ + accessLogger.info('[LEAVE]' + + ` pad:${session.padId}` + + ` socket:${socket.id}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + + ` authorID:${session.author}` + + (user && user.username ? ` username:${user.username}` : '')); + /* eslint-enable prefer-template */ + socket.broadcast.to(session.padId).json.send({ + type: 'COLLABROOM', + data: { + type: 'USER_LEAVE', + userInfo: { + colorId: await authorManager.getAuthorColorId(session.author), + userId: session.author, + }, + }, + }); + await hooks.aCallAll('userLeave', { + ...session, // For backwards compatibility. + authorId: session.author, + readOnly: session.readonly, + socket, + }); }; /** @@ -183,8 +202,8 @@ exports.handleMessage = async (socket, message) => { try { await rateLimiter.consume(socket.request.ip); // consume 1 point per event from IP } catch (e) { - console.warn(`Rate limited: ${socket.request.ip} to reduce the amount of rate limiting ` + - 'that happens edit the rateLimit values in settings.json'); + messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` + + 'limiting that happens edit the rateLimit values in settings.json'); stats.meter('rateLimited').mark(); socket.json.send({disconnect: 'rateLimited'}); return; @@ -207,8 +226,14 @@ exports.handleMessage = async (socket, message) => { } if (message.type === 'CLIENT_READY') { - // client tried to auth for the first time (first msg from the client) - createSessionInfoAuth(thisSession, message); + // Remember this information since we won't have the cookie in further socket.io messages. This + // information will be used to check if the sessionId of this connection is still valid since it + // could have been deleted by the API. + thisSession.auth = { + sessionID: message.sessionID, + padID: message.padId, + token: message.token, + }; } const auth = thisSession.auth; @@ -263,7 +288,7 @@ exports.handleMessage = async (socket, message) => { // Check what type of message we get and delegate to the other methods if (message.type === 'CLIENT_READY') { - await handleClientReady(socket, message, authorID); + await handleClientReady(socket, message); } else if (message.type === 'CHANGESET_REQ') { await handleChangesetRequest(socket, message); } else if (message.type === 'COLLABROOM') { @@ -271,7 +296,7 @@ exports.handleMessage = async (socket, message) => { messageLogger.warn('Dropped message, COLLABROOM for readonly pad'); } else if (message.data.type === 'USER_CHANGES') { stats.counter('pendingEdits').inc(); - padChannels.emit(message.padId, {socket, message}); // add to pad queue + await padChannels.enqueue(message.padId, {socket, message}); } else if (message.data.type === 'USERINFO_UPDATE') { await handleUserInfoUpdate(socket, message); } else if (message.data.type === 'CHAT_MESSAGE') { @@ -287,8 +312,6 @@ exports.handleMessage = async (socket, message) => { } else { messageLogger.warn(`Dropped message, unknown COLLABROOM Data Type ${message.data.type}`); } - } else if (message.type === 'SWITCH_TO_PAD') { - await handleSwitchToPad(socket, message, authorID); } else { messageLogger.warn(`Dropped message, unknown Message Type ${message.type}`); } @@ -349,37 +372,38 @@ exports.handleCustomMessage = (padID, msgString) => { * @param message the message from the client */ const handleChatMessage = async (socket, message) => { - const time = Date.now(); - const text = message.data.text; + const chatMessage = ChatMessage.fromObject(message.data.message); const {padId, author: authorId} = sessioninfos[socket.id]; - await exports.sendChatMessageToPadClients(time, authorId, text, padId); + // Don't trust the user-supplied values. + chatMessage.time = Date.now(); + chatMessage.authorId = authorId; + await exports.sendChatMessageToPadClients(chatMessage, padId); }; /** - * Sends a chat message to all clients of this pad - * @param time the timestamp of the chat message - * @param userId the author id of the chat message - * @param text the text of the chat message - * @param padId the padId to send the chat message to + * Adds a new chat message to a pad and sends it to connected clients. + * + * @param {(ChatMessage|number)} mt - Either a chat message object (recommended) or the timestamp of + * the chat message in ms since epoch (deprecated). + * @param {string} puId - If `mt` is a chat message object, this is the destination pad ID. + * Otherwise, this is the user's author ID (deprecated). + * @param {string} [text] - The text of the chat message. Deprecated; use `mt.text` instead. + * @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message + * object as the first argument and the destination pad ID as the second argument instead. */ -exports.sendChatMessageToPadClients = async (time, userId, text, padId) => { - // get the pad +exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => { + const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); + padId = mt instanceof ChatMessage ? puId : padId; const pad = await padManager.getPad(padId); - - // get the author - const userName = await authorManager.getAuthorName(userId); - - // save the chat message - const promise = pad.appendChatMessage(text, userId, time); - - const msg = { + await hooks.aCallAll('chatNewMessage', {message, pad, padId}); + // pad.appendChatMessage() ignores the displayName property so we don't need to wait for + // authorManager.getAuthorName() to resolve before saving the message to the database. + const promise = pad.appendChatMessage(message); + message.displayName = await authorManager.getAuthorName(message.userId); + socketio.sockets.in(padId).json.send({ type: 'COLLABROOM', - data: {type: 'CHAT_MESSAGE', userId, userName, time, text}, - }; - - // broadcast the chat message to everyone on the pad - socketio.sockets.in(padId).json.send(msg); - + data: {type: 'CHAT_MESSAGE', message}, + }); await promise; }; @@ -536,22 +560,6 @@ const handleUserChanges = async (socket, message) => { // This one's no longer pending, as we're gonna process it now stats.counter('pendingEdits').dec(); - // Make sure all required fields are present - if (message.data.baseRev == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no baseRev!'); - return; - } - - if (message.data.apool == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no apool!'); - return; - } - - if (message.data.changeset == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no changeset!'); - return; - } - // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. const thisSession = sessioninfos[socket.id]; @@ -560,75 +568,64 @@ const handleUserChanges = async (socket, message) => { // and always use the copy. atm a message will be ignored if the session is gone even // if the session was valid when the message arrived in the first place if (!thisSession) { - messageLogger.warn('Dropped message, disconnect happened in the mean time'); + messageLogger.warn('Ignoring USER_CHANGES from disconnected user'); return; } - // get all Vars we need - const baseRev = message.data.baseRev; - const wireApool = (new AttributePool()).fromJsonable(message.data.apool); - let changeset = message.data.changeset; - // Measure time to process edit const stopWatch = stats.timer('edits').start(); - - // get the pad - const pad = await padManager.getPad(thisSession.padId); - - // create the changeset try { - try { - // Verify that the changeset has valid syntax and is in canonical form - Changeset.checkRep(changeset); - - // Verify that the attribute indexes used in the changeset are all - // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, (n) => { - if (!wireApool.getAttrib(n)) { - throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); - } - }); + const {data: {baseRev, apool, changeset}} = message; + if (baseRev == null) throw new Error('missing baseRev'); + if (apool == null) throw new Error('missing apool'); + if (changeset == null) throw new Error('missing changeset'); + const wireApool = (new AttributePool()).fromJsonable(apool); + const pad = await padManager.getPad(thisSession.padId); + + // Verify that the changeset has valid syntax and is in canonical form + Changeset.checkRep(changeset); + + // Verify that the attribute indexes used in the changeset are all + // defined in the accompanying attribute pool. + Changeset.eachAttribNumber(changeset, (n) => { + if (!wireApool.getAttrib(n)) { + throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); + } + }); - // Validate all added 'author' attribs to be the same value as the current user - const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); - let op; + // Validate all added 'author' attribs to be the same value as the current user + const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); + let op; - while (iterator.hasNext()) { - op = iterator.next(); + while (iterator.hasNext()) { + op = iterator.next(); - // + can add text with attribs - // = can change or add attribs - // - can have attribs, but they are discarded and don't show up in the attribs - - // but do show up in the pool + // + can add text with attribs + // = can change or add attribs + // - can have attribs, but they are discarded and don't show up in the attribs - + // but do show up in the pool - op.attribs.split('*').forEach((attr) => { - if (!attr) return; + op.attribs.split('*').forEach((attr) => { + if (!attr) return; - attr = wireApool.getAttrib(attr); - if (!attr) return; + attr = wireApool.getAttrib(Changeset.parseNum(attr)); + if (!attr) return; - // the empty author is used in the clearAuthorship functionality so this - // should be the only exception - if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { - throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + - `${attr[1]} in changeset ${changeset}`); - } - }); - } + // the empty author is used in the clearAuthorship functionality so this + // should be the only exception + if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { + throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + + `${attr[1]} in changeset ${changeset}`); + } + }); + } - // ex. adoptChangesetAttribs + // ex. adoptChangesetAttribs - // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - } catch (e) { - // There is an error in this changeset, so just refuse it - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw new Error(`Can't apply USER_CHANGES from Socket ${socket.id} because: ${e.message}`); - } + // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool + let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); // ex. applyUserChanges - const apool = pad.pool; let r = baseRev; // The client's changeset might not be based on the latest revision, @@ -644,40 +641,23 @@ const handleUserChanges = async (socket, message) => { // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - try { - // a changeset can be based on an old revision with the same changes in it - // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES - // of that revision - if (baseRev + 1 === r && c === changeset) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw new Error("Won't apply USER_CHANGES, as it contains an already accepted changeset"); - } + // a changeset can be based on an old revision with the same changes in it + // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES + // of that revision + if (baseRev + 1 === r && c === changeset) throw new Error('Changeset already accepted'); - changeset = Changeset.follow(c, changeset, false, apool); - } catch (e) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw new Error(`Can't apply USER_CHANGES, because ${e.message}`); - } + rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool); } const prevText = pad.text(); - if (Changeset.oldLen(changeset) !== prevText.length) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ` + - `${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); + if (Changeset.oldLen(rebasedChangeset) !== prevText.length) { + throw new Error( + `Can't apply changeset ${rebasedChangeset} with oldLen ` + + `${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`); } - try { - await pad.appendRevision(changeset, thisSession.author); - } catch (e) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw e; - } + await pad.appendRevision(rebasedChangeset, thisSession.author); const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { @@ -692,10 +672,13 @@ const handleUserChanges = async (socket, message) => { await exports.updatePadClients(pad); } catch (err) { - console.warn(err.stack || err); + socket.json.send({disconnect: 'badChangeset'}); + stats.meter('failedChangesets').mark(); + messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + + `(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`); + } finally { + stopWatch.end(); } - - stopWatch.end(); }; exports.updatePadClients = async (pad) => { @@ -810,58 +793,6 @@ const _correctMarkersInPad = (atext, apool) => { return builder.toString(); }; -const handleSwitchToPad = async (socket, message, _authorID) => { - const currentSessionInfo = sessioninfos[socket.id]; - const padId = currentSessionInfo.padId; - - // Check permissions for the new pad. - const newPadIds = await readOnlyManager.getIds(message.padId); - const {session: {user} = {}} = socket.client.request; - const {accessStatus, authorID} = await securityManager.checkAccess( - newPadIds.padId, message.sessionID, message.token, user); - if (accessStatus !== 'grant') { - // Access denied. Send the reason to the user. - socket.json.send({accessStatus}); - return; - } - // The same token and session ID were passed to checkAccess in handleMessage, so this second call - // to checkAccess should return the same author ID. - assert(authorID === _authorID); - assert(authorID === currentSessionInfo.author); - - // Check if the connection dropped during the access check. - if (sessioninfos[socket.id] !== currentSessionInfo) return; - - // clear the session and leave the room - _getRoomSockets(padId).forEach((socket) => { - const sinfo = sessioninfos[socket.id]; - if (sinfo && sinfo.author === currentSessionInfo.author) { - // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; - socket.leave(padId); - } - }); - - // start up the new pad - const newSessionInfo = sessioninfos[socket.id]; - createSessionInfoAuth(newSessionInfo, message); - await handleClientReady(socket, message, authorID); -}; - -// Creates/replaces the auth object in the given session info. -const createSessionInfoAuth = (sessionInfo, message) => { - // Remember this information since we won't - // have the cookie in further socket.io messages. - // This information will be used to check if - // the sessionId of this connection is still valid - // since it could have been deleted by the API. - sessionInfo.auth = { - sessionID: message.sessionID, - padID: message.padId, - token: message.token, - }; -}; - /** * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client * to the server. The Client sends his token @@ -869,42 +800,33 @@ const createSessionInfoAuth = (sessionInfo, message) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleClientReady = async (socket, message, authorID) => { - // check if all ok - if (!message.token) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); - return; - } +const handleClientReady = async (socket, message) => { + const sessionInfo = sessioninfos[socket.id]; + // Check if the user has already disconnected. + if (sessionInfo == null) return; + assert(sessionInfo.author); - if (!message.padId) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no padId!'); - return; - } + const padIds = await readOnlyManager.getIds(sessionInfo.auth.padID); + sessionInfo.padId = padIds.padId; + sessionInfo.readOnlyPadId = padIds.readOnlyPadId; + sessionInfo.readonly = + padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); - if (!message.protocolVersion) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no protocolVersion!'); - return; - } + await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context. - if (message.protocolVersion !== 2) { - messageLogger.warn('Dropped message, CLIENT_READY Message has a unknown protocolVersion ' + - `'${message.protocolVersion}'!`); - return; + let {colorId: authorColorId, name: authorName} = message.userInfo || {}; + if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) { + messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`); + authorColorId = null; } - - hooks.callAll('clientReady', message); - - // Get ro/rw id:s - const padIds = await readOnlyManager.getIds(message.padId); - - // get all authordata of this new user - assert(authorID); - const value = await authorManager.getAuthor(authorID); - const authorColorId = value.colorId; - const authorName = value.name; + await Promise.all([ + authorName && authorManager.setAuthorName(sessionInfo.author, authorName), + authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId), + ]); + ({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author)); // load the pad-object from the database - const pad = await padManager.getPad(padIds.padId); + const pad = await padManager.getPad(sessionInfo.padId); // these db requests all need the pad object (timestamp of latest revision, author data) const authors = pad.getAllAuthors(); @@ -914,7 +836,8 @@ const handleClientReady = async (socket, message, authorID) => { // get all author data out of the database (in parallel) const historicalAuthorData = {}; - await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => { + await Promise.all(authors.map(async (authorId) => { + const author = await authorManager.getAuthor(authorId); if (!author) { messageLogger.error(`There is no author for authorId: ${authorId}. ` + 'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); @@ -922,45 +845,42 @@ const handleClientReady = async (socket, message, authorID) => { // Filter author attribs (e.g. don't send author's pads to all clients) historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; } - }))); + })); // glue the clientVars together, send them and tell the other clients that a new one is there - // Check that the client is still here. It might have disconnected between callbacks. - const sessionInfo = sessioninfos[socket.id]; - if (sessionInfo == null) return; + // Check if the user has disconnected during any of the above awaits. + if (sessionInfo !== sessioninfos[socket.id]) return; // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); - for (const socket of roomSockets) { - const sinfo = sessioninfos[socket.id]; - if (sinfo && sinfo.author === authorID) { + for (const otherSocket of roomSockets) { + // The user shouldn't have joined the room yet, but check anyway just in case. + if (otherSocket.id === socket.id) continue; + const sinfo = sessioninfos[otherSocket.id]; + if (sinfo && sinfo.author === sessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; - socket.leave(padIds.padId); - socket.json.send({disconnect: 'userdup'}); + sessioninfos[otherSocket.id] = {}; + otherSocket.leave(sessionInfo.padId); + otherSocket.json.send({disconnect: 'userdup'}); } } - // Save in sessioninfos that this session belonges to this pad - sessionInfo.padId = padIds.padId; - sessionInfo.readOnlyPadId = padIds.readOnlyPadId; - sessionInfo.readonly = - padIds.readonly || !webaccess.userCanModify(message.padId, socket.client.request); - const {session: {user} = {}} = socket.client.request; - accessLogger.info(`${`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + - ` pad:${padIds.padId}` + + /* 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}` + - ` authorID:${authorID}`}${ - (user && user.username) ? ` username:${user.username}` : ''}`); + ` authorID:${sessionInfo.author}` + + (user && user.username ? ` username:${user.username}` : '')); + /* eslint-enable prefer-template */ if (message.reconnect) { // If this is a reconnect, we don't have to send the client the ClientVars again // Join the pad and start receiving updates - socket.join(padIds.padId); + socket.join(sessionInfo.padId); // Save the revision in sessioninfos, we take the revision from the info the client send to us sessionInfo.rev = message.client_rev; @@ -989,15 +909,14 @@ const handleClientReady = async (socket, message, authorID) => { changesets[r] = {}; } - // get changesets, author and timestamp needed for pending revisions (in parallel) - const promises = []; - for (const revNum of revisionsNeeded) { + await Promise.all(revisionsNeeded.map(async (revNum) => { const cs = changesets[revNum]; - promises.push(pad.getRevisionChangeset(revNum).then((result) => cs.changeset = result)); - promises.push(pad.getRevisionAuthor(revNum).then((result) => cs.author = result)); - promises.push(pad.getRevisionDate(revNum).then((result) => cs.timestamp = result)); - } - await Promise.all(promises); + [cs.changeset, cs.author, cs.timestamp] = await Promise.all([ + pad.getRevisionChangeset(revNum), + pad.getRevisionAuthor(revNum), + pad.getRevisionDate(revNum), + ]); + })); // return pending changesets for (const r of revisionsNeeded) { @@ -1031,15 +950,14 @@ const handleClientReady = async (socket, message, authorID) => { apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } catch (e) { - console.error(e.stack || e); + messageLogger.error(e.stack || e); socket.json.send({disconnect: 'corruptPad'}); // pull the brakes return; } - // Warning: never ever send padIds.padId to the client. If the - // client is read only you would open a security hole 1 swedish - // mile wide... + // Warning: never ever send sessionInfo.padId to the client. If the client is read only you + // would open a security hole 1 swedish mile wide... const clientVars = { skinName: settings.skinName, skinVariants: settings.skinVariants, @@ -1054,7 +972,7 @@ const handleClientReady = async (socket, message, authorID) => { collab_client_vars: { initialAttributedText: atext, clientIp: '127.0.0.1', - padId: message.padId, + padId: sessionInfo.auth.padID, historicalAuthorData, apool, rev: pad.getHeadRevisionNumber(), @@ -1063,19 +981,19 @@ const handleClientReady = async (socket, message, authorID) => { colorPalette: authorManager.getColorPalette(), clientIp: '127.0.0.1', userColor: authorColorId, - padId: message.padId, + padId: sessionInfo.auth.padID, padOptions: settings.padOptions, padShortcutEnabled: settings.padShortcutEnabled, - initialTitle: `Pad: ${message.padId}`, + initialTitle: `Pad: ${sessionInfo.auth.padID}`, opts: {}, // tell the client the number of the latest chat-message, which will be // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) chatHead: pad.chatHead, numConnectedUsers: roomSockets.length, - readOnlyId: padIds.readOnlyPadId, + readOnlyId: sessionInfo.readOnlyPadId, readonly: sessionInfo.readonly, serverTimestamp: Date.now(), - userId: authorID, + userId: sessionInfo.author, abiwordAvailable: settings.abiwordAvailable(), sofficeAvailable: settings.sofficeAvailable(), exportAvailable: settings.exportAvailable(), @@ -1114,7 +1032,7 @@ const handleClientReady = async (socket, message, authorID) => { } // Join the pad and start receiving updates - socket.join(padIds.padId); + socket.join(sessionInfo.padId); // Send the clientVars to the Client socket.json.send({type: 'CLIENT_VARS', data: clientVars}); @@ -1124,14 +1042,14 @@ const handleClientReady = async (socket, message, authorID) => { } // Notify other users about this new user. - socket.broadcast.to(padIds.padId).json.send({ + socket.broadcast.to(sessionInfo.padId).json.send({ type: 'COLLABROOM', data: { type: 'USER_NEWINFO', userInfo: { colorId: authorColorId, name: authorName, - userId: authorID, + userId: sessionInfo.author, }, }, }); @@ -1176,6 +1094,15 @@ const handleClientReady = async (socket, message, authorID) => { socket.json.send(msg); })); + + await hooks.aCallAll('userJoin', { + authorId: sessionInfo.author, + displayName: authorName, + padId: sessionInfo.padId, + readOnly: sessionInfo.readonly, + readOnlyPadId: sessionInfo.readOnlyPadId, + socket, + }); }; /** @@ -1226,8 +1153,8 @@ const handleChangesetRequest = async (socket, message) => { data.requestID = message.data.requestID; socket.json.send({type: 'CHANGESET_REQ', data}); } catch (err) { - console.error(`Error while handling a changeset request for ${padIds.padId}`, - err.toString(), message.data); + messageLogger.error(`Error while handling a changeset request ${message.data} ` + + `for ${padIds.padId}: ${err.stack || err}`); } }; @@ -1237,12 +1164,10 @@ const handleChangesetRequest = async (socket, message) => { */ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { const pad = await padManager.getPad(padId); - const head_revision = pad.getHeadRevisionNumber(); + const headRevision = pad.getHeadRevisionNumber(); // calculate the last full endnum - if (endNum > head_revision + 1) { - endNum = head_revision + 1; - } + if (endNum > headRevision + 1) endNum = headRevision + 1; endNum = Math.floor(endNum / granularity) * granularity; const compositesChangesetNeeded = []; @@ -1262,39 +1187,22 @@ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { revTimesNeeded.push(end - 1); } - // get all needed db values parallel - no await here since - // it would make all the lookups run in series - - // get all needed composite Changesets + // Get all needed db values in parallel. const composedChangesets = {}; - const p1 = Promise.all( - compositesChangesetNeeded.map( - (item) => composePadChangesets( - padId, item.start, item.end - ).then( - (changeset) => { - composedChangesets[`${item.start}/${item.end}`] = changeset; - } - ) - ) - ); - - // get all needed revision Dates const revisionDate = []; - const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum) - .then((revDate) => { - revisionDate[revNum] = Math.floor(revDate / 1000); - }) - )); - - // get the lines - let lines; - const p3 = getPadLines(padId, startNum - 1).then((_lines) => { - lines = _lines; - }); - - // wait for all of the above to complete - await Promise.all([p1, p2, p3]); + const [lines] = await Promise.all([ + getPadLines(padId, startNum - 1), + // Get all needed composite Changesets. + ...compositesChangesetNeeded.map(async (item) => { + const changeset = await composePadChangesets(padId, item.start, item.end); + composedChangesets[`${item.start}/${item.end}`] = changeset; + }), + // Get all needed revision Dates. + ...revTimesNeeded.map(async (revNum) => { + const revDate = await pad.getRevisionDate(revNum); + revisionDate[revNum] = Math.floor(revDate / 1000); + }), + ]); // doesn't know what happens here exactly :/ const timeDeltas = []; @@ -1304,9 +1212,7 @@ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { for (let compositeStart = startNum; compositeStart < endNum; compositeStart += granularity) { const compositeEnd = compositeStart + granularity; - if (compositeEnd > endNum || compositeEnd > head_revision + 1) { - break; - } + if (compositeEnd > endNum || compositeEnd > headRevision + 1) break; const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); @@ -1391,7 +1297,8 @@ const composePadChangesets = async (padId, startNum, endNum) => { return changeset; } catch (e) { // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 - console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); + messageLogger.warn( + `failed to compose cs in pad: ${padId} startrev: ${startNum} current rev: ${r}`); throw e; } }; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index cad53d1735b..53bb6d241ce 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -21,9 +21,11 @@ */ const log4js = require('log4js'); -const messageLogger = log4js.getLogger('message'); +const settings = require('../utils/Settings'); const stats = require('../stats'); +const logger = log4js.getLogger('socket.io'); + /** * Saves all components * key is the component name @@ -31,53 +33,57 @@ const stats = require('../stats'); */ const components = {}; -let socket; +let io; /** * adds a component */ exports.addComponent = (moduleName, module) => { - // save the component + if (module == null) return exports.deleteComponent(moduleName); components[moduleName] = module; - - // give the module the socket - module.setSocketIO(socket); + module.setSocketIO(io); }; +exports.deleteComponent = (moduleName) => { delete components[moduleName]; }; + /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = (_socket) => { - // save this socket internaly - socket = _socket; +exports.setSocketIO = (_io) => { + io = _io; + + io.sockets.on('connection', (socket) => { + const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; + logger.debug(`${socket.id} connected from IP ${ip}`); - socket.sockets.on('connection', (client) => { // wrap the original send function to log the messages - client._send = client.send; - client.send = (message) => { - messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); - client._send(message); + socket._send = socket.send; + socket.send = (message) => { + logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`); + socket._send(message); }; // tell all components about this connect for (const i of Object.keys(components)) { - components[i].handleConnect(client); + components[i].handleConnect(socket); } - client.on('message', async (message) => { - if (message.protocolVersion && message.protocolVersion !== 2) { - messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); - return; - } + socket.on('message', (message, ack = () => {}) => { if (!message.component || !components[message.component]) { - messageLogger.error(`Can't route the message: ${JSON.stringify(message)}`); + logger.error(`Can't route the message: ${JSON.stringify(message)}`); return; } - messageLogger.debug(`from ${client.id}: ${JSON.stringify(message)}`); - await components[message.component].handleMessage(client, message); + logger.debug(`from ${socket.id}: ${JSON.stringify(message)}`); + (async () => await components[message.component].handleMessage(socket, message))().then( + (val) => ack(null, val), + (err) => { + logger.error(`Error while handling message from ${socket.id}: ${err.stack || err}`); + ack({name: err.name, message: err.message}); + }); }); - client.on('disconnect', () => { + socket.on('disconnect', (reason) => { + logger.debug(`${socket.id} disconnected: ${reason}`); // store the lastDisconnect as a timestamp, this is useful if you want to know // when the last user disconnected. If your activePads is 0 and totalUsers is 0 // you can say, if there has been no active pads or active users for 10 minutes @@ -85,7 +91,7 @@ exports.setSocketIO = (_socket) => { stats.gauge('lastDisconnect', () => Date.now()); // tell all components about this disconnect for (const i of Object.keys(components)) { - components[i].handleDisconnect(client); + components[i].handleDisconnect(socket); } }); }); diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 8cbf3762ae0..792801dc72e 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -1,48 +1,44 @@ 'use strict'; const eejs = require('../../eejs'); -const fs = require('fs'); +const fsp = require('fs').promises; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); const settings = require('../../utils/Settings'); -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/admin/settings', (req, res) => { +exports.expressCreateServer = (hookName, {app}) => { + app.get('/admin/settings', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { req, settings: '', errors: [], })); }); - return cb(); }; -exports.socketio = (hookName, args, cb) => { - const io = args.io.of('/settings'); - io.on('connection', (socket) => { +exports.socketio = (hookName, {io}) => { + io.of('/settings').on('connection', (socket) => { const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; if (!isAdmin) return; - socket.on('load', (query) => { - fs.readFile('settings.json', 'utf8', (err, data) => { - if (err) { - return console.log(err); - } - - // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result - if (settings.showSettingsInAdminPage === false) { - socket.emit('settings', {results: 'NOT_ALLOWED'}); - } else { - socket.emit('settings', {results: data}); - } - }); + socket.on('load', async (query) => { + let data; + try { + data = await fsp.readFile(settings.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', (settings) => { - fs.writeFile('settings.json', settings, (err) => { - if (err) throw err; - socket.emit('saveprogress', 'saved'); - }); + socket.on('saveSettings', async (newSettings) => { + await fsp.writeFile(settings.settingsFilename, newSettings); + socket.emit('saveprogress', 'saved'); }); socket.on('restartServer', async () => { @@ -53,5 +49,4 @@ exports.socketio = (hookName, args, cb) => { await hooks.aCallAll('restartServer'); }); }); - return cb(); }; diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index b72ed11e5f9..a0fbbc6388f 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -4,6 +4,7 @@ const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); const formidable = require('formidable'); const apiHandler = require('../../handler/APIHandler'); +const util = require('util'); exports.expressCreateServer = (hookName, args, cb) => { // The Etherpad client side sends information about how a disconnect happened @@ -14,18 +15,26 @@ exports.expressCreateServer = (hookName, args, cb) => { }); }); + const parseJserrorForm = async (req) => await new Promise((resolve, reject) => { + const form = new formidable.IncomingForm(); + form.maxFileSize = 1; // Files are not expected. Not sure if 0 means unlimited, so 1 is used. + form.on('error', (err) => reject(err)); + form.parse(req, (err, fields) => err != null ? reject(err) : resolve(fields.errorInfo)); + }); + // The Etherpad client side sends information about client side javscript errors - args.app.post('/jserror', (req, res) => { - new formidable.IncomingForm().parse(req, (err, fields, files) => { - let data; - try { - data = JSON.parse(fields.errorInfo); - } catch (e) { - return res.end(); - } - clientLogger.warn(`${data.msg} --`, data); + args.app.post('/jserror', (req, res, next) => { + (async () => { + 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. + options = Object.assign({}, options, {depth: Infinity, colors: true}); + return util.inspect(data, options); + }, + }); res.end('OK'); - }); + })().catch((err) => next(err || new Error(err))); }); // Provide a possibility to query the latest available API version diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js deleted file mode 100644 index 4dda67b1f0f..00000000000 --- a/src/node/hooks/express/padreadonly.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const readOnlyManager = require('../../db/ReadOnlyManager'); -const hasPadAccess = require('../../padaccess'); -const exporthtml = require('../../utils/ExportHtml'); - -exports.expressCreateServer = (hookName, args, cb) => { - // serve read only pad - args.app.get('/ro/:id', async (req, res) => { - // translate the read only pad to a padId - const padId = await readOnlyManager.getPadId(req.params.id); - if (padId == null) { - res.status(404).send('404 - Not Found'); - return; - } - - // we need that to tell hasPadAcess about the pad - req.params.pad = padId; - - if (await hasPadAccess(req, res)) { - // render the html document - const html = await exporthtml.getPadHTMLDocument(padId, null); - res.send(html); - } - }); - return cb(); -}; diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 48c850af996..45683bc650c 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -19,23 +19,18 @@ const db = require('../db/DB'); const hooks = require('../../static/js/pluginfw/hooks'); -exports.getPadRaw = async (padId) => { - const padKey = `pad:${padId}`; - const padcontent = await db.get(padKey); +exports.getPadRaw = async (padId, readOnlyId) => { + const keyPrefixRead = `pad:${padId}`; + const keyPrefixWrite = readOnlyId ? `pad:${readOnlyId}` : keyPrefixRead; + const padcontent = await db.get(keyPrefixRead); - const records = [padKey]; - for (let i = 0; i <= padcontent.head; i++) { - records.push(`${padKey}:revs:${i}`); - } - - for (let i = 0; i <= padcontent.chatHead; i++) { - records.push(`${padKey}:chat:${i}`); - } + const keySuffixes = ['']; + for (let i = 0; i <= padcontent.head; i++) keySuffixes.push(`:revs:${i}`); + for (let i = 0; i <= padcontent.chatHead; i++) keySuffixes.push(`:chat:${i}`); const data = {}; - for (const key of records) { - // For each piece of info about a pad. - const entry = data[key] = await db.get(key); + for (const keySuffix of keySuffixes) { + const entry = data[keyPrefixWrite + keySuffix] = await db.get(keyPrefixRead + keySuffix); // Get the Pad Authors if (entry.pool && entry.pool.numToAttrib) { @@ -50,7 +45,7 @@ exports.getPadRaw = async (padId) => { if (authorEntry) { data[`globalAuthor:${authorId}`] = authorEntry; if (authorEntry.padIDs) { - authorEntry.padIDs = padId; + authorEntry.padIDs = readOnlyId || padId; } } } @@ -63,7 +58,8 @@ exports.getPadRaw = async (padId) => { const prefixes = await hooks.aCallAll('exportEtherpadAdditionalContent'); await Promise.all(prefixes.map(async (prefix) => { const key = `${prefix}:${padId}`; - data[key] = await db.get(key); + const writeKey = readOnlyId ? `${prefix}:${readOnlyId}` : key; + data[writeKey] = await db.get(key); })); return data; diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index 0c593eca143..ba71269d1ab 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -53,7 +53,8 @@ exports._analyzeLine = (text, aline, apool) => { if (aline) { const opIter = Changeset.opIterator(aline); if (opIter.hasNext()) { - let listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + const op = opIter.next(); + let listType = Changeset.opAttributeValue(op, 'list', apool); if (listType) { lineMarker = 1; listType = /([a-z]+)([0-9]+)/.exec(listType); @@ -62,10 +63,7 @@ exports._analyzeLine = (text, aline, apool) => { line.listLevel = Number(listType[2]); } } - } - const opIter2 = Changeset.opIterator(aline); - if (opIter2.hasNext()) { - const start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); + const start = Changeset.opAttributeValue(op, 'start', apool); if (start) { line.start = start; } diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 38e5fb1a60a..bc50da77b4b 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -457,7 +457,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getPadHTMLDocument = async (padId, revNum) => { +exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { const pad = await padManager.getPad(padId); // Include some Styles into the Head for Export @@ -475,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum) => { return eejs.require('ep_etherpad-lite/templates/export_html.html', { body: html, - padId: Security.escapeHTML(padId), + padId: Security.escapeHTML(readOnlyId || padId), extraCSS: stylesForExportCSS, }); }; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 83160b54eca..58b79f3a1a8 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -18,26 +18,21 @@ const log4js = require('log4js'); const Changeset = require('../../static/js/Changeset'); const contentcollector = require('../../static/js/contentcollector'); -const cheerio = require('cheerio'); +const jsdom = require('jsdom'); const rehype = require('rehype'); const minifyWhitespace = require('rehype-minify-whitespace'); -exports.setPadHTML = async (pad, html) => { - const apiLogger = log4js.getLogger('ImportHtml'); - - rehype() - .use(minifyWhitespace, {newlines: false}) - .process(html, (err, output) => { - html = String(output); - }); +const apiLogger = log4js.getLogger('ImportHtml'); +const processor = rehype().use(minifyWhitespace, {newlines: false}); - const $ = cheerio.load(html); +exports.setPadHTML = async (pad, html) => { + html = String(await processor.process(html)); + const {window: {document}} = new jsdom.JSDOM(html); // Appends a line break, used by Etherpad to ensure a caret is available // below the last line of an import - $('body').append('

'); + document.body.appendChild(document.createElement('p')); - const doc = $('body')[0]; apiLogger.debug('html:'); apiLogger.debug(html); @@ -46,12 +41,10 @@ exports.setPadHTML = async (pad, html) => { const cc = contentcollector.makeContentCollector(true, null, pad.pool); try { // we use a try here because if the HTML is bad it will blow up - cc.collectContent(doc); - } catch (e) { - apiLogger.warn('HTML was not properly formed', e); - - // don't process the HTML because it was bad - throw e; + cc.collectContent(document.body); + } catch (err) { + apiLogger.warn(`Error processing HTML: ${err.stack || err}`); + throw err; } const result = cc.finish(); @@ -70,35 +63,29 @@ exports.setPadHTML = async (pad, html) => { apiLogger.debug(newText); const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; - const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { - const attribsIter = Changeset.opIterator(attribs); - let textIndex = 0; - const newTextStart = 0; - const newTextEnd = newText.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); - const nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { - func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); - } - textIndex = nextIndex; - } - }; - // create a new changeset with a helper builder object const builder = Changeset.builder(1); // assemble each line into the builder - eachAttribRun(newAttribs, (start, end, attribs) => { - builder.insert(newText.substring(start, end), attribs); - }); + const attribsIter = Changeset.opIterator(newAttribs); + let textIndex = 0; + const newTextStart = 0; + const newTextEnd = newText.length; + while (attribsIter.hasNext()) { + const op = attribsIter.next(); + const nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { + const start = Math.max(newTextStart, textIndex); + const end = Math.min(newTextEnd, nextIndex); + builder.insert(newText.substring(start, end), op.attribs); + } + textIndex = nextIndex; + } // the changeset is ready! const theChangeset = builder.toString(); apiLogger.debug(`The changeset: ${theChangeset}`); - await Promise.all([ - pad.setText('\n'), - pad.appendRevision(theChangeset), - ]); + await pad.setText('\n'); + if (!Changeset.isIdentity(theChangeset)) await pad.appendRevision(theChangeset); }; diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js index 276bc3003ad..33920919461 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.js @@ -22,17 +22,15 @@ const fs = require('fs').promises; const log4js = require('log4js'); const os = require('os'); const path = require('path'); +const runCmd = require('./run_cmd'); const settings = require('./Settings'); -const spawn = require('child_process').spawn; -const libreOfficeLogger = log4js.getLogger('LibreOffice'); +const logger = log4js.getLogger('LibreOffice'); const doConvertTask = async (task) => { const tmpDir = os.tmpdir(); - - libreOfficeLogger.debug( - `Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); - const soffice = spawn(settings.soffice, [ + const p = runCmd([ + settings.soffice, '--headless', '--invisible', '--nologo', @@ -43,33 +41,32 @@ const doConvertTask = async (task) => { task.srcFile, '--outdir', tmpDir, - ]); + ], {stdio: [ + null, + (line) => logger.info(`[${p.child.pid}] stdout: ${line}`), + (line) => logger.error(`[${p.child.pid}] stderr: ${line}`), + ]}); + logger.info(`[${p.child.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`); // Soffice/libreoffice is buggy and often hangs. // To remedy this we kill the spawned process after a while. + // TODO: Use the timeout option once support for Node.js < v15.13.0 is dropped. const hangTimeout = setTimeout(() => { - soffice.stdin.pause(); // required to kill hanging threads - soffice.kill(); + logger.error(`[${p.child.pid}] Conversion timed out; killing LibreOffice...`); + p.child.kill(); }, 120000); - let stdoutBuffer = ''; - soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); - soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - await new Promise((resolve, reject) => { - soffice.on('exit', (code) => { - clearTimeout(hangTimeout); - if (code !== 0) { - const err = - new Error(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`); - libreOfficeLogger.error(err.stack); - return reject(err); - } - resolve(); - }); - }); - + try { + await p; + } catch (err) { + logger.error(`[${p.child.pid}] Conversion failed: ${err.stack || err}`); + throw err; + } finally { + clearTimeout(hangTimeout); + } + logger.info(`[${p.child.pid}] Conversion done.`); const filename = path.basename(task.srcFile); const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; const sourcePath = path.join(tmpDir, sourceFile); - libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); + logger.debug(`Renaming ${sourcePath} to ${task.destFile}`); await fs.rename(sourcePath, task.destFile); }; diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 1ef6490ee78..364ecc96cca 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -5,56 +5,27 @@ const CleanCSS = require('clean-css'); const Terser = require('terser'); +const fsp = require('fs').promises; const path = require('path'); const Threads = require('threads'); const compressJS = (content) => Terser.minify(content); -const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => { +const compressCSS = async (filename, ROOT_DIR) => { + const absPath = path.resolve(ROOT_DIR, filename); try { - const absPath = path.resolve(ROOT_DIR, filename); - - /* - * Changes done to migrate CleanCSS 3.x -> 4.x: - * - * 1. Rework the rebase logic, because the API was simplified (but we have - * less control now). See: - * https://github.com/jakubpawlowicz/clean-css/blob/08f3a74925524d30bbe7ac450979de0a8a9e54b2/README.md#important-40-breaking-changes - * - * EXAMPLE: - * The URLs contained in a CSS file (including all the stylesheets - * imported by it) residing on disk at: - * /home/muxator/etherpad/src/static/css/pad.css - * - * Will be rewritten rebasing them to: - * /home/muxator/etherpad/src/static/css - * - * 2. CleanCSS.minify() can either receive a string containing the CSS, or - * an array of strings. In that case each array element is interpreted as - * an absolute local path from which the CSS file is read. - * - * In version 4.x, CleanCSS API was simplified, eliminating the - * relativeTo parameter, and thus we cannot use our already loaded - * "content" argument, but we have to wrap the absolute path to the CSS - * in an array and ask the library to read it by itself. - */ - const basePath = path.dirname(absPath); - - new CleanCSS({ + const output = await new CleanCSS({ rebase: true, rebaseTo: basePath, - }).minify([absPath], (errors, minified) => { - if (errors) return rej(errors); - - return res(minified.styles); - }); + }).minify([absPath]); + return output.styles; } catch (error) { // on error, just yield the un-minified original, but write a log message console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`); - callback(null, content); + return await fsp.readFile(absPath, 'utf8'); } -}); +}; Threads.expose({ compressJS, diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 40576c3459c..814601f894a 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -28,6 +28,7 @@ */ const absolutePaths = require('./AbsolutePaths'); +const deepEqual = require('fast-deep-equal/es6'); const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -39,10 +40,39 @@ const suppressDisableMsg = ' -- To suppress these warning messages change ' + 'suppressErrorsInPadText to true in your settings.json\n'; const _ = require('underscore'); +const logger = log4js.getLogger('settings'); + +// Exported values that settings.json and credentials.json cannot override. +const nonSettings = [ + 'credentialsFilename', + 'settingsFilename', +]; + +// This is a function to make it easy to create a new instance. It is important to not reuse a +// config object after passing it to log4js.configure() because that method mutates the object. :( +const defaultLogConfig = () => ({appenders: [{type: 'console'}]}); +const defaultLogLevel = 'INFO'; + +const initLogging = (logLevel, config) => { + // log4js.configure() modifies exports.logconfig so check for equality first. + const logConfigIsDefault = deepEqual(config, defaultLogConfig()); + log4js.configure(config); + log4js.setGlobalLogLevel(logLevel); + log4js.replaceConsole(); + // Log the warning after configuring log4js to increase the chances the user will see it. + if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.'); +}; + +// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized +// with the user's chosen log level and logger config after the settings have been loaded. +initLogging(defaultLogLevel, defaultLogConfig()); + /* Root path of the installation */ exports.root = absolutePaths.findEtherpadRoot(); -console.log('All relative paths will be interpreted relative to the identified ' + +logger.info('All relative paths will be interpreted relative to the identified ' + `Etherpad base dir: ${exports.root}`); +exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); +exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); /** * The app title, visible e.g. in the browser window @@ -234,7 +264,7 @@ exports.allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = 'INFO'; +exports.loglevel = defaultLogLevel; /** * Disable IP logging @@ -264,7 +294,7 @@ exports.indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = {appenders: [{type: 'console'}]}; +exports.logconfig = defaultLogConfig(); /* * Session Key, do not sure this. @@ -450,7 +480,7 @@ exports.getGitCommit = () => { } version = version.substring(0, 7); } catch (e) { - console.warn(`Can't get git version for server header\n${e.message}`); + logger.warn(`Can't get git version for server header\n${e.message}`); } return version; }; @@ -467,9 +497,14 @@ exports.getEpVersion = () => require('../../package.json').version; */ const storeSettings = (settingsObj) => { for (const i of Object.keys(settingsObj || {})) { + if (nonSettings.includes(i)) { + logger.warn(`Ignoring setting: '${i}'`); + continue; + } + // test if the setting starts with a lowercase character if (i.charAt(0).search('[a-z]') !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); + logger.warn(`Settings should start with a lowercase character: '${i}'`); } // we know this setting, so we overwrite it @@ -482,7 +517,7 @@ const storeSettings = (settingsObj) => { } } else { // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } }; @@ -598,10 +633,10 @@ const lookupEnvironmentVariables = (obj) => { const defaultValue = match[3]; if ((envVarValue === undefined) && (defaultValue === undefined)) { - console.warn(`Environment variable "${envVarName}" does not contain any value for ` + - `configuration key "${key}", and no default was given. Using null. ` + - 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + - 'explicitly use "null" as the default if you want to continue to use null.'); + logger.warn(`Environment variable "${envVarName}" does not contain any value for ` + + `configuration key "${key}", and no default was given. Using null. ` + + 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + + 'explicitly use "null" as the default if you want to continue to use null.'); /* * We have to return null, because if we just returned undefined, the @@ -611,8 +646,8 @@ const lookupEnvironmentVariables = (obj) => { } if ((envVarValue === undefined) && (defaultValue !== undefined)) { - console.debug(`Environment variable "${envVarName}" not found for ` + - `configuration key "${key}". Falling back to default value.`); + logger.debug(`Environment variable "${envVarName}" not found for ` + + `configuration key "${key}". Falling back to default value.`); return coerceValue(defaultValue); } @@ -623,7 +658,7 @@ const lookupEnvironmentVariables = (obj) => { * For numeric and boolean strings let's convert it to proper types before * returning it, in order to maintain backward compatibility. */ - console.debug( + logger.debug( `Configuration key "${key}" will be read from environment variable "${envVarName}"`); return coerceValue(envVarValue); @@ -650,11 +685,11 @@ const parseSettings = (settingsFilename, isSettings) => { if (isSettings) { settingsType = 'settings'; notFoundMessage = 'Continuing using defaults!'; - notFoundFunction = console.warn; + notFoundFunction = logger.warn.bind(logger); } else { settingsType = 'credentials'; notFoundMessage = 'Ignoring.'; - notFoundFunction = console.info; + notFoundFunction = logger.info.bind(logger); } try { @@ -672,42 +707,30 @@ const parseSettings = (settingsFilename, isSettings) => { const settings = JSON.parse(settingsStr); - console.info(`${settingsType} loaded from: ${settingsFilename}`); + logger.info(`${settingsType} loaded from: ${settingsFilename}`); const replacedSettings = lookupEnvironmentVariables(settings); return replacedSettings; } catch (e) { - console.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); + logger.error(`There was an error processing your ${settingsType} ` + + `file from ${settingsFilename}: ${e.message}`); process.exit(1); } }; exports.reloadSettings = () => { - // Discover where the settings file lives - const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); - - // Discover if a credential file exists - const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); - - // try to parse the settings - const settings = parseSettings(settingsFilename, true); - - // try to parse the credentials - const credentials = parseSettings(credentialsFilename, false); - + const settings = parseSettings(exports.settingsFilename, true); + const credentials = parseSettings(exports.credentialsFilename, false); storeSettings(settings); storeSettings(credentials); - log4js.configure(exports.logconfig);// Configure the logging appenders - log4js.setGlobalLogLevel(exports.loglevel);// set loglevel - log4js.replaceConsole(); + initLogging(exports.loglevel, exports.logconfig); if (!exports.skinName) { - console.warn('No "skinName" parameter found. Please check out settings.json.template and ' + - 'update your settings.json. Falling back to the default "colibris".'); + logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + + 'update your settings.json. Falling back to the default "colibris".'); exports.skinName = 'colibris'; } @@ -717,8 +740,8 @@ exports.reloadSettings = () => { const countPieces = exports.skinName.split(path.sep).length; if (countPieces !== 1) { - console.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + + `not valid: "${exports.skinName}". Falling back to the default "colibris".`); exports.skinName = 'colibris'; } @@ -728,21 +751,20 @@ exports.reloadSettings = () => { // what if someone sets skinName == ".." or "."? We catch him! if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { - console.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + - 'Falling back to the default "colibris".'); + logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + + 'Falling back to the default "colibris".'); exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } if (fs.existsSync(skinPath) === false) { - console.error( - `Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); + logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } - console.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); } if (exports.abiword) { @@ -754,7 +776,7 @@ exports.reloadSettings = () => { if (!exports.suppressErrorsInPadText) { exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; } - console.error(`${abiwordError} File location: ${exports.abiword}`); + logger.error(`${abiwordError} File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -770,7 +792,7 @@ exports.reloadSettings = () => { if (!exports.suppressErrorsInPadText) { exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - console.error(`${sofficeError} File location: ${exports.soffice}`); + logger.error(`${sofficeError} File location: ${exports.soffice}`); exports.soffice = null; } }); @@ -780,18 +802,18 @@ exports.reloadSettings = () => { const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); - console.info(`Session key loaded from: ${sessionkeyFilename}`); + logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (e) { - console.info( + logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); exports.sessionKey = randomString(32); fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); } } else { - console.warn('Declaring the sessionKey in the settings.json is deprecated. ' + - 'This value is auto-generated now. Please remove the setting from the file. -- ' + - 'If you are seeing this error after restarting using the Admin User ' + - 'Interface then you can ignore this message.'); + logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + + 'This value is auto-generated now. Please remove the setting from the file. -- ' + + 'If you are seeing this error after restarting using the Admin User ' + + 'Interface then you can ignore this message.'); } if (exports.dbType === 'dirty') { @@ -801,13 +823,13 @@ exports.reloadSettings = () => { } exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - console.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); } if (exports.ip === '') { // using Unix socket for connectivity - console.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + - '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); + logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); } /* @@ -822,7 +844,7 @@ exports.reloadSettings = () => { * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ exports.randomVersionString = randomString(4); - console.log(`Random string used for versioning assets: ${exports.randomVersionString}`); + logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); }; exports.exportedForTestingOnly = { diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 2da3532e2a7..670e8d6a17c 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -9,11 +9,7 @@ function PadDiff(pad, fromRev, toRev) { } const range = pad.getValidRevisionRange(fromRev, toRev); - if (!range) { - throw new Error(`${'Invalid revision range.' + - ' startRev: '}${fromRev - } endRev: ${toRev}`); - } + if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`); this._pad = pad; this._fromRev = range.startRev; @@ -164,7 +160,7 @@ PadDiff.prototype._createDiffAtext = async function () { if (superChangeset == null) { superChangeset = changeset; } else { - superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool); + superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool); } } @@ -277,7 +273,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curChar = 0; let curLineOpIter = null; let curLineOpIterLine; - const curLineNextOp = Changeset.newOp('+'); + let curLineNextOp = Changeset.newOp('+'); const unpacked = Changeset.unpack(cs); const csIter = Changeset.opIterator(unpacked.ops); @@ -289,15 +285,13 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; - let done = false; - while (!done) { - curLineOpIter.next(curLineNextOp); + while (curLineOpIter.hasNext()) { + curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } else { - indexIntoLine += curLineNextOp.chars; + break; } + indexIntoLine += curLineNextOp.chars; } } @@ -311,7 +305,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { } if (!curLineNextOp.chars) { - curLineOpIter.next(curLineNextOp); + curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : Changeset.newOp(); } const charsToUse = Math.min(numChars, curLineNextOp.chars); diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 7f1fe01350e..896913ffe8f 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -2,7 +2,7 @@ "pad.js": [ "pad.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "security.js" , "$security.js" , "vendors/browser.js" @@ -19,9 +19,10 @@ , "pad_impexp.js" , "pad_savedrevs.js" , "pad_connectionstatus.js" + , "ChatMessage.js" , "chat.js" , "vendors/gritter.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "$tinycon/tinycon.js" , "vendors/farbtastic.js" , "skin_variants.js" @@ -33,7 +34,7 @@ , "colorutils.js" , "draggable.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "vendors/browser.js" , "pad_cookie.js" , "pad_editor.js" @@ -73,7 +74,7 @@ , "scroll.js" , "caretPosition.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "security.js" , "$security.js" ] diff --git a/src/package-lock.json b/src/package-lock.json index 04358c82c85..bccb0f8c0af 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.8.14", + "version": "1.8.15", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -29,27 +29,51 @@ "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==" }, "@azure/core-auth": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.0.tgz", - "integrity": "sha512-kSDSZBL6c0CYdhb+7KuutnKGf2geeT+bCJAgccB0DD7wmNJSsQPcF7TcuoZX83B7VK4tLz/u+8sOO/CnCsYp8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz", + "integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==", "requires": { "@azure/abort-controller": "^1.0.0", - "tslib": "^2.0.0" + "tslib": "^2.2.0" + } + }, + "@azure/core-client": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.3.1.tgz", + "integrity": "sha512-7IHm2DGg2u7dJYtCW84Ik7uENHfE8VsM/sWloZezPKYDoWZrg7JzwjvdGAfsaELKi2p0GE+JBaAbDYnNpr5V1w==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "1.0.0-preview.13", + "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "requires": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + } + } } }, "@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.1.tgz", + "integrity": "sha512-7ATnV3OGzCO2K9kMrh3NKUM8b4v+xasmlUhkNZz6uMbm+8XH/AexLkhRGsoo0GyKNlEGvyGEfytqTk0nUY2I4A==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", "node-fetch": "^2.6.0", "process": "^0.11.10", "tough-cookie": "^4.0.0", @@ -59,24 +83,13 @@ "xml2js": "^0.4.19" }, "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" - } - }, - "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==", + "@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": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" } }, "uuid": { @@ -87,42 +100,86 @@ } }, "@azure/core-lro": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-1.0.5.tgz", - "integrity": "sha512-0EFCFZxARrIoLWMIRt4vuqconRVIO2Iin7nFBfJiYCCbKp5eEmxutNk8uqudPmG0XFl5YqlVh68/al/vbE5OOg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.1.tgz", + "integrity": "sha512-HE6PBl+mlKa0eBsLwusHqAqjLc5n9ByxeDo3Hz4kF3B1hqHvRkBr4oMgoT6tX7Hc3q97KfDctDUon7EhvoeHPA==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-http": "^1.2.0", - "@azure/core-tracing": "1.0.0-preview.11", - "events": "^3.0.0", - "tslib": "^2.0.0" + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.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-paging": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.1.3.tgz", - "integrity": "sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.2.0.tgz", + "integrity": "sha512-ZX1bCjm/MjKPCN6kQD/9GJErYSoKA8YWp6YWoo5EIzcTWlSBLXu3gNaBTUl8usGl+UShiKo7b4Gdy1NSTIlpZg==", + "requires": { + "@azure/core-asynciterator-polyfill": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-rest-pipeline": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.3.1.tgz", + "integrity": "sha512-xTQiv47O5cWzJFkwiDrUTT4K4IYbUIts0gaou5TZxAAuhQi9kAKWHEmFTjHVMOeAmyDhlMM5cb21M2n4WDto1A==", "requires": { - "@azure/core-asynciterator-polyfill": "^1.0.0" + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "requires": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "version": "1.0.0-preview.12", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.12.tgz", + "integrity": "sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==", "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.0", + "tslib": "^2.2.0" } }, "@azure/identity": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.3.0.tgz", - "integrity": "sha512-qYTaWA+5ir4+/iEry7n3l1TyeNhTHP8IRpjsbNv8ur8W/QjqZmCz1H2naebRp5tQmehXfo1pUrp2ew+qGhTh0g==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.2.tgz", + "integrity": "sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==", "requires": { - "@azure/core-http": "^1.2.4", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", "@azure/msal-node": "1.0.0-beta.6", "@types/stoppable": "^1.1.0", @@ -146,25 +203,36 @@ } }, "@azure/keyvault-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.2.1.tgz", - "integrity": "sha512-bO3Dl4cJgOkYSLudmzkSFg4os4gsDvaUozcJ9ZKdqZjIp/RHIZRFytbRcNe40rpKH2iLXcavNGVpMvEzAfERyQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.3.0.tgz", + "integrity": "sha512-OEosl0/rE/mKD5Ji9KaQN7UH+yQnV5MS0MRhGqQIiJrG+qAvAla0MYudJzv3XvBlplpGk0+MVgyL9H3KX/UAwQ==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-http": "^1.2.0", - "@azure/core-lro": "^1.0.2", + "@azure/core-http": "^2.0.0", + "@azure/core-lro": "^2.0.0", "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.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/logger": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.2.tgz", - "integrity": "sha512-YZNjNV0vL3nN2nedmcjQBcpCTo3oqceXmgiQtEm6fLpucjRZyQKAQruhCmCpRlB1iykqKJJ/Y8CDmT5rIE6IJw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@azure/ms-rest-azure-env": { @@ -173,9 +241,9 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.5.2.tgz", - "integrity": "sha512-9nCuuoYwHZEZw1t0MVtENH+c1k2R4maYAlBBDSZhZu6bEucyfYUUigNXXKjt2cFBt4sO+sTzi0uI0f/fiPFr+Q==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz", + "integrity": "sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==", "requires": { "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", @@ -184,7 +252,7 @@ "tough-cookie": "^3.0.1", "tslib": "^1.10.0", "tunnel": "0.0.6", - "uuid": "^3.3.2", + "uuid": "^8.3.2", "xml2js": "^0.4.19" }, "dependencies": { @@ -212,13 +280,18 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, "@azure/ms-rest-nodeauth": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.10.tgz", - "integrity": "sha512-oel7ibYlredh2wo7XwNYMx4jWlbMkIzCC8t8VpdhsAWDJVNSSce+DYj5jjZn1oED+QsCytVM2B7/QTuLN1/yDw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.1.0.tgz", + "integrity": "sha512-F4NKrbkZg0qD3+rUM8fvJHOFRkXFoEiptYTZtLBruN3VwBFIqbTFW0fmgRyBW9seZl+mX2OexQA5GzWenSA3Kw==", "requires": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", @@ -226,17 +299,17 @@ } }, "@azure/msal-common": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz", - "integrity": "sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.5.1.tgz", + "integrity": "sha512-/i5dXM+QAtO+6atYd5oHGBAx48EGSISkXNXViheliOQe+SIFMDo3gSq3lL54W0suOSAsVPws3XnTaIHlla0PIQ==", "requires": { "debug": "^4.1.1" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -276,9 +349,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", "dev": true }, "@babel/highlight": { @@ -293,9 +366,9 @@ } }, "@eslint/eslintrc": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", - "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -309,6 +382,18 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -319,9 +404,9 @@ } }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -337,6 +422,12 @@ "esprima": "^4.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -357,6 +448,40 @@ } } }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@js-joda/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-3.2.0.tgz", @@ -376,9 +501,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -395,15 +520,27 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, - "@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + } }, "@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", + "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -415,18 +552,18 @@ } }, "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/samsam": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -445,15 +582,23 @@ "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.3.0.tgz", "integrity": "sha512-d/keJiNKfpHo+GmSB8QcsAwBx8h+V1UbdozA5TD+eSLXprNY53JAYub47J9evsSKWDdNG5uVj0FiMozLKuzowQ==" }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "requires": { + "@types/unist": "*" + } }, "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "@types/long": { "version": "4.0.1", @@ -461,14 +606,14 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "16.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" }, "@types/node-fetch": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.10.tgz", - "integrity": "sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==", + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", "requires": { "@types/node": "*", "form-data": "^3.0.0" @@ -486,28 +631,10 @@ } } }, - "@types/request": { - "version": "2.48.5", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", - "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } + "@types/parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, "@types/stoppable": { "version": "1.1.1", @@ -518,22 +645,38 @@ } }, "@types/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", + "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" }, "@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", "requires": { "@types/node": "*" } }, "@types/unist": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@xmldom/xmldom": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz", + "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==" + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" }, "abbrev": { "version": "1.1.1", @@ -559,38 +702,52 @@ } }, "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } }, "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, "adal-node": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.2.tgz", - "integrity": "sha512-luzQ9cXOjUlZoCiWeYbyR+nHwScSrPTDTbOInFphQs/PnwNz6wAIVkbsHEXtvYBnjLctByTTI8ccfpGX100oRQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.3.tgz", + "integrity": "sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ==", "requires": { - "@types/node": "^8.0.47", + "@xmldom/xmldom": "^0.7.0", "async": "^2.6.3", "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", "underscore": ">= 1.3.1", "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", "xpath.js": "~1.1.0" }, "dependencies": { - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", @@ -611,15 +768,38 @@ } }, "adm-zip": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.5.tgz", - "integrity": "sha512-IWwXKnCbirdbyXSfUDvCCrmYrOHANRZcc8NcRrvTlIApdl7PwE9oGcsYvNeJPAVY1M+70b4PxXGKIf8AEuiQ6w==" + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-QLEo3eoC2B0i3+g/G5nNzKbGoVOjW2ingZ4TXl7/YeDM+FAl3SiHSNnokTZLFEuVHBn5CbZ42KJcIIsRji1EgQ==" }, "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "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==" + } + } + }, "agentkeepalive": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", @@ -629,16 +809,24 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -675,9 +863,9 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -705,12 +893,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -757,14 +939,9 @@ "dev": true }, "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "async-stacktrace": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/async-stacktrace/-/async-stacktrace-0.0.2.tgz", - "integrity": "sha1-i7uXh+OzjINscpp+nXwIYw210e8=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", + "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" }, "asynckit": { "version": "0.4.0", @@ -782,11 +959,20 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "axios-cookiejar-support": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz", + "integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==", "requires": { - "follow-redirects": "^1.10.0" + "is-redirect": "^1.0.0", + "pify": "^5.0.0" } }, "backo2": { @@ -874,13 +1060,6 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "string_decoder": { @@ -889,13 +1068,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } } } @@ -905,15 +1077,6 @@ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -947,19 +1110,9 @@ "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -978,10 +1131,10 @@ "fill-range": "^7.0.1" } }, - "browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "browser-stdout": { "version": "1.3.1", @@ -998,6 +1151,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1009,9 +1163,9 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-writer": { "version": "2.0.0", @@ -1043,9 +1197,9 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, "caseless": { @@ -1079,11 +1233,6 @@ "supports-color": "^5.3.0" } }, - "channels": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/channels/-/channels-0.0.4.tgz", - "integrity": "sha1-G+4yPt6hUrue8E9BvG5rD1lIqUE=" - }, "character-entities-html4": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", @@ -1094,43 +1243,20 @@ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" }, - "cheerio": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", - "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash.assignin": "^4.0.9", - "lodash.bind": "^4.1.4", - "lodash.defaults": "^4.0.1", - "lodash.filter": "^4.4.0", - "lodash.flatten": "^4.2.0", - "lodash.foreach": "^4.3.0", - "lodash.map": "^4.4.0", - "lodash.merge": "^4.4.0", - "lodash.pick": "^4.2.1", - "lodash.reduce": "^4.4.0", - "lodash.reject": "^4.4.0", - "lodash.some": "^4.4.0" - } - }, "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" + "readdirp": "~3.6.0" } }, "chownr": { @@ -1140,86 +1266,55 @@ "optional": true }, "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.1.tgz", + "integrity": "sha512-ooQCa1/70oRfVdUUGjKpbHuxgMgm8BsDT5EBqBGvPxMoRoGXf4PNx5mMnkjzJ9Ptx4vvmDdha0QVh86QtYIk1g==", "requires": { "source-map": "~0.6.0" } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "cloudant-follow": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.18.2.tgz", - "integrity": "sha512-qu/AmKxDqJds+UmT77+0NbM7Yab2K3w0qSeJRzsq5dRWJTEJdWeb+XpG4OpKuTE9RKOa/Awn2gR3TTnvNr3TeA==", - "requires": { - "browser-request": "~0.3.0", - "debug": "^4.0.1", - "request": "^2.88.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" + "ansi-regex": "^5.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==" } } }, @@ -1228,11 +1323,6 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1296,13 +1386,6 @@ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "requires": { "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "content-type": { @@ -1350,22 +1433,26 @@ "which": "^2.0.1" } }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } } }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1374,6 +1461,16 @@ "assert-plus": "^1.0.0" } }, + "data-urls": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.0.tgz", + "integrity": "sha512-4AefxbTTdFtxDUdh0BuMBs2qJVL25Mow2zlcuuePegQwgD6GEmQao42LLEeksOui8nL4RcNEugIpFP7eRd33xg==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^9.0.0" + } + }, "date-utils": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", @@ -1388,11 +1485,16 @@ } }, "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, + "decimal.js": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", + "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" + }, "decompress-response": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", @@ -1411,17 +1513,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "delayed-stream": { "version": "1.0.0", @@ -1435,9 +1527,9 @@ "optional": true }, "denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" }, "depd": { "version": "1.1.2", @@ -1456,15 +1548,15 @@ "optional": true }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "dirty": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.1.tgz", - "integrity": "sha512-l/SMZcT+MjqOPpjarzJ8nQdxtxurURJM7js1l0Q2TQWtNbPzDYzkK++HlbT+XmM+adPFNdb3SOlVz9Jr7Df7xQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.3.tgz", + "integrity": "sha512-PlnV9+KeJ6bh8o5qQZqRnD80Wegijyr47dpwxCIuJ6SzwJ6/deO+NRTEnq/mubIYtBvBBgWznlE6dZ+nQsS/og==" }, "doctrine": { "version": "3.0.0", @@ -1475,35 +1567,19 @@ "esutils": "^2.0.2" } }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + } } }, "ecc-jsbn": { @@ -1620,6 +1696,11 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -1648,6 +1729,11 @@ "requires": { "ms": "2.0.0" } + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -1672,64 +1758,17 @@ "ansi-colors": "^4.1.1" } }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "errs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", - "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" - }, - "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true }, "escape-html": { "version": "1.0.3", @@ -1741,14 +1780,27 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, "eslint": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.28.0.tgz", - "integrity": "sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.2", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1788,6 +1840,18 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -1813,9 +1877,9 @@ } }, "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1838,9 +1902,9 @@ "dev": true }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -1868,21 +1932,48 @@ "esprima": "^4.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1912,6 +2003,15 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } } } }, @@ -2044,6 +2144,14 @@ "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } } }, "eslint-utils": { @@ -2085,6 +2193,12 @@ "eslint-visitor-keys": "^1.3.0" }, "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, "eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", @@ -2096,8 +2210,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.4.0", @@ -2106,14 +2219,6 @@ "dev": true, "requires": { "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } } }, "esrecurse": { @@ -2123,27 +2228,17 @@ "dev": true, "requires": { "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } } }, "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -2151,24 +2246,28 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "etherpad-cli-client": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.0.9.tgz", - "integrity": "sha1-A+5+fNzA4EZLTu/djn7gzwUaVDs=", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.1.12.tgz", + "integrity": "sha512-7Cz9Ofd2xa4OJwOHNHyWdzKhRLLa17Mqbav2IV2old+DoVPUiFyOXz6YXaqBvkj09bS8BuTEQVajqo/rQ6N0LA==", "dev": true, "requires": { - "async": "*", - "socket.io-client": "*" + "async": "^3.2.1", + "socket.io-client": "^2.3.0", + "superagent": "^6.1.0" } }, "etherpad-require-kernel": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.11.tgz", - "integrity": "sha512-I03bkNiBMrcsJRSl0IqotUU70s9v6VISrITj/cQgAoVQSoRFbV/NUn2fPIF4LskysTpmwlmwJqgfL2FZpAtxEw==" + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.15.tgz", + "integrity": "sha512-t8Z950sCfgS4ssex6SHhb3Ni8BQL0XdvZhMQWWDLhSWttyHgf+zPSMglBODyAUGh8mBX0XwGK7hpICGBHsvSGQ==" }, "etherpad-yajsml": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/etherpad-yajsml/-/etherpad-yajsml-0.0.4.tgz", - "integrity": "sha512-rxpEOMZmv6DOCQeaDo6tztneaKF9ZxbLo/+hQcV+hn0lNrxJZ7MKIPD2pTWWnNLj6gFFfs6QQ67RfMNWIr3fSA==" + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/etherpad-yajsml/-/etherpad-yajsml-0.0.12.tgz", + "integrity": "sha512-lVCqsZYpFsuIz417h+O83I7eadNXJ3MnQavriFa52/KTwj6xPAzEYr0PvH7KTxcqyAFtW7ItoTNVXe2h7zGxlw==", + "requires": { + "mime": "^1.6.0" + } }, "event-target-shim": { "version": "5.0.1", @@ -2221,19 +2320,12 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "express-rate-limit": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz", - "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.0.tgz", + "integrity": "sha512-/1mrKggjXMxd1/ghPub5N3d36u5VlK8KjbQFQLxYub09BWSSgSXMQbXgFiIW0BYxjM49YCj8bkihONZR2U4+mQ==" }, "express-session": { "version": "1.17.2", @@ -2259,6 +2351,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -2285,7 +2382,12 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fast-safe-stringify": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz", + "integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==", "dev": true }, "file-entry-cache": { @@ -2334,22 +2436,20 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "flat-cache": { "version": "3.0.4", @@ -2373,15 +2473,15 @@ } }, "flatted": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", - "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, "follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" }, "forever-agent": { "version": "0.6.1", @@ -2389,12 +2489,12 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -2420,12 +2520,12 @@ "optional": true }, "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "optional": true, "requires": { - "minipass": "^2.6.0" + "minipass": "^3.0.0" } }, "fs.realpath": { @@ -2434,24 +2534,12 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2532,18 +2620,18 @@ } }, "globals": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", - "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "optional": true }, "growl": { @@ -2564,6 +2652,24 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + } } }, "has": { @@ -2582,12 +2688,6 @@ "ansi-regex": "^2.0.0" } }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", @@ -2633,15 +2733,16 @@ } }, "hast-util-from-parse5": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz", - "integrity": "sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", + "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==", "requires": { - "ccount": "^1.0.3", - "hastscript": "^5.0.0", + "@types/parse5": "^5.0.0", + "hastscript": "^6.0.0", "property-information": "^5.0.0", - "web-namespaces": "^1.1.2", - "xtend": "^4.0.1" + "vfile": "^4.0.0", + "vfile-location": "^3.2.0", + "web-namespaces": "^1.0.0" } }, "hast-util-is-element": { @@ -2655,20 +2756,20 @@ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" }, "hast-util-to-html": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz", - "integrity": "sha512-IlC+LG2HGv0Y8js3wqdhg9O2sO4iVpRDbHOPwXd7qgeagpGsnY49i8yyazwqS35RA35WCzrBQE/n0M6GG/ewxA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz", + "integrity": "sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw==", "requires": { "ccount": "^1.0.0", - "comma-separated-tokens": "^1.0.1", + "comma-separated-tokens": "^1.0.0", "hast-util-is-element": "^1.0.0", "hast-util-whitespace": "^1.0.0", "html-void-elements": "^1.0.0", - "property-information": "^5.2.0", + "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0", - "stringify-entities": "^2.0.0", - "unist-util-is": "^3.0.0", - "xtend": "^4.0.1" + "stringify-entities": "^3.0.1", + "unist-util-is": "^4.0.0", + "xtend": "^4.0.0" } }, "hast-util-whitespace": { @@ -2677,10 +2778,11 @@ "integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==" }, "hastscript": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", - "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "requires": { + "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", @@ -2693,24 +2795,19 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, "html-void-elements": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==" }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, "http-errors": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", @@ -2723,6 +2820,11 @@ "toidentifier": "1.0.0" }, "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2730,6 +2832,31 @@ } } }, + "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" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2740,17 +2867,41 @@ "sshpk": "^1.7.0" } }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "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==", "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "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==" + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -2767,15 +2918,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -2813,9 +2955,9 @@ } }, "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.8", @@ -2833,26 +2975,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "dev": true - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2862,45 +2984,19 @@ "binary-extensions": "^2.0.0" } }, - "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "dev": true - }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", "requires": { "has": "^1.0.3" } }, - "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" - }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -2929,29 +3025,12 @@ "is-extglob": "^2.1.1" } }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true - }, "is-observable": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", @@ -2962,41 +3041,33 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "is-promise": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" - }, - "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=", "dev": true }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -3039,9 +3110,9 @@ } }, "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", + "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" }, "js-tokens": { "version": "4.0.0", @@ -3058,24 +3129,58 @@ } }, "jsbi": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", - "integrity": "sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg==" + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", + "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==" }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "jsdom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-17.0.0.tgz", + "integrity": "sha512-MUq4XdqwtNurZDVeKScENMPHnkgmdIvMzZ1r1NSwHkDuaqI6BouPjr+17COo4/19oLNnmdpFDPOHVpgIZmZ+VA==", + "requires": { + "abab": "^2.0.5", + "acorn": "^8.4.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.0", + "decimal.js": "^10.3.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^9.0.0", + "ws": "^8.0.0", + "xml-name-validator": "^3.0.0" + } + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3135,6 +3240,11 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -3150,9 +3260,9 @@ } }, "jszip": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", - "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", @@ -3182,12 +3292,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3258,13 +3362,12 @@ "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" }, "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "lie": { @@ -3277,13 +3380,12 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" } }, "lodash": { @@ -3291,41 +3393,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -3362,11 +3434,6 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3377,26 +3444,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" - }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -3404,12 +3451,64 @@ "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "log4js": { @@ -3421,26 +3520,10 @@ "semver": "~4.3.3" }, "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "semver": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, @@ -3453,23 +3536,31 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "requires": { + "semver": "^6.0.0" }, "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "optional": true } } }, "measured-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.51.1.tgz", - "integrity": "sha512-DZQP9SEwdqqYRvT2slMK81D/7xwdxXosZZBtLVfPSo6y5P672FBTbzHVdN4IQyUkUpcVOR9pIvtUy5Ryl7NKyg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-2.0.0.tgz", + "integrity": "sha512-SIzGtX1WGDvR59FqcJaGEAqDueBvLBh6W4T/gQaHr5ufcqvQkUHGcfQhlmq77mkeF5Mo+UpD+8hm69CwUVibGw==", "requires": { "binary-search": "^1.3.3", "optional-js": "^2.0.0" @@ -3502,16 +3593,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" }, "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", "requires": { - "mime-db": "1.48.0" + "mime-db": "1.50.0" } }, "mimic-response": { @@ -3531,34 +3622,33 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true }, "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", "optional": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "yallist": "^4.0.0" } }, "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "optional": true, "requires": { - "minipass": "^2.9.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" } }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true }, "mkdirp-classic": { "version": "0.5.3", @@ -3567,113 +3657,86 @@ "optional": true }, "mocha": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", - "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", "dev": true, "requires": { - "ansi-colors": "3.2.3", + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", + "chokidar": "3.5.2", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", + "ms": "2.1.3", + "nanoid": "3.1.23", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.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==", + "dev": true + } } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "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 }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "isexe": "^2.0.0" + "has-flag": "^4.0.0" } } } @@ -3693,14 +3756,14 @@ } }, "mongodb": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.9.tgz", - "integrity": "sha512-1nSCKgSunzn/CXwgOWgbPHUWOO5OfERcuOWISmqd610jn0s8BU9K4879iJVabqgpPPbA6hO7rG48eq+fGED3Mg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.2.tgz", + "integrity": "sha512-/Qi0LmOjzIoV66Y2JQkqmIIfFOy7ZKsXnQNlUXPFXChOw3FCdNqVD5zvci9ybm6pkMe/Nw+Rz9I0Zsk2a+05iQ==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", "denque": "^1.4.1", - "optional-require": "^1.0.3", + "optional-require": "^1.1.8", "safe-buffer": "^5.1.2", "saslprep": "^1.0.0" } @@ -3711,9 +3774,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "msal": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.11.tgz", - "integrity": "sha512-8vW5/+irlcQQk87r8Qp3/kQEc552hr7FQLJ6GF5LLkqnwJDDxrswz6RYPiQhmiampymIs0PbHVZrNf8m+6DmgQ==", + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.14.tgz", + "integrity": "sha512-k8M5+/jbfSQoCf7CyQzBP5HE5mY8TkBujykLGTEp2x0MvOK/FQsfUTNis28zlvvPVzhgrhb5GQiGM8rRpXyHdA==", "requires": { "tslib": "^1.9.3" }, @@ -3726,21 +3789,21 @@ } }, "mssql": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.1.3.tgz", - "integrity": "sha512-VCtGfJhb9ik5RV3PZQS9jG9I261cghwyWG4YZWn4+13k377sclkCx7/loctCnMNk1EYJFIIAWYCsk1GYwF1Yag==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.2.1.tgz", + "integrity": "sha512-kq0hVeD1tR+ikZqmLwgQqLGSavOhrrwaiYsYxdUQASifc3oIOFRx2IHpuWk+8oLI6Ab/s3o3JfpFX1v1Nf2sxA==", "requires": { "@tediousjs/connection-string": "^0.3.0", - "debug": "^4", + "debug": "^4.3.2", "rfdc": "^1.3.0", "tarn": "^3.0.1", - "tedious": "^11.0.7" + "tedious": "^11.4.0" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -3782,11 +3845,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3798,32 +3856,33 @@ } }, "nano": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.3.tgz", - "integrity": "sha512-nubyTQeZ/p+xf3ZFFMd7WrZwpcy9tUDrbaXw9HFBsM6zBY5gXspvOjvG2Zz3emT6nfJtP/h7F2/ESfsVVXnuMw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/nano/-/nano-9.0.5.tgz", + "integrity": "sha512-fEAhwAdXh4hDDnC8cYJtW6D8ivOmpvFAqT90+zEuQREpRkzA/mJPcI4EKv15JUdajaqiLTXNoKK6PaRF+/06DQ==", "requires": { - "@types/request": "^2.48.4", - "cloudant-follow": "^0.18.2", - "debug": "^4.1.1", - "errs": "^0.3.2", - "request": "^2.88.0" + "@types/tough-cookie": "^4.0.0", + "axios": "^0.21.1", + "axios-cookiejar-support": "^1.0.1", + "qs": "^6.9.4", + "tough-cookie": "^4.0.0" }, "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "requires": { - "ms": "2.1.2" + "side-channel": "^1.0.4" } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -3841,47 +3900,19 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "optional": true - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "nise": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", - "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/fake-timers": "^7.0.4", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" @@ -3899,18 +3930,26 @@ } }, "node-abi": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", - "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "optional": true, "requires": { "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + } } }, "node-abort-controller": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.2.1.tgz", - "integrity": "sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-2.0.0.tgz", + "integrity": "sha512-L8RfEgjBTHAISTuagw51PprVAqNZoG6KSB6LQ6H1bskMVkFs5E71IyjauLBv3XbuomJlguWF/VnRHdJ1gqiAqA==" }, "node-addon-api": { "version": "3.2.1", @@ -3918,116 +3957,67 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "optional": true }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" + "whatwg-url": "^5.0.0" }, "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "optional": true + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { - "isexe": "^2.0.0" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } } } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "node-gyp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", "optional": true, "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - } + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" } }, "nodeify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", + "dev": true, "requires": { "is-promise": "~1.0.0", "promise": "~1.3.0" } }, "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "optional": true, "requires": { "abbrev": "1" @@ -4040,9 +4030,9 @@ "dev": true }, "npm": { - "version": "6.14.13", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.13.tgz", - "integrity": "sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag==", + "version": "6.14.15", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.15.tgz", + "integrity": "sha512-dkcQc4n+DiJAMYG2haNAMyJbmuvevjXz+WC9dCUzodw8EovwTIc6CATSsTEplCY6c0jG4OshxFGFJsrnKJguWA==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -4153,7 +4143,7 @@ "sorted-union-stream": "~2.1.3", "ssri": "^6.0.2", "stringify-package": "^1.0.1", - "tar": "^4.4.13", + "tar": "^4.4.19", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "uid-number": "0.0.6", @@ -6161,7 +6151,7 @@ "bundled": true }, "path-parse": { - "version": "1.0.6", + "version": "1.0.7", "bundled": true }, "performance-now": { @@ -6712,16 +6702,16 @@ } }, "tar": { - "version": "4.4.13", + "version": "4.4.19", "bundled": true, "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" }, "dependencies": { "minipass": { @@ -6731,6 +6721,14 @@ "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } + }, + "safe-buffer": { + "version": "5.2.1", + "bundled": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true } } }, @@ -7133,32 +7131,6 @@ } } }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -7171,19 +7143,16 @@ "set-blocking": "~2.0.0" } }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "requires": { - "boolbase": "~1.0.0" - } - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -7196,38 +7165,9 @@ "optional": true }, "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", - "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2" - } + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, "observable-fns": { "version": "0.6.1", @@ -7265,18 +7205,18 @@ } }, "openapi-backend": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.2.tgz", - "integrity": "sha512-+IqhtObMGeRf4aDB6L5Lc3nZYPHB9JRkTiOaNHKx26SDWcaMAof6RnABbgLDNVRRiz+fbJPmizWcFSkCPX8qeQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-4.2.0.tgz", + "integrity": "sha512-eqdgJAjDbVZ7zhiIF68mlItFxqE48OPAM9nHHYx6BJMoGK2xInSBc2Oqp4dzsrsLIzoY8nVzK/vUtYktyXGb9Q==", "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.7", - "ajv": "^6.10.0", + "ajv": "^8.5.0", "bath-es5": "^3.0.3", "cookie": "^0.4.0", "lodash": "^4.17.15", "mock-json-schema": "^1.0.7", - "openapi-schema-validator": "^7.0.1", - "openapi-types": "^7.0.1", + "openapi-schema-validator": "^9.2.0", + "openapi-types": "^9.2.0", "qs": "^6.9.3" }, "dependencies": { @@ -7302,20 +7242,20 @@ } }, "openapi-schema-validator": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-7.2.3.tgz", - "integrity": "sha512-XT8NM5e/zBBa/cydTS1IeYkCPzJp9oixvt9Y1lEx+2gsCTOooNxw9x/KEivtWMSokne7X1aR+VtsYHQtNNOSyA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.3.0.tgz", + "integrity": "sha512-KlvgZMWTu+H1FHFSZNAGj369uXl3BD1nXSIq+sXlG6P+OrsAHd3YORx0ZEZ3WGdu2LQrPGmtowGQavYXL+PLwg==", "requires": { - "ajv": "^6.5.2", + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", "lodash.merge": "^4.6.1", - "openapi-types": "^7.2.3", - "swagger-schema-official": "2.0.0-bab6bed" + "openapi-types": "^9.3.0" } }, "openapi-types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.3.tgz", - "integrity": "sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA==" + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.0.tgz", + "integrity": "sha512-sR23YjmuwDSMsQVZDHbV9mPgi0RyniQlqR0AQxTC2/F3cpSjRFMH3CFPjoWvNqhC4OxPkDYNb2l8Mc1Me6D/KQ==" }, "optional-js": { "version": "2.3.0", @@ -7323,70 +7263,44 @@ "integrity": "sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==" }, "optional-require": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", - "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==" - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", + "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", "requires": { - "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" + "require-at": "^1.0.6" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "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" } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^3.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -7408,9 +7322,9 @@ } }, "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "parseqs": { "version": "0.0.6", @@ -7428,9 +7342,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": { @@ -7459,14 +7373,14 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.6.0.tgz", - "integrity": "sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", + "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", - "pg-pool": "^3.3.0", + "pg-pool": "^3.4.1", "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" @@ -7483,9 +7397,9 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz", - "integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" }, "pg-protocol": { "version": "1.5.0", @@ -7518,6 +7432,11 @@ "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -7542,9 +7461,9 @@ } }, "prebuild-install": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz", - "integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", + "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "optional": true, "requires": { "detect-libc": "^1.0.3", @@ -7563,10 +7482,9 @@ } }, "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "process": { "version": "0.11.10", @@ -7588,6 +7506,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", + "dev": true, "requires": { "is-promise": "~1" } @@ -7645,15 +7564,24 @@ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.2.tgz", - "integrity": "sha512-8qpJC/Zc/0dM9BW21/JyROt6eUeLZ8l06vrSWZFwgNV9IpthIJe6Pcuowpzxe0PJ3vYDaECiqvF/1J/+Nh5wgA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.1.tgz", + "integrity": "sha512-u4Ual0ssf/RHHxK3rqKo9W2S7ulVoNdCAOrsk1gR9JLtzqg7fGw+yaCeyBAEncsL2n6XqHh/0qJk3BPDn49BjA==" }, "raw-body": { "version": "2.4.0", @@ -7677,11 +7605,6 @@ "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, @@ -7698,22 +7621,23 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { - "picomatch": "^2.0.4" + "picomatch": "^2.2.1" } }, "redis": { @@ -7752,12 +7676,12 @@ "dev": true }, "rehype": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-10.0.0.tgz", - "integrity": "sha512-0W8M4Y91b2QuzDSTjkZgBOJo79bP089YbSQNPMqebuUVrp6iveoi+Ra6/H7fJwUxq8FCHGCGzkLaq3fvO9XnVg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-11.0.0.tgz", + "integrity": "sha512-qXqRqiCFJD5CJ61CSJuNImTFrm3zVkOU9XywHDwrUuvWN74MWt72KJ67c5CM5x8g0vGcOkRVCrYj85vqkmHulQ==", "requires": { - "rehype-parse": "^6.0.0", - "rehype-stringify": "^6.0.0", + "rehype-parse": "^7.0.0", + "rehype-stringify": "^8.0.0", "unified": "^9.0.0" } }, @@ -7770,32 +7694,23 @@ "hast-util-is-element": "^1.0.0", "hast-util-whitespace": "^1.0.4", "unist-util-is": "^4.0.0" - }, - "dependencies": { - "unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" - } } }, "rehype-parse": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-6.0.2.tgz", - "integrity": "sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-7.0.1.tgz", + "integrity": "sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==", "requires": { - "hast-util-from-parse5": "^5.0.0", - "parse5": "^5.0.0", - "xtend": "^4.0.0" + "hast-util-from-parse5": "^6.0.0", + "parse5": "^6.0.0" } }, "rehype-stringify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-6.0.1.tgz", - "integrity": "sha512-JfEPRDD4DiG7jet4md7sY07v6ACeb2x+9HWQtRPm2iA6/ic31hCv1SNBUtpolJASxQ/D8gicXiviW4TJKEMPKQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-8.0.0.tgz", + "integrity": "sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g==", "requires": { - "hast-util-to-html": "^6.0.0", - "xtend": "^4.0.0" + "hast-util-to-html": "^7.1.1" } }, "request": { @@ -7825,13 +7740,37 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } } } }, + "require-at": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", + "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7841,14 +7780,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "resolve": { "version": "1.20.0", @@ -7879,18 +7811,18 @@ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "optional": true, "requires": { "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", @@ -7911,15 +7843,23 @@ "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", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, "security": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/security/-/security-1.0.0.tgz", "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" }, "selenium-webdriver": { - "version": "4.0.0-beta.4", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.4.tgz", - "integrity": "sha512-+s/CIYkWzmnC9WASBxxVj7Lm0dcyl6OaFxwIJaFCT5WCuACiimEEr4lUnOOFP/QlKfkDQ56m+aRczaq2EvJEJg==", + "version": "4.0.0-rc-1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", + "integrity": "sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==", "dev": true, "requires": { "jszip": "^3.6.0", @@ -7940,9 +7880,12 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } }, "send": { "version": "0.17.1", @@ -7976,6 +7919,11 @@ "toidentifier": "1.0.0" } }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -7983,6 +7931,15 @@ } } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -7997,7 +7954,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true }, "set-cookie-parser": { "version": "2.4.8", @@ -8040,9 +7998,9 @@ } }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "optional": true }, "simple-concat": { @@ -8063,9 +8021,9 @@ } }, "simple-git": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.40.0.tgz", - "integrity": "sha512-7IO/eQwrN5kvS38TTu9ljhG9tx2nn0BTqZOmqpPpp51TvE44YIvLA6fETqEVA8w/SeEfPaVv6mk7Tsk9Jns+ag==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.46.0.tgz", + "integrity": "sha512-6eumII1vfP4NpRqxZcVWCcIT5xHH6dRyvBZSjkH4dJRDRpv+0f75hrN5ysp++y23Mfr3AbRC/dO2NDbfj1lJpQ==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -8073,9 +8031,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -8088,25 +8046,19 @@ } }, "sinon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" }, "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8284,9 +8236,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8318,6 +8270,31 @@ "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "requires": { "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "sprintf-js": { @@ -8326,14 +8303,13 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" }, "sqlite3": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", - "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==", + "version": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a", + "from": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a", "optional": true, "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", "node-addon-api": "^3.0.0", - "node-gyp": "3.x", - "node-pre-gyp": "^0.11.0" + "node-gyp": "7.x" } }, "sqlstring": { @@ -8377,44 +8353,19 @@ "strip-ansi": "^3.0.0" } }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "stringify-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-2.0.0.tgz", - "integrity": "sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", "requires": { "character-entities-html4": "^1.0.0", "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.2", - "is-hexadecimal": "^1.0.0" + "xtend": "^4.0.0" } }, "strip-ansi": { @@ -8428,87 +8379,105 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "optional": true }, "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", "dev": true, "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" }, "dependencies": { "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", "dev": true }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "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": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } } } }, "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.1.6.tgz", + "integrity": "sha512-0hACYGNJ8OHRg8CRITeZOdbjur7NLuNs0mBjVhdpxi7hP6t3QIbOzLON5RTUmZcy2I9riuII3+Pr2C7yztrIIg==", "dev": true, "requires": { "methods": "^1.1.2", - "superagent": "^3.8.3" + "superagent": "^6.1.0" } }, "supports-color": { @@ -8522,7 +8491,13 @@ "swagger-schema-official": { "version": "2.0.0-bab6bed", "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" + "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=", + "dev": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "table": { "version": "6.7.1", @@ -8538,18 +8513,6 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "ajv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", - "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -8562,12 +8525,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -8591,14 +8548,25 @@ } }, "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "optional": true, "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true + } } }, "tar-fs": { @@ -8635,6 +8603,40 @@ "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "optional": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" } } } @@ -8645,36 +8647,44 @@ "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, "tedious": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.0.9.tgz", - "integrity": "sha512-VEIDlPYQNp9Mct0LDFV5O4cihyq/7D+UU0WH6973+NnQZessYe3CFggHeyfKRw2Dx8AQtWB6tOg4misKiG2mpg==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.8.0.tgz", + "integrity": "sha512-GtFrO694x/7CRiUBt0AI4jrMtrkXV+ywifiOrDy4K0ufJLeKB4rgmPjy5Ws366fCaBaKlqQ9RnJ+sCJ1Jbd1lw==", "requires": { "@azure/identity": "^1.3.0", "@azure/keyvault-keys": "^4.1.0", "@azure/ms-rest-nodeauth": "^3.0.6", "@js-joda/core": "^3.2.0", "adal-node": "^0.2.1", - "bl": "^4.0.3", + "bl": "^5.0.0", "depd": "^2.0.0", - "iconv-lite": "^0.6.2", - "jsbi": "^3.1.4", + "iconv-lite": "^0.6.3", + "jsbi": "^3.1.5", "native-duplexpair": "^1.0.0", - "node-abort-controller": "^1.1.0", + "node-abort-controller": "^2.0.0", "punycode": "^2.1.0", - "readable-stream": "^3.6.0", "sprintf-js": "^1.1.2" }, "dependencies": { "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", + "integrity": "sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==", "requires": { - "buffer": "^5.5.0", + "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8687,17 +8697,52 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } } } }, "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", + "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", "requires": { "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } } }, "text-table": { @@ -8707,9 +8752,9 @@ "dev": true }, "threads": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.5.tgz", - "integrity": "sha512-yL1NN4qZ25crW8wDoGn7TqbENJ69w3zCEjIGXpbqmQ4I+QHrG8+DLaZVKoX74OQUXWCI2lbbrUxDxAbr1xjDGQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", + "integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==", "requires": { "callsites": "^3.1.0", "debug": "^4.2.0", @@ -8719,9 +8764,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -8786,11 +8831,20 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", "requires": { - "psl": "^1.1.28", "punycode": "^2.1.1" } }, @@ -8800,9 +8854,9 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" }, "tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tunnel": { "version": "0.0.6", @@ -8823,12 +8877,11 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "requires": { - "prelude-ls": "^1.2.1" + "prelude-ls": "~1.1.2" } }, "type-detect": { @@ -8853,23 +8906,23 @@ } }, "ueberdb2": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.10.tgz", - "integrity": "sha512-Odg0Mdj17oK6biHuHMKN2DLny+WDG07qb2JoRx6SoluRIXb6eqceK7TilkydyDSiY6MrEeqWcjh9NKBLS2KtSQ==", - "requires": { - "async": "^3.2.0", - "cassandra-driver": "^4.5.1", - "dirty": "^1.1.1", - "elasticsearch": "^16.7.1", - "mongodb": "^3.6.3", - "mssql": "^7.0.0-beta.2", + "version": "1.4.18", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.18.tgz", + "integrity": "sha512-u0Joo4FpNPw4PeTJTPe6GIZBFscZ8DbIFuD0cd60mMbkBpAh7l039hhOxoAGHuF0eRM9QEEqPpOunlOOJ1TTeg==", + "requires": { + "async": "^3.2.1", + "cassandra-driver": "^4.6.3", + "dirty": "^1.1.3", + "elasticsearch": "^16.7.2", + "mongodb": "^3.7.1", + "mssql": "^7.2.1", "mysql": "2.18.1", - "nano": "^8.2.2", - "pg": "^8.0.3", + "nano": "^9.0.5", + "pg": "^8.7.1", "redis": "^3.1.2", "rethinkdb": "^2.4.2", - "simple-git": "^2.4.0", - "sqlite3": "^5.0.1" + "simple-git": "^2.45.1", + "sqlite3": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a" } }, "uid-safe": { @@ -8880,27 +8933,15 @@ "random-bytes": "~1.0.0" } }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, "underscore": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" }, "unified": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", - "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", "requires": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -8911,9 +8952,9 @@ } }, "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" }, "unist-util-stringify-position": { "version": "2.0.3", @@ -8993,6 +9034,11 @@ "vfile-message": "^2.0.0" } }, + "vfile-location": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", + "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==" + }, "vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", @@ -9002,11 +9048,54 @@ "unist-util-stringify-position": "^2.0.0" } }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "requires": { + "xml-name-validator": "^3.0.0" + } + }, "web-namespaces": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==" }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz", + "integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==", + "requires": { + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9015,25 +9104,6 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -9045,56 +9115,79 @@ "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==", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -9105,18 +9198,19 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.1.tgz", + "integrity": "sha512-XkgWpJU3sHU7gX8f13NqTn6KQ85bd1WU7noBHTT8fSohx7OS1TPY8k+cyRPCzFkia7C4mM229yeHr1qK9sM4JQ==" }, "wtfnode": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.0.tgz", - "integrity": "sha512-IKHfNAFZwfm0uCt/zuFADN3mHyoB+ZrmwFpRGOxKPIXV0tifqpIaTH3NvImA7yy7GimsAayZGTaNvOmavKzE+A==", - "requires": { - "coffeescript": "^2.5.1", - "source-map-support": "^0.5.19" - } + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", + "integrity": "sha512-Ip6C2KeQPl/F3aP1EfOnPoQk14Udd9lffpoqWDNH3Xt78svxPbv53ngtmtfI0q2Te3oTq79XKTnRNXVIn/GsPA==" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, "xml2js": { "version": "0.4.23", @@ -9132,10 +9226,10 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, - "xmldom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", - "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, "xmlhttprequest-ssl": { "version": "1.6.3", @@ -9153,100 +9247,93 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" } }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/src/package.json b/src/package.json index 61c4cac157d..90ac13b36ff 100644 --- a/src/package.json +++ b/src/package.json @@ -30,55 +30,53 @@ } ], "dependencies": { - "async": "^3.2.0", - "async-stacktrace": "0.0.2", - "channels": "0.0.4", - "cheerio": "0.22.0", - "clean-css": "4.2.3", + "async": "^3.2.1", + "clean-css": "^5.2.1", "cookie-parser": "1.4.5", "cross-spawn": "^7.0.3", "ejs": "^3.1.6", - "etherpad-require-kernel": "1.0.11", - "etherpad-yajsml": "0.0.4", + "etherpad-require-kernel": "^1.0.15", + "etherpad-yajsml": "0.0.12", "express": "4.17.1", - "express-rate-limit": "5.2.6", + "express-rate-limit": "5.5.0", "express-session": "1.17.2", + "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", "formidable": "1.2.2", "http-errors": "1.8.0", - "js-cookie": "^2.2.1", + "js-cookie": "^3.0.1", + "jsdom": "^17.0.0", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", "log4js": "0.6.38", - "measured-core": "1.51.1", - "mime-types": "^2.1.27", - "nodeify": "1.0.1", - "npm": "6.14.13", - "openapi-backend": "^3.9.1", - "proxy-addr": "^2.0.6", - "rate-limiter-flexible": "^2.1.4", - "rehype": "^10.0.0", + "measured-core": "^2.0.0", + "mime-types": "^2.1.33", + "npm": "^6.14.15", + "openapi-backend": "^4.2.0", + "proxy-addr": "^2.0.7", + "rate-limiter-flexible": "^2.3.1", + "rehype": "^11.0.0", "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", "resolve": "1.20.0", "security": "1.0.0", - "semver": "5.7.1", + "semver": "^7.3.5", "socket.io": "^2.4.1", - "terser": "^4.7.0", - "threads": "^1.4.0", + "terser": "^5.9.0", + "threads": "^1.7.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", - "ueberdb2": "^1.4.7", + "ueberdb2": "^1.4.18", "underscore": "1.13.1", "unorm": "1.6.0", - "wtfnode": "^0.9.0" + "wtfnode": "^0.9.1" }, "bin": { "etherpad-lite": "node/server.js" }, "devDependencies": { - "eslint": "^7.28.0", + "eslint": "^7.32.0", "eslint-config-etherpad": "^2.0.0", "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-eslint-comments": "^3.2.0", @@ -87,16 +85,17 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", - "etherpad-cli-client": "0.0.9", - "mocha": "7.1.2", + "etherpad-cli-client": "^0.1.12", + "mocha": "^9.1.1", "mocha-froth": "^0.2.10", + "nodeify": "^1.0.1", "openapi-schema-validation": "^0.4.2", - "selenium-webdriver": "^4.0.0-beta.3", - "set-cookie-parser": "^2.4.6", - "sinon": "^9.2.0", + "selenium-webdriver": "^4.0.0-rc-1", + "set-cookie-parser": "^2.4.8", + "sinon": "^11.1.2", "split-grid": "^1.0.11", - "superagent": "^3.8.3", - "supertest": "4.0.2" + "superagent": "^6.1.0", + "supertest": "^6.1.6" }, "eslintConfig": { "ignorePatterns": [ @@ -247,6 +246,6 @@ "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --timeout 5000 tests/container/specs/api" }, - "version": "1.8.14", + "version": "1.8.15", "license": "Apache-2.0" } diff --git a/src/static/css/pad/chat.css b/src/static/css/pad/chat.css index 98f96109be6..bfda0109acc 100644 --- a/src/static/css/pad/chat.css +++ b/src/static/css/pad/chat.css @@ -92,12 +92,18 @@ #chattext p { padding: 3px; overflow-x: hidden; + white-space: pre-wrap; word-wrap: break-word; } #chattext .time { float: right; font-style: italic; - font-size: .85rem; + /* + * 'smaller' is relative to the parent element, so if the parent has its own + * 'font-size: smaller' rule then the timestamp will become even smaller (as + * desired). + */ + font-size: smaller; opacity: .8; margin-left: 3px; margin-right: 2px; @@ -109,6 +115,7 @@ } #chatinputbox #chatinput { width: 100%; + resize: vertical; } diff --git a/src/static/css/pad/icons.css b/src/static/css/pad/icons.css index 0ae7ed2db6a..eb1016dca5f 100644 --- a/src/static/css/pad/icons.css +++ b/src/static/css/pad/icons.css @@ -55,9 +55,6 @@ } .buttonicon-clearauthorship:before { content: "\e843"; - left: -9px; - position: absolute; - top: -9px; } .buttonicon-settings:before { content: "\e851"; @@ -87,9 +84,9 @@ .ep_font_color .buttonicon:before { content: '\e84e' !important; border-bottom: solid 2px #e42a2a; } .buttonicon-underline:before { - top: -8px; - left: -8px; - position: absolute; + /* The baseline of the underscore glyph seems off. Compensate for it here. */ + top: 0.1em; + position: relative; } /* COPY CSS GENERATED BY FONTELLO HERE */ diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 0eb00099673..6de108e409e 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -45,9 +45,6 @@ .popup input[type=text], #users input[type=text] { outline: none; } -.popup a { - text-decoration: none -} .popup h1 { font-size: 1.8rem; margin-bottom: 10px; diff --git a/src/static/empty.html b/src/static/empty.html new file mode 100644 index 00000000000..ef02d22ca72 --- /dev/null +++ b/src/static/empty.html @@ -0,0 +1 @@ +Empty diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index a2ea15b6c31..12443403169 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -103,12 +103,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ const markerWidth = this.lineHasMarker(row) ? 1 : 0; if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); - const startCol = row === start[0] ? start[1] : markerWidth; - if (startCol - markerWidth < 0) throw new RangeError('selection starts before line start'); + if (start[1] < 0) throw new RangeError('selection starts at negative column'); + const startCol = Math.max(markerWidth, row === start[0] ? start[1] : 0); if (startCol > lineLength) throw new RangeError('selection starts after line end'); - const endCol = row === end[0] ? end[1] : lineLength; - if (endCol - markerWidth < 0) throw new RangeError('selection ends before line start'); + if (end[1] < 0) throw new RangeError('selection ends at negative column'); + const endCol = Math.max(markerWidth, row === end[0] ? end[1] : lineLength); if (endCol > lineLength) throw new RangeError('selection ends after line end'); if (startCol > endCol) throw new RangeError('selection ends before it starts'); diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 79849f4d39e..d8e5876548f 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -23,75 +23,171 @@ * limitations under the License. */ -/* - An AttributePool maintains a mapping from [key,value] Pairs called - Attributes to Numbers (unsigened integers) and vice versa. These numbers are - used to reference Attributes in Changesets. -*/ +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ -const AttributePool = function () { - this.numToAttrib = {}; // e.g. {0: ['foo','bar']} - this.attribToNum = {}; // e.g. {'foo,bar': 0} - this.nextNum = 0; -}; +/** + * Maps an attribute's identifier to the attribute. + * + * @typedef {Object.} NumToAttrib + */ -AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) { - const str = String(attrib); - if (str in this.attribToNum) { - return this.attribToNum[str]; - } - if (dontAddIfAbsent) { - return -1; +/** + * An intermediate representation of the contents of an attribute pool, suitable for serialization + * via `JSON.stringify` and transmission to another user. + * + * @typedef {Object} Jsonable + * @property {NumToAttrib} numToAttrib - The pool's attributes and their identifiers. + * @property {number} nextNum - The attribute ID to assign to the next new attribute. + */ + +/** + * Represents an attribute pool, which is a collection of attributes (pairs of key and value + * strings) along with their identifiers (non-negative integers). + * + * The attribute pool enables attribute interning: rather than including the key and value strings + * in changesets, changesets reference attributes by their identifiers. + * + * There is one attribute pool per pad, and it includes every current and historical attribute used + * in the pad. + */ +class AttributePool { + constructor() { + /** + * Maps an attribute identifier to the attribute's `[key, value]` string pair. + * + * TODO: Rename to `_numToAttrib` once all users have been migrated to call `getAttrib` instead + * of accessing this directly. + * @private + * TODO: Convert to an array. + * @type {NumToAttrib} + */ + this.numToAttrib = {}; // e.g. {0: ['foo','bar']} + + /** + * Maps the string representation of an attribute (`String([key, value])`) to its non-negative + * identifier. + * + * TODO: Rename to `_attribToNum` once all users have been migrated to use `putAttrib` instead + * of accessing this directly. + * @private + * TODO: Convert to a `Map` object. + * @type {Object.} + */ + this.attribToNum = {}; // e.g. {'foo,bar': 0} + + /** + * The attribute ID to assign to the next new attribute. + * + * TODO: This property will not be necessary once `numToAttrib` is converted to an array (just + * push onto the array). + * + * @private + * @type {number} + */ + this.nextNum = 0; } - const num = this.nextNum++; - this.attribToNum[str] = num; - this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; -}; -AttributePool.prototype.getAttrib = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) { - return pair; + /** + * Add an attribute to the attribute set, or query for an existing attribute identifier. + * + * @param {Attribute} attrib - The attribute's `[key, value]` pair of strings. + * @param {boolean} [dontAddIfAbsent=false] - If true, do not insert the attribute into the pool + * if the attribute does not already exist in the pool. This can be used to test for + * membership in the pool without mutating the pool. + * @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool. + */ + putAttrib(attrib, dontAddIfAbsent = false) { + const str = String(attrib); + if (str in this.attribToNum) { + return this.attribToNum[str]; + } + if (dontAddIfAbsent) { + return -1; + } + const num = this.nextNum++; + this.attribToNum[str] = num; + this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; + return num; } - return [pair[0], pair[1]]; // return a mutable copy -}; -AttributePool.prototype.getAttribKey = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; -}; + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {Attribute} The attribute with the given identifier, or nullish if there is no such + * attribute. + */ + getAttrib(num) { + const pair = this.numToAttrib[num]; + if (!pair) { + return pair; + } + return [pair[0], pair[1]]; // return a mutable copy + } -AttributePool.prototype.getAttribValue = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; -}; + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty + * string. + */ + getAttribKey(num) { + const pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[0]; + } -AttributePool.prototype.eachAttrib = function (func) { - for (const n of Object.keys(this.numToAttrib)) { - const pair = this.numToAttrib[n]; - func(pair[0], pair[1]); + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty + * string. + */ + getAttribValue(num) { + const pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[1]; } -}; -AttributePool.prototype.toJsonable = function () { - return { - numToAttrib: this.numToAttrib, - nextNum: this.nextNum, - }; -}; + /** + * Executes a callback for each attribute in the pool. + * + * @param {Function} func - Callback to call with two arguments: key and value. Its return value + * is ignored. + */ + eachAttrib(func) { + for (const n of Object.keys(this.numToAttrib)) { + const pair = this.numToAttrib[n]; + func(pair[0], pair[1]); + } + } -AttributePool.prototype.fromJsonable = function (obj) { - this.numToAttrib = obj.numToAttrib; - this.nextNum = obj.nextNum; - this.attribToNum = {}; - for (const n of Object.keys(this.numToAttrib)) { - this.attribToNum[String(this.numToAttrib[n])] = Number(n); + /** + * @returns {Jsonable} An object that can be passed to `fromJsonable` to reconstruct this + * attribute pool. The returned object can be converted to JSON. + */ + toJsonable() { + return { + numToAttrib: this.numToAttrib, + nextNum: this.nextNum, + }; } - return this; -}; + /** + * Replace the contents of this attribute pool with values from a previous call to `toJsonable`. + * + * @param {Jsonable} obj - Object returned by `toJsonable` containing the attributes and their + * identifiers. + */ + fromJsonable(obj) { + this.numToAttrib = obj.numToAttrib; + this.nextNum = obj.nextNum; + this.attribToNum = {}; + for (const n of Object.keys(this.numToAttrib)) { + this.attribToNum[String(this.numToAttrib[n])] = Number(n); + } + return this; + } +} module.exports = AttributePool; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index d985e504f8b..a335f7ccb88 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1,17 +1,5 @@ 'use strict'; -/* - * This is the Changeset library copied from the old Etherpad with some modifications - * to use it in node.js - * Can be found in https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js - */ - -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /* * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * @@ -28,98 +16,139 @@ * limitations under the License. */ +/* + * This is the Changeset library copied from the old Etherpad with some modifications + * to use it in node.js. The original can be found at: + * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js + */ + const AttributePool = require('./AttributePool'); +const {padutils} = require('./pad_utils'); /** - * ==================== General Util Functions ======================= + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute */ /** - * This method is called whenever there is an error in the sync process - * @param msg {string} Just some message + * This method is called whenever there is an error in the sync process. + * + * @param {string} msg - Just some message */ -exports.error = (msg) => { +const error = (msg) => { const e = new Error(msg); e.easysync = true; throw e; }; /** - * This method is used for assertions with Messages - * if assert fails, the error function is called. - * @param b {boolean} assertion condition - * @param msgParts {string} error to be passed if it fails + * Assert that a condition is truthy. If the condition is falsy, the `error` function is called to + * throw an exception. + * + * @param {boolean} b - assertion condition + * @param {string} msg - error message to include in the exception + * @type {(b: boolean, msg: string) => asserts b} */ -exports.assert = (b, ...msgParts) => { - if (!b) { - exports.error(`Failed assertion: ${msgParts.join('')}`); - } +const assert = (b, msg) => { + if (!b) error(`Failed assertion: ${msg}`); }; /** - * Parses a number from string base 36 - * @param str {string} string of the number in base 36 - * @returns {int} number + * Parses a number from string base 36. + * + * @param {string} str - string of the number in base 36 + * @returns {number} number */ exports.parseNum = (str) => parseInt(str, 36); /** - * Writes a number in base 36 and puts it in a string - * @param num {int} number + * Writes a number in base 36 and puts it in a string. + * + * @param {number} num - number * @returns {string} string */ exports.numToString = (num) => num.toString(36).toLowerCase(); /** - * Converts stuff before $ to base 10 - * @obsolete not really used anywhere?? - * @param cs {string} the string - * @return integer + * An operation to apply to a shared document. + * + * @typedef {object} Op + * @property {('+'|'-'|'='|'')} opcode - The operation's operator: + * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in + * the document. The inserted characters come from the changeset's character bank. + * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an + * operation. + * @property {number} chars - The number of characters to keep, insert, or delete. + * @property {number} lines - The number of characters among the `chars` characters that are + * newlines. If non-zero, the last character must be a newline. + * @property {string} attribs - Identifiers of attributes to apply to the text, represented as a + * repeated (zero or more) sequence of asterisk followed by a non-negative base-36 (lower-case) + * integer. For example, '*2*1o' indicates that attributes 2 and 60 apply to the text affected + * by the operation. The identifiers come from the document's attribute pool. This is the empty + * string for remove ('-') operations. For keep ('=') operations, the attributes are merged with + * the base text's existing attributes: + * - A keep op attribute with a non-empty value replaces an existing base text attribute that + * has the same key. + * - A keep op attribute with an empty value is interpreted as an instruction to remove an + * existing base text attribute that has the same key, if one exists. */ -exports.toBaseTen = (cs) => { - const dollarIndex = cs.indexOf('$'); - const beforeDollar = cs.substring(0, dollarIndex); - const fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(exports.parseNum(s))) + fromDollar; -}; - /** - * ==================== Changeset Functions ======================= + * 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 cs {string} String representation of the Changeset + * Returns the required length of the text before changeset can be applied. + * + * @param {string} cs - String representation of the Changeset + * @returns {number} oldLen property */ exports.oldLen = (cs) => exports.unpack(cs).oldLen; /** - * returns the length of the text after changeset is applied - * @param cs {string} String representation of the Changeset + * Returns the length of the text after changeset is applied. + * + * @param {string} cs - String representation of the Changeset + * @returns {number} newLen property */ exports.newLen = (cs) => exports.unpack(cs).newLen; /** - * this function creates an iterator which decodes string changeset operations - * @param opsStr {string} String encoding of the change operations to be performed - * @param optStartIndex {int} from where in the string should the iterator start - * @return {Op} type object iterator + * Iterator over a changeset's operations. + * + * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. + * + * @typedef {object} OpIter + * @property {Function} hasNext - + * @property {Function} next - */ -exports.opIterator = (opsStr, optStartIndex) => { + +/** + * Creates an iterator which decodes string changeset operations. + * + * @param {string} opsStr - String encoding of the change operations to perform. + * @returns {OpIter} Operator iterator object. + */ +exports.opIterator = (opsStr) => { const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - const startIndex = (optStartIndex || 0); - let curIndex = startIndex; - let prevIndex = curIndex; const nextRegexMatch = () => { - prevIndex = curIndex; - regex.lastIndex = curIndex; const result = regex.exec(opsStr); - curIndex = regex.lastIndex; if (result[0] === '?') { - exports.error('Hit error opcode in op stream'); + error('Hit error opcode in op stream'); } return result; @@ -130,32 +159,30 @@ exports.opIterator = (opsStr, optStartIndex) => { const op = optOp || exports.newOp(); if (regexResult[0]) { op.attribs = regexResult[1]; - op.lines = exports.parseNum(regexResult[2] || 0); + op.lines = exports.parseNum(regexResult[2] || '0'); op.opcode = regexResult[3]; op.chars = exports.parseNum(regexResult[4]); regexResult = nextRegexMatch(); } else { - exports.clearOp(op); + clearOp(op); } return op; }; const hasNext = () => !!(regexResult[0]); - const lastIndex = () => prevIndex; - return { next, hasNext, - lastIndex, }; }; /** - * Cleans an Op object - * @param {Op} object to be cleared + * Cleans an Op object. + * + * @param {Op} op - object to clear */ -exports.clearOp = (op) => { +const clearOp = (op) => { op.opcode = ''; op.chars = 0; op.lines = 0; @@ -164,7 +191,9 @@ exports.clearOp = (op) => { /** * Creates a new Op object - * @param optOpcode the type operation of the Op object + * + * @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator. + * @returns {Op} */ exports.newOp = (optOpcode) => ({ opcode: (optOpcode || ''), @@ -174,51 +203,90 @@ exports.newOp = (optOpcode) => ({ }); /** - * Clones an Op - * @param op Op to be cloned + * 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` */ -exports.cloneOp = (op) => ({ - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs, -}); +const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1); /** - * Copies op1 to op2 - * @param op1 src Op - * @param op2 dest Op - */ -exports.copyOp = (op1, op2) => { - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; + * Serializes a sequence of Ops. + * + * @typedef {object} OpAssembler + * @property {Function} append - + * @property {Function} clear - + * @property {Function} toString - + */ /** - * Writes the Op in a string the way that changesets need it + * 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 - */ -exports.opString = (op) => { - // just for debugging - if (!op.opcode) return 'null'; - const assem = exports.opAssembler(); - assem.append(op); - return assem.toString(); + +/** + * Generates operations from the given text and attributes. + * + * @param {('-'|'+'|'=')} opcode - The operator to use. + * @param {string} text - The text to remove/add/keep. + * @param {(string|Attribute[])} [attribs] - The attributes to apply to the operations. See + * `makeAttribsString`. + * @param {?AttributePool} [pool] - See `makeAttribsString`. + * @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text. + * @returns {Generator} + */ +const opsFromText = function* (opcode, text, attribs = '', pool = null) { + const op = exports.newOp(opcode); + op.attribs = exports.makeAttribsString(opcode, attribs, pool); + const lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + yield op; + } else { + op.chars = lastNewlinePos + 1; + op.lines = text.match(/\n/g).length; + yield op; + const op2 = copyOp(op); + op2.chars = text.length - (lastNewlinePos + 1); + op2.lines = 0; + yield op2; + } }; /** - * Used just for debugging + * 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 - */ -exports.stringOp = (str) => exports.opIterator(str).next(); /** - * Used to check if a Changeset if valid - * @param cs {Changeset} Changeset to be checked + * Used to check if a Changeset is valid. This function does not check things that require access to + * the attribute pool (e.g., attribute order) or original text (e.g., newline positions). + * + * @param {string} cs - Changeset to check + * @returns {string} the checked Changeset */ exports.checkRep = (cs) => { - // doesn't check things that require access to attrib pool (e.g. attribute order) - // or original string (e.g. newline positions) const unpacked = exports.unpack(cs); const oldLen = unpacked.oldLen; const newLen = unpacked.newLen; @@ -239,13 +307,13 @@ exports.checkRep = (cs) => { break; case '-': oldPos += o.chars; - exports.assert(oldPos <= oldLen, oldPos, ' > ', oldLen, ' in ', cs); + assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); break; case '+': { calcNewLen += o.chars; numInserted += o.chars; - exports.assert(calcNewLen <= newLen, calcNewLen, ' > ', newLen, ' in ', cs); + assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); break; } } @@ -260,28 +328,15 @@ exports.checkRep = (cs) => { assem.endDocument(); const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); - exports.assert(normalized === cs, 'Invalid changeset (checkRep failed)'); + assert(normalized === cs, 'Invalid changeset (checkRep failed)'); return cs; }; - -/** - * ==================== Util Functions ======================= - */ - /** - * creates an object that allows you to append operations (type Op) and also - * compresses them if possible + * @returns {SmartOpAssembler} */ exports.smartOpAssembler = () => { - // Like opAssembler but able to produce conforming exportss - // from slightly looser input, at the cost of speed. - // Specifically: - // - merges consecutive operations that can be merged - // - strips final "=" - // - ignores 0-length changes - // - reorders consecutive + and - (which margingOpAssembler doesn't do) const minusAssem = exports.mergingOpAssembler(); const plusAssem = exports.mergingOpAssembler(); const keepAssem = exports.mergingOpAssembler(); @@ -326,22 +381,20 @@ exports.smartOpAssembler = () => { 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|Attribute[])} attribs - The attributes to apply to the operations. See + * `makeAttribsString`. + * @param {?AttributePool} pool - See `makeAttribsString`. + */ const appendOpWithText = (opcode, text, attribs, pool) => { - const op = exports.newOp(opcode); - op.attribs = exports.makeAttribsString(opcode, attribs, pool); - const lastNewlinePos = text.lastIndexOf('\n'); - if (lastNewlinePos < 0) { - op.chars = text.length; - op.lines = 0; - append(op); - } else { - op.chars = lastNewlinePos + 1; - op.lines = text.match(/\n/g).length; - append(op); - op.chars = text.length - (lastNewlinePos + 1); - op.lines = 0; - append(op); - } + padutils.warnWithStack('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + + 'use opsFromText() instead.'); + for (const op of opsFromText(opcode, text, attribs, pool)) append(op); }; const toString = () => { @@ -374,12 +427,10 @@ exports.smartOpAssembler = () => { }; }; - +/** + * @returns {MergingOpAssembler} + */ exports.mergingOpAssembler = () => { - // This assembler can be used in production; it efficiently - // merges consecutive operations that are mergeable, ignores - // no-ops, and drops final pure "keeps". It does not re-order - // operations. const assem = exports.opAssembler(); const bufOp = exports.newOp(); @@ -390,41 +441,39 @@ exports.mergingOpAssembler = () => { let bufOpAdditionalCharsAfterNewline = 0; const flush = (isEndDocument) => { - if (bufOp.opcode) { - if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { - // final merged keep, leave it implicit - } else { + if (!bufOp.opcode) return; + if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { + // final merged keep, leave it implicit + } else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; assem.append(bufOp); - if (bufOpAdditionalCharsAfterNewline) { - bufOp.chars = bufOpAdditionalCharsAfterNewline; - bufOp.lines = 0; - assem.append(bufOp); - bufOpAdditionalCharsAfterNewline = 0; - } + bufOpAdditionalCharsAfterNewline = 0; } - bufOp.opcode = ''; } + bufOp.opcode = ''; }; const append = (op) => { - if (op.chars > 0) { - if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) { - if (op.lines > 0) { - // bufOp and additional chars are all mergeable into a multi-line op - bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; - bufOp.lines += op.lines; - bufOpAdditionalCharsAfterNewline = 0; - } else if (bufOp.lines === 0) { - // both bufOp and op are in-line - bufOp.chars += op.chars; - } else { - // append in-line text to multi-line bufOp - bufOpAdditionalCharsAfterNewline += op.chars; - } + if (op.chars <= 0) return; + if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } else if (bufOp.lines === 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; } else { - flush(); - exports.copyOp(op, bufOp); + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; } + } else { + flush(); + copyOp(op, bufOp); } }; @@ -439,7 +488,7 @@ exports.mergingOpAssembler = () => { const clear = () => { assem.clear(); - exports.clearOp(bufOp); + clearOp(bufOp); }; return { append, @@ -449,24 +498,28 @@ exports.mergingOpAssembler = () => { }; }; - +/** + * @returns {OpAssembler} + */ exports.opAssembler = () => { - const pieces = []; - // this function allows op to be mutated later (doesn't keep a ref) + let serialized = ''; + /** + * @param {Op} op - Operation to add. Ownership remains with the caller. + */ const append = (op) => { - pieces.push(op.attribs); - if (op.lines) { - pieces.push('|', exports.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(exports.numToString(op.chars)); + if (!op.opcode) throw new TypeError('null op'); + if (typeof op.attribs !== 'string') throw new TypeError('attribs must be a string'); + serialized += op.attribs; + if (op.lines) serialized += `|${exports.numToString(op.lines)}`; + serialized += op.opcode; + serialized += exports.numToString(op.chars); }; - const toString = () => pieces.join(''); + const toString = () => serialized; const clear = () => { - pieces.length = 0; + serialized = ''; }; return { append, @@ -477,7 +530,18 @@ exports.opAssembler = () => { /** * A custom made String Iterator - * @param str {string} String to be iterated over + * + * @typedef {object} StringIterator + * @property {Function} newlines - + * @property {Function} peek - + * @property {Function} remaining - + * @property {Function} skip - + * @property {Function} take - + */ + +/** + * @param {string} str - String to iterate over + * @returns {StringIterator} */ exports.stringIterator = (str) => { let curIndex = 0; @@ -486,7 +550,7 @@ exports.stringIterator = (str) => { const getnewLines = () => newLines; const assertRemaining = (n) => { - exports.assert(n <= remaining(), '!(', n, ' <= ', remaining(), ')'); + assert(n <= remaining(), `!(${n} <= ${remaining()})`); }; const take = (n) => { @@ -520,39 +584,77 @@ exports.stringIterator = (str) => { /** * A custom made StringBuffer + * + * @typedef {object} StringAssembler + * @property {Function} append - + * @property {Function} toString - */ -exports.stringAssembler = () => { - const pieces = []; - const append = (x) => { - pieces.push(String(x)); - }; +/** + * @returns {StringAssembler} + */ +exports.stringAssembler = () => ({ + _str: '', + /** + * @param {string} x - + */ + append(x) { this._str += String(x); }, + toString() { return this._str; }, +}); - const toString = () => pieces.join(''); - return { - append, - toString, - }; -}; +/** + * @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()`. + */ /** - * This class allows to iterate and modify texts which have several lines - * It is used for applying Changesets on arrays of lines - * Note from prev docs: "lines" need not be an array as long as it supports - * certain calls (lines_foo inside). - */ -exports.textLinesMutator = (lines) => { - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as exports operations - // with respect to newlines, but not the other additional constraints - // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). - // Can be used to mutate lists of strings where the last char of each string - // is not actually a newline, but for the purposes of N and L values, - // the caller should pretend it is, and for things to work right in that case, the input - // to insert() should be a single line with no newlines. + * Class to iterate and modify texts which have several lines. It is used for applying Changesets on + * arrays of lines. + * + * Mutation operations have the same constraints as exports operations with respect to newlines, but + * not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability, + * final newline). Can be used to mutate lists of strings where the last char of each string is not + * actually a newline, but for the purposes of N and L values, the caller should pretend it is, and + * for things to work right in that case, the input to the `insert` method should be a single line + * with no newlines. + * + * @typedef {object} TextLinesMutator + * @property {Function} close - + * @property {Function} hasMore - + * @property {Function} insert - + * @property {Function} remove - + * @property {Function} removeLines - + * @property {Function} skip - + * @property {Function} skipLines - + */ + +/** + * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). + * @returns {TextLinesMutator} + */ +const textLinesMutator = (lines) => { + /** + * curSplice holds values that will be passed as arguments to lines.splice() to insert, delete, or + * change lines: + * - curSplice[0] is an index into the lines array. + * - curSplice[1] is the number of lines that will be removed from the lines array starting at + * the index. + * - The other elements represent mutated (changed by ops) lines or new lines (added by ops) to + * insert at the index. + * + * @type {[number, number?, ...string[]?]} + */ const curSplice = [0, 0]; let inSplice = false; - // position in document after curSplice is applied: + + // position in lines after curSplice is applied: let curLine = 0; let curCol = 0; // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && @@ -560,22 +662,28 @@ exports.textLinesMutator = (lines) => { // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then // curCol == 0 - const lines_applySplice = (s) => { - lines.splice.apply(lines, s); - }; - - const lines_toSource = () => lines.toSource(); - - const lines_get = (idx) => { - if (lines.get) { + /** + * Get a line from `lines` at given index. + * + * @param {number} idx - an index + * @returns {string} + */ + const linesGet = (idx) => { + if ('get' in lines) { return lines.get(idx); } else { return lines[idx]; } }; - // can be unimplemented if removeLines's return value not needed - const lines_slice = (start, end) => { + /** + * Return a slice from `lines`. + * + * @param {number} start - the start index + * @param {number} end - the end index + * @returns {string[]} + */ + const linesSlice = (start, end) => { if (lines.slice) { return lines.slice(start, end); } else { @@ -583,7 +691,12 @@ exports.textLinesMutator = (lines) => { } }; - const lines_length = () => { + /** + * Return the length of `lines`. + * + * @returns {number} + */ + const linesLength = () => { if ((typeof lines.length) === 'number') { return lines.length; } else { @@ -591,172 +704,236 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Starts a new splice. + */ const enterSplice = () => { curSplice[0] = curLine; curSplice[1] = 0; + // TODO(doc) when is this the case? + // check all enterSplice calls and changes to curCol if (curCol > 0) { putCurLineInSplice(); } inSplice = true; }; + /** + * Changes the lines array according to the values in curSplice and resets curSplice. Called via + * close or TODO(doc). + */ const leaveSplice = () => { - lines_applySplice(curSplice); + lines.splice(...curSplice); curSplice.length = 2; curSplice[0] = curSplice[1] = 0; inSplice = false; }; + /** + * Indicates if curLine is already in the splice. This is necessary because the last element in + * curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting). + * + * TODO(doc) why aren't removals considered? + * + * @returns {boolean} true if curLine is in splice + */ const isCurLineInSplice = () => (curLine - curSplice[0] < (curSplice.length - 2)); - const debugPrint = (typ) => { /* eslint-disable-line no-unused-vars */ - print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`); - }; - + /** + * Incorporates current line into the splice and marks its old position to be deleted. + * + * @returns {number} the index of the added line in curSplice + */ const putCurLineInSplice = () => { if (!isCurLineInSplice()) { - curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice.push(linesGet(curSplice[0] + curSplice[1])); curSplice[1]++; } - return 2 + curLine - curSplice[0]; + return 2 + curLine - curSplice[0]; // TODO should be the same as curSplice.length - 1 }; + /** + * It will skip some newlines by putting them into the splice. + * + * @param {number} L - + * @param {boolean} includeInSplice - indicates if attributes are present + */ const skipLines = (L, includeInSplice) => { - if (L) { - if (includeInSplice) { - if (!inSplice) { - enterSplice(); - } - for (let i = 0; i < L; i++) { - curCol = 0; + if (!L) return; + if (includeInSplice) { + if (!inSplice) enterSplice(); + // TODO(doc) should this count the number of characters that are skipped to check? + for (let i = 0; i < L; i++) { + curCol = 0; + putCurLineInSplice(); + curLine++; + } + } else { + if (inSplice) { + if (L > 1) { + // TODO(doc) figure out why single lines are incorporated into splice instead of ignored + leaveSplice(); + } else { putCurLineInSplice(); - curLine++; - } - } else { - if (inSplice) { - if (L > 1) { - leaveSplice(); - } else { - putCurLineInSplice(); - } } - curLine += L; - curCol = 0; } - // tests case foo in remove(), which isn't otherwise covered in current impl + curLine += L; + 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 + */ const skip = (N, L, includeInSplice) => { - if (N) { - if (L) { - skipLines(L, includeInSplice); - } else { - if (includeInSplice && !inSplice) { - enterSplice(); - } - if (inSplice) { - putCurLineInSplice(); - } - curCol += N; + if (!N) return; + if (L) { + skipLines(L, includeInSplice); + } else { + if (includeInSplice && !inSplice) enterSplice(); + if (inSplice) { + // although the line is put into splice curLine is not increased, because + // only some chars are skipped, not the whole line + putCurLineInSplice(); } + curCol += N; } }; + /** + * Remove whole lines from lines array. + * + * @param {number} L - number of lines to remove + * @returns {string} + */ const removeLines = (L) => { - let removed = ''; - if (L) { - if (!inSplice) { - enterSplice(); - } + if (!L) return ''; + if (!inSplice) enterSplice(); - const nextKLinesText = (k) => { - const m = curSplice[0] + curSplice[1]; - return lines_slice(m, m + k).join(''); - }; - if (isCurLineInSplice()) { - if (curCol === 0) { - removed = curSplice[curSplice.length - 1]; - curSplice.length--; - removed += nextKLinesText(L - 1); - curSplice[1] += L - 1; - } else { - removed = nextKLinesText(L - 1); - curSplice[1] += L - 1; - const sline = curSplice.length - 1; - removed = curSplice[sline].substring(curCol) + removed; - curSplice[sline] = curSplice[sline].substring(0, curCol) + - lines_get(curSplice[0] + curSplice[1]); - curSplice[1] += 1; - } + /** + * Gets a string of joined lines after the end of the splice. + * + * @param {number} k - number of lines + * @returns {string} joined lines + */ + const nextKLinesText = (k) => { + const m = curSplice[0] + curSplice[1]; + return linesSlice(m, m + k).join(''); + }; + + let removed = ''; + if (isCurLineInSplice()) { + if (curCol === 0) { + removed = curSplice[curSplice.length - 1]; + curSplice.length--; + removed += nextKLinesText(L - 1); + curSplice[1] += L - 1; } else { - removed = nextKLinesText(L); - curSplice[1] += L; + removed = nextKLinesText(L - 1); + curSplice[1] += L - 1; + const sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + + linesGet(curSplice[0] + curSplice[1]); + curSplice[1] += 1; } + } else { + removed = nextKLinesText(L); + curSplice[1] += L; } return removed; }; + /** + * Remove text from lines array. + * + * @param {number} N - characters to delete + * @param {number} L - lines to delete + * @returns {string} + */ const remove = (N, L) => { - let removed = ''; - if (N) { - if (L) { - return removeLines(L); - } else { - if (!inSplice) { - enterSplice(); - } - const sline = putCurLineInSplice(); - removed = curSplice[sline].substring(curCol, curCol + N); - curSplice[sline] = curSplice[sline].substring(0, curCol) + - curSplice[sline].substring(curCol + N); - } - } + if (!N) return ''; + if (L) return removeLines(L); + if (!inSplice) enterSplice(); + // although the line is put into splice, curLine is not increased, because + // only some chars are removed not the whole line + const sline = putCurLineInSplice(); + const removed = curSplice[sline].substring(curCol, curCol + N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + curSplice[sline].substring(curCol + N); return removed; }; + /** + * Inserts text into lines array. + * + * @param {string} text - the text to insert + * @param {number} L - number of newlines in text + */ const insert = (text, L) => { - if (text) { - if (!inSplice) { - enterSplice(); - } - if (L) { - const newLines = exports.splitTextLines(text); - if (isCurLineInSplice()) { - const sline = curSplice.length - 1; - const theLine = curSplice[sline]; - const lineCol = curCol; - curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - curLine++; - newLines.splice(0, 1); - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - curSplice.push(theLine.substring(lineCol)); - curCol = 0; - } else { - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - } + if (!text) return; + if (!inSplice) enterSplice(); + if (L) { + const newLines = exports.splitTextLines(text); + if (isCurLineInSplice()) { + const sline = curSplice.length - 1; + /** @type {string} */ + const theLine = curSplice[sline]; + const lineCol = curCol; + // insert the first new line + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + // insert the remaining new lines + curSplice.push(...newLines); + curLine += newLines.length; + // insert the remaining chars from the "old" line (e.g. the line we were in + // when we started to insert new lines) + curSplice.push(theLine.substring(lineCol)); + curCol = 0; // TODO(doc) why is this not set to the length of last line? } else { - const sline = putCurLineInSplice(); - if (!curSplice[sline]) { - console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); - } - curSplice[sline] = curSplice[sline].substring(0, curCol) + text + - curSplice[sline].substring(curCol); - curCol += text.length; + curSplice.push(...newLines); + curLine += newLines.length; + } + } else { + // there are no additional lines + // although the line is put into splice, curLine is not increased, because + // there may be more chars in the line (newline is not reached) + const sline = putCurLineInSplice(); + if (!curSplice[sline]) { + const err = new Error( + 'curSplice[sline] not populated, actual curSplice contents is ' + + `${JSON.stringify(curSplice)}. Possibly related to ` + + 'https://github.com/ether/etherpad-lite/issues/2802'); + console.error(err.stack || err.toString()); } + curSplice[sline] = curSplice[sline].substring(0, curCol) + text + + curSplice[sline].substring(curCol); + 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 + */ const hasMore = () => { - let docLines = lines_length(); + let docLines = linesLength(); if (inSplice) { docLines += curSplice.length - 2 - curSplice[1]; } return curLine < docLines; }; + /** + * Closes the splice + */ const close = () => { if (inSplice) { leaveSplice(); @@ -776,50 +953,53 @@ exports.textLinesMutator = (lines) => { }; /** - * Function allowing iterating over two Op strings. - * @params in1 {string} first Op string - * @params idx1 {int} integer where 1st iterator should start - * @params in2 {string} second Op string - * @params idx2 {int} integer where 2nd iterator should start - * @params func {function} which decides how 1st or 2nd iterator - * advances. When opX.opcode = 0, iterator X advances to - * next element - * func has signature f(op1, op2, opOut) - * op1 - current operation of the first iterator - * op2 - current operation of the second iterator - * opOut - result operator to be put into Changeset - * @return {string} the integrated changeset - */ -exports.applyZip = (in1, idx1, in2, idx2, func) => { - const iter1 = exports.opIterator(in1, idx1); - const iter2 = exports.opIterator(in2, idx2); + * 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 iter1 = exports.opIterator(in1); + const iter2 = exports.opIterator(in2); const assem = exports.smartOpAssembler(); const op1 = exports.newOp(); const op2 = exports.newOp(); - const opOut = exports.newOp(); while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); - func(op1, op2, opOut); - if (opOut.opcode) { - assem.append(opOut); - opOut.opcode = ''; - } + const opOut = func(op1, op2); + if (opOut && opOut.opcode) assem.append(opOut); } assem.endDocument(); return assem.toString(); }; /** - * Unpacks a string encoded Changeset into a proper Changeset object - * @params cs {string} String encoded Changeset - * @returns {Changeset} a Changeset class + * Parses an encoded changeset. + * + * @param {string} cs - The encoded changeset. + * @returns {Changeset} */ exports.unpack = (cs) => { const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; const headerMatch = headerRegex.exec(cs); if ((!headerMatch) || (!headerMatch[0])) { - exports.error(`Not a exports: ${cs}`); + error(`Not a exports: ${cs}`); } const oldLen = exports.parseNum(headerMatch[1]); const changeSign = (headerMatch[2] === '>') ? 1 : -1; @@ -837,12 +1017,13 @@ exports.unpack = (cs) => { }; /** - * Packs Changeset object into a string - * @params oldLen {int} Old length of the Changeset - * @params newLen {int] New length of the Changeset - * @params opsStr {string} String encoding of the changes to be made - * @params bank {string} Charbank of the Changeset - * @returns {Changeset} a Changeset class + * Creates an encoded changeset. + * + * @param {number} oldLen - The length of the document before applying the changeset. + * @param {number} newLen - The length of the document after applying the changeset. + * @param {string} opsStr - Encoded operations to apply to the document. + * @param {string} bank - Characters for insert operations. + * @returns {string} The encoded changeset. */ exports.pack = (oldLen, newLen, opsStr, bank) => { const lenDiff = newLen - oldLen; @@ -854,14 +1035,15 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { }; /** - * Applies a Changeset to a string - * @params cs {string} String encoded Changeset - * @params str {string} String to which a Changeset should be applied + * Applies a Changeset to a string. + * + * @param {string} cs - String encoded Changeset + * @param {string} str - String to which a Changeset should be applied + * @returns {string} */ exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); - exports.assert(str.length === unpacked.oldLen, 'mismatched apply: ', str.length, - ' / ', unpacked.oldLen); + assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const strIter = exports.stringIterator(str); @@ -900,15 +1082,16 @@ exports.applyToText = (cs, str) => { }; /** - * applies a changeset on an array of lines - * @param CS {Changeset} the changeset to be applied - * @param lines The lines to which the changeset needs to be applied + * Applies a changeset on an array of lines. + * + * @param {string} cs - the changeset to apply + * @param {string[]} lines - The lines to which the changeset needs to be applied */ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); - const mut = exports.textLinesMutator(lines); + const mut = textLinesMutator(lines); while (csIter.hasNext()) { const op = csIter.next(); switch (op.opcode) { @@ -926,12 +1109,23 @@ exports.mutateTextLines = (cs, lines) => { mut.close(); }; +/** + * Sorts an array of attributes by key. + * + * @param {Attribute[]} attribs - The array of attributes to sort in place. + * @returns {Attribute[]} The `attribs` array. + */ +const sortAttribs = + (attribs) => attribs.sort((a, b) => (a[0] > b[0] ? 1 : 0) - (a[0] < b[0] ? 1 : 0)); + /** * Composes two attribute strings (see below) into one. - * @param att1 {string} first attribute string - * @param att2 {string} second attribue string - * @param resultIsMutaton {boolean} - * @param pool {AttribPool} attribute pool + * + * @param {string} att1 - first attribute string + * @param {string} att2 - second attribue string + * @param {boolean} resultIsMutation - + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. @@ -955,142 +1149,102 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { return att2; } if (!att2) return att1; - const atts = []; + const atts = new Map(); att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - atts.push(pool.getAttrib(exports.parseNum(a))); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + atts.set(key, val); return ''; }); att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - const pair = pool.getAttrib(exports.parseNum(a)); - let found = false; - for (let i = 0; i < atts.length; i++) { - const oldPair = atts[i]; - if (oldPair[0] === pair[0]) { - if (pair[1] || resultIsMutation) { - oldPair[1] = pair[1]; - } else { - atts.splice(i, 1); - } - found = true; - break; - } - } - if ((!found) && (pair[1] || resultIsMutation)) { - atts.push(pair); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + if (val || resultIsMutation) { + atts.set(key, val); + } else { + atts.delete(key); } return ''; }); - atts.sort(); const buf = exports.stringAssembler(); - for (let i = 0; i < atts.length; i++) { + for (const att of sortAttribs([...atts])) { buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(atts[i]))); + buf.append(exports.numToString(pool.putAttrib(att))); } return buf.toString(); }; /** - * Function used as parameter for applyZip to apply a Changeset to an - * attribute + * Function used as parameter for applyZip to apply a Changeset to an attribute. + * + * @param {Op} attOp - The op from the sequence that is being operated on, either an attribution + * string or the earlier of two exportss being composed. + * @param {Op} csOp - + * @param {AttributePool} pool - Can be null if definitely not needed. + * @returns {Op} The result of applying `csOp` to `attOp`. */ -exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two exportss being composed. - // pool can be null if definitely not needed. - if (attOp.opcode === '-') { - exports.copyOp(attOp, opOut); +const slicerZipperFunc = (attOp, csOp, pool) => { + const opOut = exports.newOp(); + 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 (!attOp.opcode) { - exports.copyOp(csOp, opOut); + } else if (csOp.opcode === '+') { + copyOp(csOp, opOut); csOp.opcode = ''; } else { - switch (csOp.opcode) { - case '-': - { - if (csOp.chars <= attOp.chars) { - // delete or delete part - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = ''; - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // delete and keep going - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = ''; - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - exports.copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - exports.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } + 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 cs {string} Changeset - * @param astr {string} the attribs string of a AText - * @param pool {AttribsPool} the attibutes pool + * + * @param {string} cs - Changeset + * @param {string} astr - the attribs string of a AText + * @param {AttributePool} pool - the attibutes pool + * @returns {string} */ exports.applyToAttribution = (cs, astr, pool) => { const unpacked = exports.unpack(cs); - - return exports.applyZip(astr, 0, unpacked.ops, 0, - (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); + return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool)); }; exports.mutateAttributionLines = (cs, lines, pool) => { @@ -1099,22 +1253,20 @@ exports.mutateAttributionLines = (cs, lines, pool) => { const csBank = unpacked.charBank; let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time - const mut = exports.textLinesMutator(lines); + const mut = textLinesMutator(lines); + /** @type {?OpIter} */ let lineIter = null; const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); - const nextMutOp = (destOp) => { + const nextMutOp = () => { if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { const line = mut.removeLines(1); lineIter = exports.opIterator(line); } - if (lineIter && lineIter.hasNext()) { - lineIter.next(destOp); - } else { - destOp.opcode = ''; - } + if (!lineIter || !lineIter.hasNext()) return exports.newOp(); + return lineIter.next(); }; let lineAssem = null; @@ -1123,21 +1275,17 @@ exports.mutateAttributionLines = (cs, lines, pool) => { lineAssem = exports.mergingOpAssembler(); } lineAssem.append(op); - if (op.lines > 0) { - exports.assert(op.lines === 1, "Can't have op.lines of ", op.lines, ' in attribution lines'); - // ship it to the mut - mut.insert(lineAssem.toString(), 1); - lineAssem = null; - } + if (op.lines <= 0) return; + assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; }; - const csOp = exports.newOp(); - const attOp = exports.newOp(); - const opOut = exports.newOp(); + let csOp = exports.newOp(); + let attOp = exports.newOp(); while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { - if ((!csOp.opcode) && csIter.hasNext()) { - csIter.next(csOp); - } + if (!csOp.opcode && csIter.hasNext()) csOp = csIter.next(); if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { break; // done } else if (csOp.opcode === '=' && csOp.lines > 0 && (!csOp.attribs) && @@ -1146,45 +1294,38 @@ exports.mutateAttributionLines = (cs, lines, pool) => { mut.skipLines(csOp.lines); csOp.opcode = ''; } else if (csOp.opcode === '+') { + const opOut = copyOp(csOp); if (csOp.lines > 1) { const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - exports.copyOp(csOp, opOut); csOp.chars -= firstLineLen; csOp.lines--; opOut.lines = 1; opOut.chars = firstLineLen; } else { - exports.copyOp(csOp, opOut); csOp.opcode = ''; } outputMutOp(opOut); csBankIndex += opOut.chars; - opOut.opcode = ''; } else { - if ((!attOp.opcode) && isNextMutOp()) { - nextMutOp(attOp); - } - exports._slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) { - outputMutOp(opOut); - opOut.opcode = ''; - } + if (!attOp.opcode && isNextMutOp()) attOp = nextMutOp(); + const opOut = slicerZipperFunc(attOp, csOp, pool); + if (opOut.opcode) outputMutOp(opOut); } } - exports.assert(!lineAssem, `line assembler not finished:${cs}`); + assert(!lineAssem, `line assembler not finished:${cs}`); mut.close(); }; /** - * joins several Attribution lines - * @param theAlines collection of Attribution lines + * Joins several Attribution lines. + * + * @param {string[]} theAlines - collection of Attribution lines * @returns {string} joined Attribution lines */ exports.joinAttributionLines = (theAlines) => { const assem = exports.mergingOpAssembler(); - for (let i = 0; i < theAlines.length; i++) { - const aline = theAlines[i]; + for (const aline of theAlines) { const iter = exports.opIterator(aline); while (iter.hasNext()) { assem.append(iter.next()); @@ -1214,7 +1355,7 @@ exports.splitAttributionLines = (attrOps, text) => { let numLines = op.lines; while (numLines > 1) { const newlineEnd = text.indexOf('\n', pos) + 1; - exports.assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); + assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); op.chars = newlineEnd - pos; op.lines = 1; appendOp(op); @@ -1232,35 +1373,39 @@ exports.splitAttributionLines = (attrOps, text) => { }; /** - * splits text into lines - * @param {string} text to be splitted + * Splits text into lines. + * + * @param {string} text - text to split + * @returns {string[]} */ exports.splitTextLines = (text) => text.match(/[^\n]*(?:\n|[^\n]$)/g); /** - * compose two Changesets - * @param cs1 {Changeset} first Changeset - * @param cs2 {Changeset} second Changeset - * @param pool {AtribsPool} Attribs pool + * Compose two Changesets. + * + * @param {string} cs1 - first Changeset + * @param {string} cs2 - second Changeset + * @param {AttributePool} pool - Attribs pool + * @returns {string} */ exports.compose = (cs1, cs2, pool) => { const unpacked1 = exports.unpack(cs1); const unpacked2 = exports.unpack(cs2); const len1 = unpacked1.oldLen; const len2 = unpacked1.newLen; - exports.assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); + assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); const len3 = unpacked2.newLen; const bankIter1 = exports.stringIterator(unpacked1.charBank); const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } - exports._slicerZipperFunc(op1, op2, opOut, pool); + const opOut = slicerZipperFunc(op1, op2, pool); if (opOut.opcode === '+') { if (op2code === '+') { bankAssem.append(bankIter2.take(opOut.chars)); @@ -1268,50 +1413,49 @@ exports.compose = (cs1, cs2, pool) => { 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 attribPair array [key,value] of the attribute - * @param pool {AttribPool} Attribute pool + * Returns a function that tests if a string of attributes (e.g. '*3*4') contains a given attribute + * key,value that is already present in the pool. + * + * @param {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; - } + if (!pool) return never; const attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) { - return never; - } else { - const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); - return (attribs) => re.test(attribs); - } + if (attribNum < 0) return never; + const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); + return (attribs) => re.test(attribs); }; /** - * creates the identity Changeset of length N - * @param N {int} length of the identity changeset + * Creates the identity Changeset of length N. + * + * @param {number} N - length of the identity changeset + * @returns {string} */ exports.identity = (N) => exports.pack(N, N, '', ''); - /** - * creates a Changeset which works on oldFullText and removes text - * from spliceStart to spliceStart+numRemoved and inserts newText - * instead. Also gives possibility to add attributes optNewTextAPairs - * for the new text. - * @param oldFullText {string} old text - * @param spliecStart {int} where splicing starts - * @param numRemoved {int} number of characters to be removed - * @param newText {string} string to be inserted - * @param optNewTextAPairs {string} new pairs to be inserted - * @param pool {AttribPool} Attribution Pool + * Creates a Changeset which works on oldFullText and removes text from spliceStart to + * spliceStart+numRemoved and inserts newText instead. Also gives possibility to add attributes + * optNewTextAPairs for the new text. + * + * @param {string} oldFullText - old text + * @param {number} spliceStart - where splicing starts + * @param {number} numRemoved - number of characters to remove + * @param {string} newText - string to insert + * @param {string} optNewTextAPairs - new pairs to insert + * @param {AttributePool} pool - Attribute pool + * @returns {string} */ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) => { const oldLen = oldFullText.length; @@ -1326,22 +1470,26 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA const newLen = oldLen + newText.length - oldText.length; const assem = exports.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + const ops = (function* () { + yield* opsFromText('=', oldFullText.substring(0, spliceStart)); + yield* opsFromText('-', oldText); + yield* opsFromText('+', newText, optNewTextAPairs, pool); + })(); + for (const op of ops) assem.append(op); assem.endDocument(); return exports.pack(oldLen, newLen, assem.toString(), newText); }; /** - * Transforms a changeset into a list of splices in the form - * [startChar, endChar, newText] meaning replace text from - * startChar to endChar with newText - * @param cs Changeset + * Transforms a changeset into a list of splices in the form [startChar, endChar, newText] meaning + * replace text from startChar to endChar with newText. + * + * @param {string} cs - Changeset + * @returns {[number, number, string][]} */ -exports.toSplices = (cs) => { - // +const toSplices = (cs) => { const unpacked = exports.unpack(cs); + /** @type {[number, number, string][]} */ const splices = []; let oldPos = 0; @@ -1371,15 +1519,17 @@ exports.toSplices = (cs) => { }; /** - * + * @param {string} cs - + * @param {number} startChar - + * @param {number} endChar - + * @param {number} insertionsAfter - + * @returns {[number, number]} */ exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { let newStartChar = startChar; let newEndChar = endChar; - const splices = exports.toSplices(cs); let lengthChangeSoFar = 0; - for (let i = 0; i < splices.length; i++) { - const splice = splices[i]; + for (const splice of toSplices(cs)) { const spliceStart = splice[0] + lengthChangeSoFar; const spliceEnd = splice[1] + lengthChangeSoFar; const newTextLength = splice[2].length; @@ -1418,12 +1568,12 @@ exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { }; /** - * Iterate over attributes in a changeset and move them from - * oldPool to newPool - * @param cs {Changeset} Chageset/attribution string to be iterated over - * @param oldPool {AttribPool} old attributes pool - * @param newPool {AttribPool} new attributes pool - * @return {string} the new Changeset + * Iterate over attributes in a changeset and move them from oldPool to newPool. + * + * @param {string} cs - Chageset/attribution string to iterate over + * @param {AttributePool} oldPool - old attributes pool + * @param {AttributePool} newPool - new attributes pool + * @returns {string} the new Changeset */ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { // works on exports or attribution string @@ -1436,38 +1586,33 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { // order of attribs stays the same return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { const oldNum = exports.parseNum(a); - let pair = oldPool.getAttrib(oldNum); - - /* - * Setting an empty pair. Required for when delete pad contents / attributes - * while another user has the timeslider open. - * - * Fixes https://github.com/ether/etherpad-lite/issues/3932 - */ - if (!pair) { - pair = []; - } - + const pair = oldPool.getAttrib(oldNum); + // The attribute might not be in the old pool if the user is viewing the current revision in the + // timeslider and text is deleted. See: https://github.com/ether/etherpad-lite/issues/3932 + if (!pair) return ''; const newNum = newPool.putAttrib(pair); return `*${exports.numToString(newNum)}`; }) + fromDollar; }; /** - * create an attribution inserting a text - * @param text {string} text to be inserted + * Create an attribution inserting a text. + * + * @param {string} text - text to insert + * @returns {string} */ exports.makeAttribution = (text) => { const assem = exports.smartOpAssembler(); - assem.appendOpWithText('+', text); + 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 - * @param cs {Changeset} changeset - * @param func {function} function to be called + * Iterates over attributes in exports, attribution string, or attribs property of an op and runs + * function func on them. + * + * @param {string} cs - changeset + * @param {Function} func - function to call */ exports.eachAttribNumber = (cs, func) => { let dollarPos = cs.indexOf('$'); @@ -1483,17 +1628,22 @@ exports.eachAttribNumber = (cs, func) => { }; /** - * Filter attributes which should remain in a Changeset - * callable on a exports, attribution string, or attribs property of an op, - * though it may easily create adjacent ops that can be merged. - * @param cs {Changeset} changeset to be filtered - * @param filter {function} fnc which returns true if an - * attribute X (int) should be kept in the Changeset + * Filter attributes which should remain in a Changeset. Callable on a exports, attribution string, + * or attribs property of an op, though it may easily create adjacent ops that can be merged. + * + * @param {string} cs - changeset to filter + * @param {Function} filter - fnc which returns true if an attribute X (int) should be kept in the + * Changeset + * @returns {string} */ exports.filterAttribNumbers = (cs, filter) => exports.mapAttribNumbers(cs, filter); /** - * does exactly the same as exports.filterAttribNumbers + * Does exactly the same as exports.filterAttribNumbers. + * + * @param {string} cs - + * @param {Function} func - + * @returns {string} */ exports.mapAttribNumbers = (cs, func) => { let dollarPos = cs.indexOf('$'); @@ -1517,10 +1667,21 @@ exports.mapAttribNumbers = (cs, func) => { }; /** - * Create a Changeset going from Identity to a certain state - * @params text {string} text of the final change - * @attribs attribs {string} optional, operations which insert - * the text and also puts the right attributes + * Represents text with attributes. + * + * @typedef {object} AText + * @property {string} attribs - Serialized sequence of insert operations that cover the text in + * `text`. These operations describe which parts of the text have what attributes. + * @property {string} text - The text. + */ + +/** + * Create a Changeset going from Identity to a certain state. + * + * @param {string} text - text of the final change + * @param {string} attribs - optional, operations which insert the text and also puts the right + * attributes + * @returns {AText} */ exports.makeAText = (text, attribs) => ({ text, @@ -1528,10 +1689,12 @@ exports.makeAText = (text, attribs) => ({ }); /** - * Apply a Changeset to a AText - * @param cs {Changeset} Changeset to be applied - * @param atext {AText} - * @param pool {AttribPool} Attribute Pool to add to + * Apply a Changeset to a AText. + * + * @param {string} cs - Changeset to apply + * @param {AText} atext - + * @param {AttributePool} pool - Attribute Pool to add to + * @returns {AText} */ exports.applyToAText = (cs, atext, pool) => ({ text: exports.applyToText(cs, atext.text), @@ -1539,21 +1702,24 @@ exports.applyToAText = (cs, atext, pool) => ({ }); /** - * Clones a AText structure - * @param atext {AText} + * Clones a AText structure. + * + * @param {AText} atext - + * @returns {AText} */ exports.cloneAText = (atext) => { - if (atext) { - return { - text: atext.text, - attribs: atext.attribs, - }; - } else { exports.error('atext is null'); } + if (!atext) error('atext is null'); + return { + text: atext.text, + attribs: atext.attribs, + }; }; /** - * Copies a AText structure from atext1 to atext2 - * @param atext {AText} + * Copies a AText structure from atext1 to atext2. + * + * @param {AText} atext1 - + * @param {AText} atext2 - */ exports.copyAText = (atext1, atext2) => { atext2.text = atext1.text; @@ -1561,47 +1727,42 @@ exports.copyAText = (atext1, atext2) => { }; /** - * Append the set of operations from atext to an assembler - * @param atext {AText} - * @param assem Assembler like smartOpAssembler + * Append the set of operations from atext to an assembler. + * + * @param {AText} atext - + * @param assem - Assembler like SmartOpAssembler TODO add desc */ exports.appendATextToAssembler = (atext, assem) => { // intentionally skips last newline char of atext const iter = exports.opIterator(atext.attribs); - const op = exports.newOp(); + let lastOp = null; while (iter.hasNext()) { - iter.next(op); - if (!iter.hasNext()) { - // last op, exclude final newline - if (op.lines <= 1) { - op.lines = 0; - op.chars--; - if (op.chars) { - assem.append(op); - } - } else { - const nextToLastNewlineEnd = - atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; - op.lines--; - op.chars -= (lastLineLength + 1); - assem.append(op); - op.lines = 0; - op.chars = lastLineLength; - if (op.chars) { - assem.append(op); - } - } - } else { - assem.append(op); - } + if (lastOp != null) assem.append(lastOp); + lastOp = iter.next(); + } + if (lastOp == null) return; + // exclude final newline + if (lastOp.lines <= 1) { + lastOp.lines = 0; + lastOp.chars--; + } else { + const nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; + const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + lastOp.lines--; + lastOp.chars -= (lastLineLength + 1); + assem.append(lastOp); + lastOp.lines = 0; + lastOp.chars = lastLineLength; } + if (lastOp.chars) assem.append(lastOp); }; /** - * Creates a clone of a Changeset and it's APool - * @param cs {Changeset} - * @param pool {AtributePool} + * Creates a clone of a Changeset and it's APool. + * + * @param {string} cs - + * @param {AttributePool} pool - + * @returns {{translated: string, pool: AttributePool}} */ exports.prepareForWire = (cs, pool) => { const newPool = new AttributePool(); @@ -1613,7 +1774,10 @@ exports.prepareForWire = (cs, pool) => { }; /** - * Checks if a changeset s the identity changeset + * Checks if a changeset s the identity changeset. + * + * @param {string} cs - + * @returns {boolean} */ exports.isIdentity = (cs) => { const unpacked = exports.unpack(cs); @@ -1621,37 +1785,48 @@ exports.isIdentity = (cs) => { }; /** - * returns all the values of attributes with a certain key - * in an Op attribs string - * @param attribs {string} Attribute string of a Op - * @param key {string} string to be seached for - * @param pool {AttribPool} attribute pool + * Returns all the values of attributes with a certain key in an Op attribs string. + * + * @param {Op} op - Op + * @param {string} key - string to search for + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.opAttributeValue = (op, key, pool) => exports.attribsAttributeValue(op.attribs, key, pool); /** - * returns all the values of attributes with a certain key - * in an attribs string - * @param attribs {string} Attribute string - * @param key {string} string to be seached for - * @param pool {AttribPool} attribute pool + * Returns all the values of attributes with a certain key in an attribs string. + * + * @param {string} attribs - Attribute string + * @param {string} key - string to search for + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.attribsAttributeValue = (attribs, key, pool) => { + if (!attribs) return ''; let value = ''; - if (attribs) { - exports.eachAttribNumber(attribs, (n) => { - if (pool.getAttribKey(n) === key) { - value = pool.getAttribValue(n); - } - }); - } + exports.eachAttribNumber(attribs, (n) => { + if (pool.getAttribKey(n) === key) { + value = pool.getAttribValue(n); + } + }); return value; }; /** - * Creates a Changeset builder for a string with initial - * length oldLen. Allows to add/remove parts of it - * @param oldLen {int} Old length + * Incrementally builds a Changeset. + * + * @typedef {object} Builder + * @property {Function} insert - + * @property {Function} keep - + * @property {Function} keepText - + * @property {Function} remove - + * @property {Function} toString - + */ + +/** + * @param {number} oldLen - Old length + * @returns {Builder} */ exports.builder = (oldLen) => { const assem = exports.smartOpAssembler(); @@ -1659,7 +1834,16 @@ exports.builder = (oldLen) => { const charBank = exports.stringAssembler(); const self = { - // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + /** + * @param {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 = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; @@ -1668,15 +1852,40 @@ exports.builder = (oldLen) => { assem.append(o); return self; }, + + /** + * @param {string} text - Text to keep. + * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' + * (no pool needed in latter case). + * @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of + * attribute key, value pairs. + * @returns {Builder} this + */ keepText: (text, attribs, pool) => { - assem.appendOpWithText('=', text, attribs, pool); + 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) => { - assem.appendOpWithText('+', 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 = ''; @@ -1685,6 +1894,7 @@ exports.builder = (oldLen) => { assem.append(o); return self; }, + toString: () => { assem.endDocument(); const newLen = oldLen + assem.getLengthChange(); @@ -1704,11 +1914,10 @@ exports.makeAttribsString = (opcode, attribs, pool) => { } else if (pool && attribs.length) { if (attribs.length > 1) { attribs = attribs.slice(); - attribs.sort(); + sortAttribs(attribs); } const result = []; - for (let i = 0; i < attribs.length; i++) { - const pair = attribs[i]; + for (const pair of attribs) { if (opcode === '=' || (opcode === '+' && pair[1])) { result.push(`*${exports.numToString(pool.putAttrib(pair))}`); } @@ -1717,30 +1926,25 @@ exports.makeAttribsString = (opcode, attribs, pool) => { } }; -// like "substring" but on a single-line attribution string +/** + * Like "substring" but on a single-line attribution string. + */ exports.subattribution = (astr, start, optEnd) => { - const iter = exports.opIterator(astr, 0); + const iter = exports.opIterator(astr); const assem = exports.smartOpAssembler(); - const attOp = exports.newOp(); + let attOp = exports.newOp(); const csOp = exports.newOp(); - const opOut = exports.newOp(); const doCsOp = () => { - if (csOp.chars) { - while (csOp.opcode && (attOp.opcode || iter.hasNext())) { - if (!attOp.opcode) iter.next(attOp); - - if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && - attOp.lines > 0 && csOp.lines <= 0) { - csOp.lines++; - } - - exports._slicerZipperFunc(attOp, csOp, opOut, null); - if (opOut.opcode) { - assem.append(opOut); - opOut.opcode = ''; - } + if (!csOp.chars) return; + while (csOp.opcode && (attOp.opcode || iter.hasNext())) { + if (!attOp.opcode) attOp = iter.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); } }; @@ -1753,10 +1957,7 @@ exports.subattribution = (astr, start, optEnd) => { if (attOp.opcode) { assem.append(attOp); } - while (iter.hasNext()) { - iter.next(attOp); - assem.append(attOp); - } + while (iter.hasNext()) assem.append(iter.next()); } else { csOp.opcode = '='; csOp.chars = optEnd - start; @@ -1771,7 +1972,7 @@ exports.inverse = (cs, lines, alines, pool) => { // They may be arrays or objects with .get(i) and .length methods. // They include final newlines on lines. - const lines_get = (idx) => { + const linesGet = (idx) => { if (lines.get) { return lines.get(idx); } else { @@ -1779,7 +1980,11 @@ exports.inverse = (cs, lines, alines, pool) => { } }; - const alines_get = (idx) => { + /** + * @param {number} idx - + * @returns {string} + */ + const alinesGet = (idx) => { if (alines.get) { return alines.get(idx); } else { @@ -1791,7 +1996,7 @@ exports.inverse = (cs, lines, alines, pool) => { let curChar = 0; let curLineOpIter = null; let curLineOpIterLine; - const curLineNextOp = exports.newOp('+'); + let curLineNextOp = exports.newOp('+'); const unpacked = exports.unpack(cs); const csIter = exports.opIterator(unpacked.ops); @@ -1800,18 +2005,16 @@ exports.inverse = (cs, lines, alines, pool) => { const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { // create curLineOpIter and advance it to curChar - curLineOpIter = exports.opIterator(alines_get(curLine)); + curLineOpIter = exports.opIterator(alinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; - let done = false; - while (!done && curLineOpIter.hasNext()) { - curLineOpIter.next(curLineNextOp); + while (curLineOpIter.hasNext()) { + curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } else { - indexIntoLine += curLineNextOp.chars; + break; } + indexIntoLine += curLineNextOp.chars; } } @@ -1821,10 +2024,10 @@ exports.inverse = (cs, lines, alines, pool) => { curChar = 0; curLineOpIterLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = exports.opIterator(alines_get(curLine)); + curLineOpIter = exports.opIterator(alinesGet(curLine)); } if (!curLineNextOp.chars) { - curLineOpIter.next(curLineNextOp); + curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : exports.newOp(); } const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && @@ -1854,13 +2057,13 @@ exports.inverse = (cs, lines, alines, pool) => { const nextText = (numChars) => { let len = 0; const assem = exports.stringAssembler(); - const firstString = lines_get(curLine).substring(curChar); + const firstString = linesGet(curLine).substring(curChar); len += firstString.length; assem.append(firstString); let lineNum = curLine + 1; while (len < numChars) { - const nextString = lines_get(lineNum); + const nextString = linesGet(lineNum); len += nextString.length; assem.append(nextString); lineNum++; @@ -1879,23 +2082,15 @@ exports.inverse = (cs, lines, alines, pool) => { }; }; - const attribKeys = []; - const attribValues = []; while (csIter.hasNext()) { const csOp = csIter.next(); if (csOp.opcode === '=') { if (csOp.attribs) { - attribKeys.length = 0; - attribValues.length = 0; - exports.eachAttribNumber(csOp.attribs, (n) => { - attribKeys.push(pool.getAttribKey(n)); - attribValues.push(pool.getAttribValue(n)); - }); + const csAttribs = []; + exports.eachAttribNumber(csOp.attribs, (n) => csAttribs.push(pool.getAttrib(n))); const undoBackToAttribs = cachedStrFunc((attribs) => { const backAttribs = []; - for (let i = 0; i < attribKeys.length; i++) { - const appliedKey = attribKeys[i]; - const appliedValue = attribValues[i]; + for (const [appliedKey, appliedValue] of csAttribs) { const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); if (appliedValue !== oldValue) { backAttribs.push([appliedKey, oldValue]); @@ -1931,7 +2126,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const unpacked2 = exports.unpack(cs2); const len1 = unpacked1.oldLen; const len2 = unpacked2.oldLen; - exports.assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); + assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); const chars1 = exports.stringIterator(unpacked1.charBank); const chars2 = exports.stringIterator(unpacked2.charBank); @@ -1941,7 +2136,8 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { + const opOut = exports.newOp(); if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { @@ -1980,7 +2176,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { } else { // whichToDo == 2 chars2.skip(op2.chars); - exports.copyOp(op2, opOut); + copyOp(op2, opOut); op2.opcode = ''; } } else if (op1.opcode === '-') { @@ -1999,7 +2195,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { op2.opcode = ''; } } else if (op2.opcode === '-') { - exports.copyOp(op2, opOut); + copyOp(op2, opOut); if (!op1.opcode) { op2.opcode = ''; } else if (op2.chars <= op1.chars) { @@ -2019,17 +2215,17 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { op1.opcode = ''; } } else if (!op1.opcode) { - exports.copyOp(op2, opOut); + copyOp(op2, opOut); op2.opcode = ''; } else if (!op2.opcode) { // @NOTE: Critical bugfix for EPL issue #1625. We do not copy op1 here // in order to prevent attributes from leaking into result changesets. - // exports.copyOp(op1, opOut); + // copyOp(op1, opOut); op1.opcode = ''; } else { // both keeps opOut.opcode = '='; - opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool); + opOut.attribs = followAttributes(op1.attribs, op2.attribs, pool); if (op1.chars <= op2.chars) { opOut.chars = op1.chars; opOut.lines = op1.lines; @@ -2059,13 +2255,14 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { newLen += opOut.chars; break; } + return opOut; }); newLen += oldLen - oldPos; return exports.pack(oldLen, newLen, newOps, unpacked2.charBank); }; -exports.followAttributes = (att1, att2, pool) => { +const followAttributes = (att1, att2, pool) => { // The merge of two sets of attribute changes to the same text // takes the lexically-earlier value if there are two values // for the same key. Otherwise, all key/value changes from @@ -2074,151 +2271,28 @@ exports.followAttributes = (att1, att2, pool) => { // to produce the merged set. if ((!att2) || (!pool)) return ''; if (!att1) return att2; - const atts = []; + const atts = new Map(); att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - atts.push(pool.getAttrib(exports.parseNum(a))); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + atts.set(key, val); return ''; }); att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - const pair1 = pool.getAttrib(exports.parseNum(a)); - for (let i = 0; i < atts.length; i++) { - const pair2 = atts[i]; - if (pair1[0] === pair2[0]) { - if (pair1[1] <= pair2[1]) { - // winner of merge is pair1, delete this attribute - atts.splice(i, 1); - } - break; - } - } + const [key, val] = pool.getAttrib(exports.parseNum(a)); + if (atts.has(key) && val <= atts.get(key)) atts.delete(key); return ''; }); // we've only removed attributes, so they're already sorted const buf = exports.stringAssembler(); - for (let i = 0; i < atts.length; i++) { + for (const att of atts) { buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(atts[i]))); + buf.append(exports.numToString(pool.putAttrib(att))); } return buf.toString(); }; -exports.composeWithDeletions = (cs1, cs2, pool) => { - const unpacked1 = exports.unpack(cs1); - const unpacked2 = exports.unpack(cs2); - const len1 = unpacked1.oldLen; - const len2 = unpacked1.newLen; - exports.assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); - const len3 = unpacked2.newLen; - const bankIter1 = exports.stringIterator(unpacked1.charBank); - const bankIter2 = exports.stringIterator(unpacked2.charBank); - const bankAssem = exports.stringAssembler(); - - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { - const op1code = op1.opcode; - const op2code = op2.opcode; - if (op1code === '+' && op2code === '-') { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool); - if (opOut.opcode === '+') { - if (op2code === '+') { - bankAssem.append(bankIter2.take(opOut.chars)); - } else { - bankAssem.append(bankIter1.take(opOut.chars)); - } - } - }); - - return exports.pack(len1, len3, newOps, bankAssem.toString()); -}; - -// This function is 95% like _slicerZipperFunc, we just changed two lines to -// ensure it merges the attribs of deletions properly. -// This is necassary for correct paddiff. But to ensure these changes doesn't -// affect anything else, we've created a seperate function only used for paddiffs -exports._slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two exportss being composed. - // pool can be null if definitely not needed. - if (attOp.opcode === '-') { - exports.copyOp(attOp, opOut); - attOp.opcode = ''; - } else if (!attOp.opcode) { - exports.copyOp(csOp, opOut); - csOp.opcode = ''; - } else { - switch (csOp.opcode) { - case '-': - { - if (csOp.chars <= attOp.chars) { - // delete or delete part - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = csOp.attribs; // changed by yammer - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // delete and keep going - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = csOp.attribs; // changed by yammer - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - exports.copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - exports.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } +exports.exportedForTestingOnly = { + followAttributes, + textLinesMutator, + toSplices, }; diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.js new file mode 100644 index 00000000000..b4658575e00 --- /dev/null +++ b/src/static/js/ChatMessage.js @@ -0,0 +1,80 @@ +'use strict'; + +/** + * Represents a chat message stored in the database and transmitted among users. Plugins can extend + * the object with additional properties. + * + * Supports serialization to JSON. + */ +class ChatMessage { + static fromObject(obj) { + return Object.assign(new ChatMessage(), obj); + } + + /** + * @param {?string} [text] - Initial value of the `text` property. + * @param {?string} [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() { return this.authorId; } + set userId(val) { this.authorId = val; } + + /** + * Alias of `displayName`, for compatibility with old plugins. + * + * @deprecated Use `displayName` instead. + * @type {string} + */ + get userName() { return this.displayName; } + set userName(val) { this.displayName = val; } + + // TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that + // doesn't support authorId and displayName. + toJSON() { + return { + ...this, + authorId: undefined, + displayName: undefined, + userId: this.authorId, + userName: this.displayName, + }; + } +} + +module.exports = ChatMessage; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 3471f442129..b0a0425702e 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -110,7 +110,6 @@ const Ace2Editor = function () { 'importAText', 'focus', 'setEditable', - 'getFormattedCode', 'setOnKeyPress', 'setOnKeyDown', 'setNotifyDirty', @@ -121,7 +120,6 @@ const Ace2Editor = function () { 'applyPreparedChangesetToBase', 'setUserChangeNotificationCallback', 'setAuthorInfo', - 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange', @@ -138,8 +136,6 @@ const Ace2Editor = function () { this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n'; - this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop); - this.getInInternationalComposition = () => loaded ? info.ace_getInInternationalComposition() : null; @@ -153,9 +149,6 @@ const Ace2Editor = function () { // changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly. this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null; - // returns array of {error: , time: +new Date()} - this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : []; - const addStyleTagsFor = (doc, files) => { for (const file of files) { const link = doc.createElement('link'); @@ -198,7 +191,9 @@ const Ace2Editor = function () { // - Chrome never fires any events on the frame or document. Eventually the document's // readyState becomes 'complete' even though it never fires a readystatechange event. // - Safari behaves like Chrome. - outerFrame.srcdoc = ''; + // srcdoc is avoided because Firefox's Content Security Policy engine does not properly handle + // 'self' with nested srcdoc iframes: https://bugzilla.mozilla.org/show_bug.cgi?id=1721296 + outerFrame.src = '../static/empty.html'; info.frame = outerFrame; document.getElementById(containerId).appendChild(outerFrame); const outerWindow = outerFrame.contentWindow; @@ -228,6 +223,10 @@ const Ace2Editor = function () { sideDiv.id = 'sidediv'; sideDiv.classList.add('sidediv'); outerDocument.body.appendChild(sideDiv); + const sideDivInner = outerDocument.createElement('div'); + sideDivInner.id = 'sidedivinner'; + sideDivInner.classList.add('sidedivinner'); + sideDiv.appendChild(sideDivInner); const lineMetricsDiv = outerDocument.createElement('div'); lineMetricsDiv.id = 'linemetricsdiv'; lineMetricsDiv.appendChild(outerDocument.createTextNode('x')); @@ -241,8 +240,7 @@ const Ace2Editor = function () { innerFrame.allowTransparency = true; // for IE // The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above // outerFrame.srcdoc. - innerFrame.srcdoc = ''; - innerFrame.ace_outerWin = outerWindow; + innerFrame.src = 'empty.html'; outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild); const innerWindow = innerFrame.contentWindow; @@ -284,7 +282,6 @@ const Ace2Editor = function () { // tag innerDocument.body.id = 'innerdocbody'; innerDocument.body.classList.add('innerdocbody'); - innerDocument.body.setAttribute('role', 'application'); innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index d3f86f699bc..c1dab5cfd8b 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -22,8 +22,6 @@ * limitations under the License. */ -const Security = require('./security'); - const isNodeText = (node) => (node.nodeType === 3); const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; @@ -60,8 +58,6 @@ const binarySearchInfinite = (expectedLength, func) => { return binarySearch(i, func); }; -const htmlPrettyEscape = (str) => Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); - const noop = () => {}; exports.isNodeText = isNodeText; @@ -69,5 +65,4 @@ exports.getAssoc = getAssoc; exports.setAssoc = setAssoc; exports.binarySearch = binarySearch; exports.binarySearchInfinite = binarySearchInfinite; -exports.htmlPrettyEscape = htmlPrettyEscape; exports.noop = noop; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index afb3b5e7ca5..6754270197c 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -26,7 +26,6 @@ const $ = require('./rjquery').$; const isNodeText = Ace2Common.isNodeText; const getAssoc = Ace2Common.getAssoc; const setAssoc = Ace2Common.setAssoc; -const htmlPrettyEscape = Ace2Common.htmlPrettyEscape; const noop = Ace2Common.noop; const hooks = require('./pluginfw/hooks'); @@ -51,8 +50,6 @@ function Ace2Inner(editorInfo, cssManagers) { const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; const SELECT_BUTTON_CLASS = 'selected'; - const caughtErrors = []; - let thisAuthor = ''; let disposed = false; @@ -61,24 +58,20 @@ function Ace2Inner(editorInfo, cssManagers) { window.focus(); }; - const iframe = window.frameElement; - const outerWin = iframe.ace_outerWin; - iframe.ace_outerWin = null; // prevent IE 6 memory leak - const sideDiv = iframe.nextSibling; - const lineMetricsDiv = sideDiv.nextSibling; - let lineNumbersShown; - let sideDivInner; - - const initLineNumbers = () => { - const htmlOpen = '
1'; - const htmlClose = '
'; - lineNumbersShown = 1; - sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; - sideDivInner = outerWin.document.getElementById('sidedivinner'); - $(sideDiv).addClass('sidediv'); + 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); }; - - initLineNumbers(); + appendNewSideDivLine(); const scroll = Scroll.init(outerWin); @@ -86,24 +79,45 @@ function Ace2Inner(editorInfo, cssManagers) { let outsideKeyPress = (e) => true; let outsideNotifyDirty = noop; - // Document representation. + /** + * Document representation. + */ const rep = { - // Each entry in this skip list is an object created by createDomLineEntry(). The object - // represents a line (paragraph) of content. + /** + * The contents of the document. Each entry in this skip list is an object representing a + * line (actually paragraph) of text. The line objects are created by createDomLineEntry(). + */ lines: new SkipList(), - // Points at the start of the selection. Represented as [zeroBasedLineNumber, - // zeroBasedColumnNumber]. - // TODO: If the selection starts at the beginning of a line, I think this could be either - // [lineNumber, 0] or [previousLineNumber, previousLineLength]. Need to confirm. + /** + * Start of the selection. Represented as an array of two non-negative numbers that point to the + * first character of the selection: [zeroBasedLineNumber, zeroBasedColumnNumber]. Notes: + * - There is an implicit newline character (not actually stored) at the end of every line. + * Because of this, a selection that starts at the end of a line (column number equals the + * number of characters in the line, not including the implicit newline) is not equivalent + * to a selection that starts at the beginning of the next line. The same goes for the + * selection end. + * - If there are N lines, [N, 0] is valid for the start of the selection. [N, 0] indicates + * that the selection starts just after the implicit newline at the end of the document's + * last line (if the document has any lines). The same goes for the end of the selection. + * - If a line starts with a line marker, a selection that starts at the beginning of the line + * may start either immediately before (column = 0) or immediately after (column = 1) the + * line marker, and the two are considered to be semantically equivalent. For safety, all + * code should be written to accept either but only produce selections that start after the + * line marker (the column number should be 1, not 0, when there is a line marker). The same + * goes for the end of the selection. + */ selStart: null, - // Points at the character just past the last selected character. Same representation as - // selStart. - // TODO: If the last selected character is the last character of a line, I think this could be - // either [lineNumber, lineLength] or [lineNumber+1, 0]. Need to confirm. + /** + * End of the selection. Represented as an array of two non-negative numbers that point to the + * character just after the end of the selection: [zeroBasedLineNumber, zeroBasedColumnNumber]. + * See the above notes for selStart. + */ selEnd: null, - // Whether the selection extends "backwards", so that the focus point (controlled with the arrow - // keys) is at the beginning. This is not supported in IE, though native IE selections have that - // behavior (which we try not to interfere with). Must be false if selection is collapsed! + /** + * Whether the selection extends "backwards", so that the focus point (controlled with the arrow + * keys) is at the beginning. This is not supported in IE, though native IE selections have that + * behavior (which we try not to interfere with). Must be false if selection is collapsed! + */ selFocusAtStart: false, alltext: '', alines: [], @@ -115,7 +129,6 @@ function Ace2Inner(editorInfo, cssManagers) { undoModule.apool = rep.apool; } - let root, doc; // set in init() let isEditable = true; let doesWrap = true; let hasLineNumbers = true; @@ -143,23 +156,15 @@ function Ace2Inner(editorInfo, cssManagers) { 'profileEnd', ]; console = {}; - for (let i = 0; i < names.length; ++i) console[names[i]] = noop; + for (const name of names) console[name] = noop; } - // "dmesg" is for displaying messages in the in-page output pane - // visible when "?djs=1" is appended to the pad URL. It generally - // remains a no-op unless djs is enabled, but we make a habit of - // only calling it in error cases or while debugging. - let dmesg = noop; - window.dmesg = 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; - // dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); // start[0]: <--- start[1] --->CCCCCCCCCCC\n // CCCCCCCCCCCCCCCCCCCC\n // CCCC\n @@ -289,9 +294,9 @@ function Ace2Inner(editorInfo, cssManagers) { applyChangesToBase: 1, }; - hooks.callAll('aceRegisterNonScrollableEditEvents').forEach((eventType) => { + for (const eventType of hooks.callAll('aceRegisterNonScrollableEditEvents')) { _nonScrollableEditEvents[eventType] = 1; - }); + } const isScrollableEditEvent = (eventType) => !_nonScrollableEditEvents[eventType]; @@ -368,14 +373,6 @@ function Ace2Inner(editorInfo, cssManagers) { }); cleanExit = true; - } catch (e) { - caughtErrors.push( - { - error: e, - time: +new Date(), - }); - dmesg(e.toString()); - throw e; } finally { const cs = currentCallStack; if (cleanExit) { @@ -417,7 +414,7 @@ function Ace2Inner(editorInfo, cssManagers) { const setWraps = (newVal) => { doesWrap = newVal; - root.classList.toggle('doesWrap', doesWrap); + document.body.classList.toggle('doesWrap', doesWrap); scheduler.setTimeout(() => { inCallStackIfNecessary('setWraps', () => { fastIncorp(7); @@ -447,7 +444,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const setTextFace = (face) => { - root.style.fontFamily = face; + document.body.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face; }; @@ -458,8 +455,8 @@ function Ace2Inner(editorInfo, cssManagers) { const setEditable = (newVal) => { isEditable = newVal; - root.contentEditable = isEditable ? 'true' : 'false'; - root.classList.toggle('static', !isEditable); + document.body.contentEditable = isEditable ? 'true' : 'false'; + document.body.classList.toggle('static', !isEditable); }; const enforceEditability = () => setEditable(isEditable); @@ -539,15 +536,11 @@ function Ace2Inner(editorInfo, cssManagers) { performDocumentApplyChangeset(changeset); performSelectionChange( - [0, rep.lines.atIndex(0).lineMarker], - [0, rep.lines.atIndex(0).lineMarker] - ); + [0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); idleWorkTimer.atMost(100); if (rep.alltext !== atext.text) { - dmesg(htmlPrettyEscape(rep.alltext)); - dmesg(htmlPrettyEscape(atext.text)); throw new Error('mismatch error setting raw text in setDocAText'); } }; @@ -586,25 +579,6 @@ function Ace2Inner(editorInfo, cssManagers) { outsideNotifyDirty = handler; }; - const getFormattedCode = () => { - if (currentCallStack && !currentCallStack.domClean) { - inCallStackIfNecessary('getFormattedCode', incorporateUserChanges); - } - const buf = []; - if (rep.lines.length() > 0) { - // should be the case, even for empty file - let entry = rep.lines.atIndex(0); - while (entry) { - const domInfo = entry.domInfo; - buf.push((domInfo && domInfo.getInnerHTML()) || - domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || - ' ' /* empty line*/); - entry = rep.lines.next(entry); - } - } - return `
${buf.join('
\n
')}
`; - }; - const CMDS = { clearauthorship: (prompt) => { if ((!(rep.selStart && rep.selEnd)) || isCaret()) { @@ -665,14 +639,13 @@ function Ace2Inner(editorInfo, cssManagers) { // These properties are exposed const setters = { wraps: setWraps, - showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val), - showsuserselections: (val) => root.classList.toggle('userSelections', !!val), + showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val), + showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val), showslinenumbers: (value) => { hasLineNumbers = !!value; sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); fixView(); }, - dmesg: () => { dmesg = window.dmesg = value; }, userauthor: (value) => { thisAuthor = String(value); documentAttributeManager.author = thisAuthor; @@ -680,8 +653,8 @@ function Ace2Inner(editorInfo, cssManagers) { styled: setStyled, textface: setTextFace, rtlistrue: (value) => { - root.classList.toggle('rtl', value); - root.classList.toggle('ltr', !value); + document.body.classList.toggle('rtl', value); + document.body.classList.toggle('ltr', !value); document.documentElement.dir = value ? 'rtl' : 'ltr'; }, }; @@ -713,27 +686,8 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_setAuthorInfo = (author, info) => { setAuthorInfo(author, info); }; - editorInfo.ace_setAuthorSelectionRange = (author, start, end) => { - changesetTracker.setAuthorSelectionRange(author, start, end); - }; - - editorInfo.ace_getUnhandledErrors = () => caughtErrors.slice(); - editorInfo.ace_getDocument = () => doc; - - editorInfo.ace_getDebugProperty = (prop) => { - if (prop === 'debugger') { - // obfuscate "eval" so as not to scare yuicompressor - window['ev' + 'al']('debugger'); - } else if (prop === 'rep') { - return rep; - } else if (prop === 'window') { - return window; - } else if (prop === 'document') { - return document; - } - return undefined; - }; + editorInfo.ace_getDocument = () => document; const now = () => Date.now(); @@ -939,11 +893,11 @@ function Ace2Inner(editorInfo, cssManagers) { clearObservedChanges(); const getCleanNodeByKey = (key) => { - let n = doc.getElementById(key); + let n = document.getElementById(key); // copying and pasting can lead to duplicate ids while (n && isNodeDirty(n)) { n.id = ''; - n = doc.getElementById(key); + n = document.getElementById(key); } return n; }; @@ -1025,11 +979,11 @@ function Ace2Inner(editorInfo, cssManagers) { const observeSuspiciousNodes = () => { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. - if (root.getElementsByTagName) { - const nds = root.getElementsByTagName('style'); - for (let i = 0; i < nds.length; i++) { - const n = topLevel(nds[i]); - if (n && n.parentNode === root) { + 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); } } @@ -1044,8 +998,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; // returns true if dom changes were made - if (!root.firstChild) { - root.innerHTML = '
'; + if (!document.body.firstChild) { + document.body.innerHTML = '
'; } observeChangesAroundSelection(); @@ -1067,9 +1021,7 @@ function Ace2Inner(editorInfo, cssManagers) { j++; } if (!dirtyRangesCheckOut) { - const numBodyNodes = root.childNodes.length; - for (let k = 0; k < numBodyNodes; k++) { - const bodyNode = root.childNodes.item(k); + for (const bodyNode of document.body.childNodes) { if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); } @@ -1091,11 +1043,11 @@ function Ace2Inner(editorInfo, cssManagers) { const range = dirtyRanges[i]; a = range[0]; b = range[1]; - let firstDirtyNode = (((a === 0) && root.firstChild) || + let firstDirtyNode = (((a === 0) && document.body.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); - let lastDirtyNode = (((b === rep.lines.length()) && root.lastChild) || + let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); @@ -1145,48 +1097,30 @@ function Ace2Inner(editorInfo, cssManagers) { const entries = []; const nodeToAddAfter = lastDirtyNode; - const lineNodeInfos = new Array(lines.length); - for (let k = 0; k < lines.length; k++) { - const lineString = lines[k]; + const lineNodeInfos = []; + for (const lineString of lines) { const newEntry = createDomLineEntry(lineString); entries.push(newEntry); - lineNodeInfos[k] = newEntry.domInfo; + lineNodeInfos.push(newEntry.domInfo); } domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - dirtyNodes.forEach((n) => { - toDeleteAtEnd.push(n); - }); + for (const n of dirtyNodes) toDeleteAtEnd.push(n); const spliceHints = {}; if (selStart) spliceHints.selStart = selStart; if (selEnd) spliceHints.selEnd = selEnd; splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]); netNumLinesChangeSoFar += (lines.length - (b - a)); } else if (b > a) { - splicesToDo.push([a + netNumLinesChangeSoFar, - b - a, - [], - []]); + splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [], []]); } i++; } const domChanges = (splicesToDo.length > 0); - // update the representation - splicesToDo.forEach((splice) => { - doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); - }); - - // do DOM inserts - domInsertsNeeded.forEach((ins) => { - insertDomLines(ins[0], ins[1]); - }); - - // delete old dom nodes - toDeleteAtEnd.forEach((n) => { - // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) - if (n.parentNode) n.parentNode.removeChild(n); - }); + for (const splice of splicesToDo) doIncorpLineSplice(...splice); + for (const ins of domInsertsNeeded) insertDomLines(...ins); + for (const n of toDeleteAtEnd) n.remove(); // needed to stop chrome from breaking the ui when long strings without spaces are pasted if (scrollToTheLeftNeeded) { @@ -1200,7 +1134,7 @@ function Ace2Inner(editorInfo, cssManagers) { callstack: currentCallStack, editorInfo, rep, - root, + root: document.body, point: selection.startPoint, documentAttributeManager, }); @@ -1212,7 +1146,7 @@ function Ace2Inner(editorInfo, cssManagers) { callstack: currentCallStack, editorInfo, rep, - root, + root: document.body, point: selection.endPoint, documentAttributeManager, }); @@ -1269,9 +1203,7 @@ function Ace2Inner(editorInfo, cssManagers) { const insertDomLines = (nodeToAddAfter, infoStructs) => { let lastEntry; let lineStartOffset; - if (infoStructs.length < 1) return; - - infoStructs.forEach((info) => { + for (const info of infoStructs) { const node = info.node; const key = uniqueId(node); let entry; @@ -1294,22 +1226,18 @@ function Ace2Inner(editorInfo, cssManagers) { info.prepareForAdd(); entry.lineMarker = info.lineMarker; if (!nodeToAddAfter) { - root.insertBefore(node, root.firstChild); + document.body.insertBefore(node, document.body.firstChild); } else { - root.insertBefore(node, nodeToAddAfter.nextSibling); + document.body.insertBefore(node, nodeToAddAfter.nextSibling); } nodeToAddAfter = node; info.notifyAdded(); markNodeClean(node); - }); + } }; - const isCaret = () => ( - rep.selStart && - rep.selEnd && - rep.selStart[0] === rep.selEnd[0] && - rep.selStart[1] === rep.selEnd[1] - ); + const isCaret = () => (rep.selStart && rep.selEnd && + rep.selStart[0] === rep.selEnd[0] && rep.selStart[1] === rep.selEnd[1]); editorInfo.ace_isCaret = isCaret; // prereq: isCaret() @@ -1397,7 +1325,7 @@ function Ace2Inner(editorInfo, cssManagers) { // Turn DOM node selection into [line,char] selection. // This method has to work when the DOM is not pristine, // assuming the point is not in a dirty node. - if (point.node === root) { + if (point.node === document.body) { if (point.index === 0) { return [0, 0]; } else { @@ -1416,7 +1344,7 @@ function Ace2Inner(editorInfo, cssManagers) { col = nodeText(n).length; } let parNode, prevSib; - while ((parNode = n.parentNode) !== root) { + while ((parNode = n.parentNode) !== document.body) { if ((prevSib = n.previousSibling)) { n = prevSib; col += nodeText(n).length; @@ -1468,10 +1396,10 @@ function Ace2Inner(editorInfo, cssManagers) { insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); - keysToDelete.forEach((k) => { - const n = doc.getElementById(k); + for (const k of keysToDelete) { + const n = document.getElementById(k); n.parentNode.removeChild(n); - }); + } if ( (rep.selStart && @@ -1494,7 +1422,6 @@ function Ace2Inner(editorInfo, cssManagers) { } const linesMutatee = { - // TODO: Rhansen to check usage of args here. splice: (start, numRemoved, ...args) => { domAndRepSplice(start, numRemoved, args.map((s) => s.slice(0, -1))); }, @@ -1506,12 +1433,9 @@ function Ace2Inner(editorInfo, cssManagers) { if (requiredSelectionSetting) { performSelectionChange( - lineAndColumnFromChar( - requiredSelectionSetting[0] - ), + lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), - requiredSelectionSetting[2] - ); + requiredSelectionSetting[2]); } }; @@ -1523,27 +1447,25 @@ function Ace2Inner(editorInfo, cssManagers) { throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`); } - ((changes) => { - const editEvent = currentCallStack.editEvent; - if (editEvent.eventType === 'nonundoable') { - if (!editEvent.changeset) { - editEvent.changeset = changes; - } else { - editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); - } + const editEvent = currentCallStack.editEvent; + if (editEvent.eventType === 'nonundoable') { + if (!editEvent.changeset) { + editEvent.changeset = changes; } else { - const inverseChangeset = Changeset.inverse(changes, { - get: (i) => `${rep.lines.atIndex(i).text}\n`, - length: () => rep.lines.length(), - }, rep.alines, rep.apool); + 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); - } + if (!editEvent.backset) { + editEvent.backset = inverseChangeset; + } else { + editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); } - })(changes); + } Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); @@ -1586,8 +1508,8 @@ function Ace2Inner(editorInfo, cssManagers) { newText = newText.substring(0, newText.length - 1); } } - performDocumentReplaceRange(lineAndColumnFromChar(startChar), - lineAndColumnFromChar(endChar), newText); + performDocumentReplaceRange( + lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); }; const performDocumentApplyAttributesToCharRange = (start, end, attribs) => { @@ -1730,10 +1652,7 @@ function Ace2Inner(editorInfo, cssManagers) { const attributeValue = selectionAllHasIt ? '' : 'true'; documentAttributeManager.setAttributesOnRange( - rep.selStart, - rep.selEnd, - [[attributeName, attributeValue]] - ); + rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); if (attribIsFormattingStyle(attributeName)) { updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... } @@ -1748,9 +1667,7 @@ function Ace2Inner(editorInfo, cssManagers) { // Change the abstract representation of the document to have a different set of lines. // Must be called after rep.alltext is set. const doRepLineSplice = (startLine, deleteCount, newLineEntries) => { - newLineEntries.forEach((entry) => { - entry.width = entry.text.length + 1; - }); + for (const entry of newLineEntries) entry.width = entry.text.length + 1; const startOldChar = rep.lines.offsetOfIndex(startLine); const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); @@ -1783,9 +1700,8 @@ function Ace2Inner(editorInfo, cssManagers) { const oldText = rep.alltext.substring(startOldChar, endOldChar); const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset - const analysis = analyzeChange( - oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar - ); + const analysis = + analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); const commonStart = analysis[0]; let commonEnd = analysis[1]; let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); @@ -1870,9 +1786,7 @@ function Ace2Inner(editorInfo, cssManagers) { return rep.apool.putAttrib([k, '']); } return false; - } - ) - ); + })); const builder1 = startBuilder(); if (shiftFinalNewlineToBeforeNewText) { @@ -2111,8 +2025,7 @@ function Ace2Inner(editorInfo, cssManagers) { isScrollableEditEvent(currentCallStack.type); const innerHeight = getInnerHeight(); scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary( - rep, isScrollableEvent, innerHeight * 2 - ); + rep, isScrollableEvent, innerHeight * 2); } return true; @@ -2120,11 +2033,7 @@ function Ace2Inner(editorInfo, cssManagers) { return false; }; - const isPadLoading = (eventType) => ( - eventType === 'setup') || - (eventType === 'setBaseText') || - (eventType === 'importText' - ); + const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText'; const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => { const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); @@ -2134,14 +2043,15 @@ function Ace2Inner(editorInfo, cssManagers) { const attribIsFormattingStyle = (attribName) => FORMATTING_STYLES.indexOf(attribName) !== -1; const selectFormattingButtonIfLineHasStyleApplied = (rep) => { - FORMATTING_STYLES.forEach((style) => { + for (const style of FORMATTING_STYLES) { const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); updateStyleButtonState(style, hasStyleOnRepSelection); - }); + } }; - const doCreateDomLine = (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, doc); + const doCreateDomLine = + (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, document); const textify = (str) => str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); @@ -2155,9 +2065,7 @@ function Ace2Inner(editorInfo, cssManagers) { ul: 1, }; - hooks.callAll('aceRegisterBlockElements').forEach((element) => { - _blockElems[element] = 1; - }); + for (const element of hooks.callAll('aceRegisterBlockElements')) _blockElems[element] = 1; const isBlockElement = (n) => !!_blockElems[(n.tagName || '').toLowerCase()]; editorInfo.ace_isBlockElement = isBlockElement; @@ -2200,7 +2108,7 @@ function Ace2Inner(editorInfo, cssManagers) { const a = cleanNodeForIndex(i - 1); const b = cleanNodeForIndex(i); if ((!a) || (!b)) return false; // violates precondition - if ((a === true) && (b === true)) return !root.firstChild; + if ((a === true) && (b === true)) return !document.body.firstChild; if ((a === true) && b.previousSibling) return false; if ((b === true) && a.nextSibling) return false; if ((a === true) || (b === true)) return true; @@ -2221,16 +2129,13 @@ function Ace2Inner(editorInfo, cssManagers) { [-1, N + 1], ]; + // returns index of cleanRange containing i, or -1 if none const rangeForLine = (i) => { - // returns index of cleanRange containing i, or -1 if none - let answer = -1; - cleanRanges.forEach((r, idx) => { - if (i >= r[1]) return false; // keep looking - if (i < r[0]) return true; // not found, stop looking - answer = idx; - return true; // found, stop looking - }); - return answer; + for (const [idx, r] of cleanRanges.entries()) { + if (i < r[0]) return -1; + if (i < r[1]) return idx; + } + return -1; }; const removeLineFromRange = (rng, line) => { @@ -2325,13 +2230,11 @@ function Ace2Inner(editorInfo, cssManagers) { detectChangesAroundLine(0, 1); detectChangesAroundLine(N - 1, 1); - for (const k in observedChanges.cleanNodesNearChanges) { - if (observedChanges.cleanNodesNearChanges[k]) { - const key = k.substring(1); - if (rep.lines.containsKey(key)) { - const line = rep.lines.indexOfKey(key); - detectChangesAroundLine(line, 2); - } + for (const k of Object.keys(observedChanges.cleanNodesNearChanges)) { + const key = k.substring(1); + if (rep.lines.containsKey(key)) { + const line = rep.lines.indexOfKey(key); + detectChangesAroundLine(line, 2); } } } @@ -2346,14 +2249,11 @@ function Ace2Inner(editorInfo, cssManagers) { const markNodeClean = (n) => { // clean nodes have knownHTML that matches their innerHTML - const dirtiness = {}; - dirtiness.nodeId = uniqueId(n); - dirtiness.knownHTML = n.innerHTML; - setAssoc(n, 'dirtiness', dirtiness); + setAssoc(n, 'dirtiness', {nodeId: uniqueId(n), knownHTML: n.innerHTML}); }; const isNodeDirty = (n) => { - if (n.parentNode !== root) return true; + if (n.parentNode !== document.body) return true; const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; @@ -2389,9 +2289,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const hideEditBarDropdowns = () => { - if (window.parent.parent.padeditbar) { // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327 - window.parent.parent.padeditbar.toggleDropDown('none'); - } + window.parent.parent.padeditbar.toggleDropDown('none'); }; const renumberList = (lineNum) => { @@ -2508,11 +2406,10 @@ function Ace2Inner(editorInfo, cssManagers) { const doIndentOutdent = (isOut) => { if (!((rep.selStart && rep.selEnd) || - ((rep.selStart[0] === rep.selEnd[0]) && - (rep.selStart[1] === rep.selEnd[1]) && - rep.selEnd[1] > 1)) && - (isOut !== true) - ) { + (rep.selStart[0] === rep.selEnd[0] && + rep.selStart[1] === rep.selEnd[1] && + rep.selEnd[1] > 1)) && + isOut !== true) { return false; } @@ -2536,9 +2433,7 @@ function Ace2Inner(editorInfo, cssManagers) { } } - mods.forEach((mod) => { - setLineListType(mod[0], mod[1]); - }); + for (const mod of mods) setLineListType(mod[0], mod[1]); return true; }; editorInfo.ace_doIndentOutdent = doIndentOutdent; @@ -2586,9 +2481,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (prevLineBlank && !prevLineListType) { // previous line is blank, remove it performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, 0], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } else { // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); @@ -2596,15 +2489,11 @@ function Ace2Inner(editorInfo, cssManagers) { } else if (thisLineHasMarker && prevLineEntry) { // If the line has any attributes assigned, remove them by removing the marker '*' performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, lineEntry.lineMarker], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); } else if (theLine > 0) { // remove newline performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, 0], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } } else { const docChar = caretDocChar(); @@ -2645,41 +2534,30 @@ function Ace2Inner(editorInfo, cssManagers) { const handleKeyEvent = (evt) => { if (!isEditable) return; - const type = evt.type; - const charCode = evt.charCode; - const keyCode = evt.keyCode; - const which = evt.which; - const altKey = evt.altKey; - const shiftKey = evt.shiftKey; + const {type, charCode, keyCode, which, altKey, shiftKey} = evt; // Don't take action based on modifier keys going up and down. // Modifier keys do not generate "keypress" events. // 224 is the command-key under Mac Firefox. // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key // 20 is capslock in IE. - const isModKey = ((!charCode) && - ((type === 'keyup') || (type === 'keydown')) && - ( - keyCode === 16 || keyCode === 17 || keyCode === 18 || - keyCode === 20 || keyCode === 224 || keyCode === 91 - )); + const isModKey = !charCode && (type === 'keyup' || type === 'keydown') && + (keyCode === 16 || keyCode === 17 || keyCode === 18 || + keyCode === 20 || keyCode === 224 || keyCode === 91); if (isModKey) return; // If the key is a keypress and the browser is opera and the key is enter, // do nothign at all as this fires twice. - if (keyCode === 13 && browser.opera && (type === 'keypress')) { + if (keyCode === 13 && browser.opera && type === 'keypress') { // This stops double enters in Opera but double Tabs still show on single // tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice return; } - let specialHandled = false; - const isTypeForSpecialKey = ((browser.safari || - browser.chrome || - browser.firefox) ? (type === 'keydown') : (type === 'keypress')); - const isTypeForCmdKey = ((browser.safari || - browser.chrome || - browser.firefox) ? (type === 'keydown') : (type === 'keypress')); + const isTypeForSpecialKey = browser.safari || browser.chrome || browser.firefox + ? type === 'keydown' : type === 'keypress'; + const isTypeForCmdKey = browser.safari || browser.chrome || browser.firefox + ? type === 'keydown' : type === 'keypress'; let stopped = false; @@ -2697,6 +2575,7 @@ function Ace2Inner(editorInfo, cssManagers) { } else if (type === 'keydown') { outsideKeyDown(evt); } + let specialHandled = false; if (!stopped) { const specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, @@ -2712,13 +2591,9 @@ function Ace2Inner(editorInfo, cssManagers) { } const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - if ( - (!specialHandled) && - altKey && - isTypeForSpecialKey && - keyCode === 120 && - padShortcutEnabled.altF9 - ) { + if (!specialHandled && isTypeForSpecialKey && + altKey && keyCode === 120 && + padShortcutEnabled.altF9) { // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... @@ -2731,26 +2606,18 @@ function Ace2Inner(editorInfo, cssManagers) { firstEditbarElement.focus(); evt.preventDefault(); } - if ( - (!specialHandled) && - altKey && keyCode === 67 && - type === 'keydown' && - padShortcutEnabled.altC - ) { + if (!specialHandled && type === 'keydown' && + altKey && keyCode === 67 && + padShortcutEnabled.altC) { // Alt c focuses on the Chat window $(this).blur(); parent.parent.chat.show(); parent.parent.$('#chatinput').focus(); evt.preventDefault(); } - if ( - (!specialHandled) && - evt.ctrlKey && - shiftKey && - keyCode === 50 && - type === 'keydown' && - padShortcutEnabled.cmdShift2 - ) { + if (!specialHandled && type === 'keydown' && + evt.ctrlKey && shiftKey && keyCode === 50 && + padShortcutEnabled.cmdShift2) { // Control-Shift-2 shows a gritter popup showing a line author const lineNumber = rep.selEnd[0]; const alineAttrs = rep.alines[lineNumber]; @@ -2760,75 +2627,33 @@ function Ace2Inner(editorInfo, cssManagers) { // TODO: Still work when authorship colors have been cleared // TODO: i18n // TODO: There appears to be a race condition or so. - const authors = []; - let author = null; + const authorIds = new Set(); if (alineAttrs) { const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()) { const op = opIter.next(); const authorId = Changeset.opAttributeValue(op, 'author', apool); - - // Only push unique authors and ones with values - if (authors.indexOf(authorId) === -1 && authorId !== '') { - authors.push(authorId); - } + if (authorId !== '') authorIds.add(authorId); } } - - let authorString; - const authorNames = []; - if (authors.length === 0) { - authorString = 'No author information is available'; - } else { - // Known authors info, both current and historical - const padAuthors = parent.parent.pad.userList(); - let authorObj = {}; - authors.forEach((authorId) => { - padAuthors.forEach((padAuthor) => { - // If the person doing the lookup is the author.. - if (padAuthor.userId === authorId) { - if (parent.parent.clientVars.userId === authorId) { - authorObj = { - name: 'Me', - }; - } else { - authorObj = padAuthor; - } - } - }); - if (!authorObj) { - author = 'Unknown'; - return; - } - author = authorObj.name; - if (!author) author = 'Unknown'; - authorNames.push(author); - }); - } - if (authors.length === 1) { - authorString = `The author of this line is ${authorNames[0]}`; - } - if (authors.length > 1) { - authorString = `The authors of this line are ${authorNames.join(' & ')}`; - } + const idToName = new Map(parent.parent.pad.userList().map((a) => [a.userId, a.name])); + const myId = parent.parent.clientVars.userId; + const authors = + [...authorIds].map((id) => id === myId ? 'me' : idToName.get(id) || 'unknown'); parent.parent.$.gritter.add({ - // (string | mandatory) the heading of the notification title: 'Line Authors', - // (string | mandatory) the text inside the notification - text: authorString, - // (bool | optional) if you want it to fade out on its own or just sit there + text: + authors.length === 0 ? 'No author information is available' + : authors.length === 1 ? `The author of this line is ${authors[0]}` + : `The authors of this line are ${authors.join(' & ')}`, sticky: false, - // (int | optional) the time you want it to be alive for before fading out time: '4000', }); } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 8 && - padShortcutEnabled.delete - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 8 && + padShortcutEnabled.delete) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, // or else deleting a blank line can take two delete presses. // -- @@ -2841,11 +2666,9 @@ function Ace2Inner(editorInfo, cssManagers) { doDeleteKey(evt); specialHandled = true; } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 13 && - padShortcutEnabled.return - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 13 && + padShortcutEnabled.return) { // return key, handle specially; // note that in mozilla we need to do an incorporation for proper return behavior anyway. fastIncorp(4); @@ -2856,11 +2679,9 @@ function Ace2Inner(editorInfo, cssManagers) { }, 0); specialHandled = true; } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 27 && - padShortcutEnabled.esc - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 27 && + padShortcutEnabled.esc) { // prevent esc key; // in mozilla versions 14-19 avoid reconnecting pad. @@ -2871,15 +2692,11 @@ function Ace2Inner(editorInfo, cssManagers) { // close all gritters when the user hits escape key parent.parent.$.gritter.removeAll(); } - if ( - (!specialHandled) && - /* Do a saved revision on ctrl S */ - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 's' && - (evt.metaKey || evt.ctrlKey) && - !evt.altKey && - padShortcutEnabled.cmdS - ) { + if (!specialHandled && isTypeForCmdKey && + /* Do a saved revision on ctrl S */ + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 's' && + !evt.altKey && + padShortcutEnabled.cmdS) { evt.preventDefault(); const originalBackground = parent.parent.$('#revisionlink').css('background'); parent.parent.$('#revisionlink').css({background: 'lightyellow'}); @@ -2890,25 +2707,21 @@ function Ace2Inner(editorInfo, cssManagers) { parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); specialHandled = true; } - if ((!specialHandled) && - // tab - isTypeForSpecialKey && - keyCode === 9 && - !(evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.tab) { + if (!specialHandled && isTypeForSpecialKey && + // tab + keyCode === 9 && + !(evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.tab) { fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); specialHandled = true; } - if ((!specialHandled) && - // cmd-Z (undo) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'z' && - (evt.metaKey || evt.ctrlKey) && - !evt.altKey && - padShortcutEnabled.cmdZ - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-Z (undo) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'z' && + !evt.altKey && + padShortcutEnabled.cmdZ) { fastIncorp(6); evt.preventDefault(); if (evt.shiftKey) { @@ -2918,120 +2731,93 @@ function Ace2Inner(editorInfo, cssManagers) { } specialHandled = true; } - if ((!specialHandled) && - // cmd-Y (redo) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'y' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdY - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-Y (redo) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'y' && + padShortcutEnabled.cmdY) { fastIncorp(10); evt.preventDefault(); doUndoRedo('redo'); specialHandled = true; } - if ((!specialHandled) && - // cmd-B (bold) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'b' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdB) { + if (!specialHandled && isTypeForCmdKey && + // cmd-B (bold) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'b' && + padShortcutEnabled.cmdB) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('bold'); specialHandled = true; } - if ((!specialHandled) && - // cmd-I (italic) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'i' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdI - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-I (italic) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'i' && + padShortcutEnabled.cmdI) { fastIncorp(14); evt.preventDefault(); toggleAttributeOnSelection('italic'); specialHandled = true; } - if ((!specialHandled) && - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'u' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdU - ) { - // cmd-U (underline) + if (!specialHandled && isTypeForCmdKey && + // cmd-U (underline) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'u' && + padShortcutEnabled.cmdU) { fastIncorp(15); evt.preventDefault(); toggleAttributeOnSelection('underline'); specialHandled = true; } - if ((!specialHandled) && - // cmd-5 (strikethrough) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === '5' && - (evt.metaKey || evt.ctrlKey) && - evt.altKey !== true && - padShortcutEnabled.cmd5 - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-5 (strikethrough) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === '5' && + evt.altKey !== true && + padShortcutEnabled.cmd5) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('strikethrough'); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-L (unorderedlist) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'l' && - (evt.metaKey || evt.ctrlKey) && - evt.shiftKey && - padShortcutEnabled.cmdShiftL - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-L (unorderedlist) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'l' && + evt.shiftKey && + padShortcutEnabled.cmdShiftL) { fastIncorp(9); evt.preventDefault(); doInsertUnorderedList(); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-N and cmd-shift-1 (orderedlist) - isTypeForCmdKey && - ( - (String.fromCharCode(which).toLowerCase() === 'n' && - padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) === '1' && - padShortcutEnabled.cmdShift1) - ) && (evt.metaKey || evt.ctrlKey) && - evt.shiftKey - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-N and cmd-shift-1 (orderedlist) + (evt.metaKey || evt.ctrlKey) && evt.shiftKey && + ((String.fromCharCode(which).toLowerCase() === 'n' && padShortcutEnabled.cmdShiftN) || + (String.fromCharCode(which) === '1' && padShortcutEnabled.cmdShift1))) { fastIncorp(9); evt.preventDefault(); doInsertOrderedList(); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-C (clearauthorship) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'c' && - (evt.metaKey || evt.ctrlKey) && - evt.shiftKey && padShortcutEnabled.cmdShiftC - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-C (clearauthorship) + (evt.metaKey || evt.ctrlKey) && evt.shiftKey && + String.fromCharCode(which).toLowerCase() === 'c' && + padShortcutEnabled.cmdShiftC) { fastIncorp(9); evt.preventDefault(); CMDS.clearauthorship(); } - if ((!specialHandled) && - // cmd-H (backspace) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'h' && - (evt.ctrlKey) && - padShortcutEnabled.cmdH - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-H (backspace) + (evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'h' && + padShortcutEnabled.cmdH) { fastIncorp(20); evt.preventDefault(); doDeleteKey(); specialHandled = true; } - if ((evt.which === 36 && evt.ctrlKey === true) && - // Control Home send to Y = 0 - padShortcutEnabled.ctrlHome) { + if (evt.ctrlKey === true && evt.which === 36 && + // Control Home send to Y = 0 + padShortcutEnabled.ctrlHome) { scroll.setScrollY(0); } if ((evt.which === 33 || evt.which === 34) && type === 'keydown' && !evt.ctrlKey) { @@ -3132,10 +2918,9 @@ function Ace2Inner(editorInfo, cssManagers) { thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { - if (type !== 'keyup') { - observeChangesAroundSelection(); - } + if (!specialHandled && !thisKeyDoesntTriggerNormalize && !inInternationalComposition && + type !== 'keyup') { + observeChangesAroundSelection(); } if (type === 'keyup') { @@ -3161,12 +2946,9 @@ function Ace2Inner(editorInfo, cssManagers) { } if (selectionInfo) { performSelectionChange( - lineAndColumnFromChar( - selectionInfo.selStart - ), + lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), - selectionInfo.selFocusAtStart - ); + selectionInfo.selFocusAtStart); } const oldEvent = currentCallStack.startNewEvent(oldEventType, true); return oldEvent; @@ -3206,15 +2988,13 @@ function Ace2Inner(editorInfo, cssManagers) { // with background doesn't seem to show up... if (isNodeText(p.node) && p.index === p.maxIndex) { let n = p.node; - while ((!n.nextSibling) && (n !== root) && (n.parentNode !== root)) { + while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) { n = n.parentNode; } - if ( - n.nextSibling && - (!((typeof n.nextSibling.tagName) === 'string' && - n.nextSibling.tagName.toLowerCase() === 'br')) && - (n !== p.node) && (n !== root) && (n.parentNode !== root) - ) { + 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); @@ -3245,25 +3025,19 @@ function Ace2Inner(editorInfo, cssManagers) { if (browserSelection) { browserSelection.removeAllRanges(); if (selection) { - isCollapsed = ( - selection.startPoint.node === selection.endPoint.node && - selection.startPoint.index === selection.endPoint.index - ); + isCollapsed = (selection.startPoint.node === selection.endPoint.node && + selection.startPoint.index === selection.endPoint.index); const start = pointToRangeBound(selection.startPoint); const end = pointToRangeBound(selection.endPoint); - if ( - (!isCollapsed) && - selection.focusAtStart && - browserSelection.collapse && - browserSelection.extend - ) { + if (!isCollapsed && selection.focusAtStart && + browserSelection.collapse && browserSelection.extend) { // can handle "backwards"-oriented selection, shift-arrow-keys move start // of selection browserSelection.collapse(end.container, end.offset); browserSelection.extend(start.container, start.offset); } else { - const range = doc.createRange(); + const range = document.createRange(); range.setStart(start.container, start.offset); range.setEnd(end.container, end.offset); browserSelection.removeAllRanges(); @@ -3304,7 +3078,6 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_setOnKeyDown = setOnKeyDown; editorInfo.ace_setNotifyDirty = setNotifyDirty; editorInfo.ace_dispose = dispose; - editorInfo.ace_getFormattedCode = getFormattedCode; editorInfo.ace_setEditable = setEditable; editorInfo.ace_execCommand = execCommand; editorInfo.ace_replaceRange = replaceRange; @@ -3341,7 +3114,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (!isInBody(container)) { // command-click in Firefox selects whole document, HEAD and BODY! return { - node: root, + node: document.body, index: 0, maxIndex: 1, }; @@ -3420,7 +3193,7 @@ function Ace2Inner(editorInfo, cssManagers) { const _teardownActions = []; - const teardown = () => _teardownActions.forEach((a) => a()); + const teardown = () => { for (const a of _teardownActions) a(); }; let inInternationalComposition = null; editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; @@ -3431,12 +3204,12 @@ function Ace2Inner(editorInfo, cssManagers) { $(document).on('keyup', handleKeyEvent); $(document).on('click', handleClick); // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer - $(outerWin.document).on('click', hideEditBarDropdowns); + $(outerDoc).on('click', hideEditBarDropdowns); // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; - $(root).on('auxclick', (e) => { + $(document.body).on('auxclick', (e) => { if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) { // The user middle-clicked on a link. Usually users do this to open a link in a new tab, but // in X11 (Linux) this will instead paste the contents of the primary selection at the mouse @@ -3458,7 +3231,7 @@ function Ace2Inner(editorInfo, cssManagers) { } }); - $(root).on('paste', (e) => { + $(document.body).on('paste', (e) => { if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { scheduler.clearTimeout(suppressPasteOnLink); suppressPasteOnLink = null; @@ -3520,8 +3293,8 @@ function Ace2Inner(editorInfo, cssManagers) { }; const topLevel = (n) => { - if ((!n) || n === root) return null; - while (n.parentNode !== root) { + if ((!n) || n === document.body) return null; + while (n.parentNode !== document.body) { n = n.parentNode; } return n; @@ -3543,8 +3316,7 @@ function Ace2Inner(editorInfo, cssManagers) { let charsToLeft = index; let charsToRight = node.nodeValue.length - index; let n; - for (n = node.previousSibling; n && - isNodeText(n); n = n.previousSibling) { + for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) { charsToLeft += n.nodeValue; } const leftEdge = (n ? rightOf(n) : leftOf(node.parentNode)); @@ -3557,11 +3329,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const getInnerHeight = () => { - const win = outerWin; - const odoc = win.document; - let h; - if (browser.opera) h = win.innerHeight; - else h = odoc.documentElement.clientHeight; + const h = browser.opera ? outerWin.innerHeight : outerDoc.documentElement.clientHeight; if (h) return h; // deal with case where iframe is hidden, hope that @@ -3569,20 +3337,15 @@ function Ace2Inner(editorInfo, cssManagers) { return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0); }; - const getInnerWidth = () => { - const win = outerWin; - const odoc = win.document; - return odoc.documentElement.clientWidth; - }; + const getInnerWidth = () => outerDoc.documentElement.clientWidth; const scrollXHorizontallyIntoView = (pixelX) => { - const win = outerWin; - const distInsideLeft = pixelX - win.scrollX; - const distInsideRight = win.scrollX + getInnerWidth() - pixelX; + const distInsideLeft = pixelX - outerWin.scrollX; + const distInsideRight = outerWin.scrollX + getInnerWidth() - pixelX; if (distInsideLeft < 0) { - win.scrollBy(distInsideLeft, 0); + outerWin.scrollBy(distInsideLeft, 0); } else if (distInsideRight < 0) { - win.scrollBy(-distInsideRight + 1, 0); + outerWin.scrollBy(-distInsideRight + 1, 0); } }; @@ -3594,9 +3357,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (!doesWrap) { const browserSelection = getSelection(); if (browserSelection) { - const focusPoint = ( - browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint - ); + const focusPoint = + browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint; const selectionPointX = getSelectionPointX(focusPoint); scrollXHorizontallyIntoView(selectionPointX); fixView(); @@ -3661,9 +3423,7 @@ function Ace2Inner(editorInfo, cssManagers) { } } - mods.forEach((mod) => { - setLineListType(mod[0], mod[1]); - }); + for (const mod of mods) setLineListType(mod[0], mod[1]); }; const doInsertUnorderedList = () => { @@ -3678,8 +3438,6 @@ function Ace2Inner(editorInfo, cssManagers) { // We apply the height of a line in the doc body, to the corresponding sidediv line number const updateLineNumbers = () => { - if (!currentCallStack || !currentCallStack.domClean) return; - // Refs #4228, to avoid layout trashing, we need to first calculate all the heights, // and then apply at once all new height to div elements const lineOffsets = []; @@ -3696,29 +3454,24 @@ function Ace2Inner(editorInfo, cssManagers) { // but as it's non-text type the line-height/margins might not be present and it // could be that this breaks a theme that has a different default line height.. // So instead of using an integer here we get the value from the Editor CSS. - const innerdocbody = document.querySelector('#innerdocbody'); - const innerdocbodyStyles = getComputedStyle(innerdocbody); + const innerdocbodyStyles = getComputedStyle(document.body); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); - let docLine = doc.body.firstChild; - let currentLine = 0; - let h = null; - - // First loop to calculate the heights from doc body - while (docLine) { - if (docLine.nextSibling) { - if (currentLine === 0) { + for (const docLine of document.body.children) { + let h; + const nextDocLine = docLine.nextElementSibling; + if (nextDocLine) { + if (lineOffsets.length === 0) { // It's the first line. For line number alignment purposes, its // height is taken to be the top offset of the next line. If we // didn't do this special case, we would miss out on any top margin // included on the first line. The default stylesheet doesn't add // extra margins/padding, but plugins might. - h = docLine.nextSibling.offsetTop - parseInt( - window.getComputedStyle(doc.body) - .getPropertyValue('padding-top').split('px')[0] - ); + h = nextDocLine.offsetTop - parseInt( + window.getComputedStyle(document.body) + .getPropertyValue('padding-top').split('px')[0]); } else { - h = docLine.nextSibling.offsetTop - docLine.offsetTop; + h = nextDocLine.offsetTop - docLine.offsetTop; } } else { // last line @@ -3739,49 +3492,15 @@ function Ace2Inner(editorInfo, cssManagers) { } else { lineHeights.push(defaultLineHeight); } - docLine = docLine.nextSibling; - currentLine++; } let newNumLines = rep.lines.length(); if (newNumLines < 1) newNumLines = 1; - let sidebarLine = sideDivInner.firstChild; - - // Apply height to existing sidediv lines - currentLine = 0; - while (sidebarLine && currentLine <= lineNumbersShown) { - if (lineOffsets[currentLine] != null) { - sidebarLine.style.height = `${lineOffsets[currentLine]}px`; - sidebarLine.style.lineHeight = `${lineHeights[currentLine]}px`; - } - sidebarLine = sidebarLine.nextSibling; - currentLine++; - } - - if (newNumLines !== lineNumbersShown) { - const container = sideDivInner; - const odoc = outerWin.document; - const fragment = odoc.createDocumentFragment(); - - // Create missing line and apply height - while (lineNumbersShown < newNumLines) { - lineNumbersShown++; - const div = odoc.createElement('DIV'); - if (lineOffsets[currentLine]) { - div.style.height = `${lineOffsets[currentLine]}px`; - div.style.lineHeight = `${lineHeights[currentLine]}px`; - } - $(div).append($(`${String(lineNumbersShown)}`)); - fragment.appendChild(div); - currentLine++; - } - container.appendChild(fragment); - - // Remove extra lines - while (lineNumbersShown > newNumLines) { - container.removeChild(container.lastChild); - lineNumbersShown--; - } + while (sideDivInner.children.length < newNumLines) appendNewSideDivLine(); + while (sideDivInner.children.length > newNumLines) sideDivInner.lastElementChild.remove(); + for (const [i, sideDivLine] of Array.prototype.entries.call(sideDivInner.children)) { + sideDivLine.style.height = `${lineOffsets[i]}px`; + sideDivLine.style.lineHeight = `${lineHeights[i]}px`; } }; @@ -3794,19 +3513,16 @@ function Ace2Inner(editorInfo, cssManagers) { this.init = async () => { await $.ready; - doc = document; // defined as a var in scope outside inCallStack('setup', () => { - const body = doc.getElementById('innerdocbody'); - root = body; // defined as a var in scope outside - if (browser.firefox) $(root).addClass('mozilla'); - if (browser.safari) $(root).addClass('safari'); - root.classList.toggle('authorColors', true); - root.classList.toggle('doesWrap', doesWrap); + if (browser.firefox) $(document.body).addClass('mozilla'); + if (browser.safari) $(document.body).addClass('safari'); + document.body.classList.toggle('authorColors', true); + document.body.classList.toggle('doesWrap', doesWrap); enforceEditability(); // set up dom and rep - while (root.firstChild) root.removeChild(root.firstChild); + while (document.body.firstChild) document.body.removeChild(document.body.firstChild); const oneEntry = createDomLineEntry(''); doRepLineSplice(0, rep.lines.length(), [oneEntry]); insertDomLines(null, [oneEntry.domInfo]); diff --git a/src/static/js/admin/settings.js b/src/static/js/admin/settings.js index d7e089b1888..ada694f818d 100644 --- a/src/static/js/admin/settings.js +++ b/src/static/js/admin/settings.js @@ -1,9 +1,7 @@ 'use strict'; -/* global socketio */ - $(document).ready(() => { - const socket = socketio.connect('..', '/settings'); + const socket = window.socketio.connect('..', '/settings'); socket.on('connect', () => { socket.emit('load'); diff --git a/src/static/js/basic_error_handler.js b/src/static/js/basic_error_handler.js new file mode 100644 index 00000000000..ab400aa8a83 --- /dev/null +++ b/src/static/js/basic_error_handler.js @@ -0,0 +1,48 @@ +// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 + +/* Copyright 2021 Richard Hansen */ + +'use strict'; + +// Set up an error handler to display errors that happen during page load. This handler will be +// overridden with a nicer handler by setupGlobalExceptionHandler() in pad_utils.js. + +(() => { + const originalHandler = window.onerror; + window.onerror = (...args) => { + const [msg, url, line, col, err] = args; + + // Purge the existing HTML and styles for a consistent view. + document.body.textContent = ''; + for (const el of document.querySelectorAll('head style, head link[rel="stylesheet"]')) { + el.remove(); + } + + const box = document.body; + box.textContent = ''; + const summary = document.createElement('p'); + box.appendChild(summary); + summary.appendChild(document.createTextNode('An error occurred while loading the page:')); + const msgBlock = document.createElement('blockquote'); + box.appendChild(msgBlock); + msgBlock.style.fontWeight = 'bold'; + msgBlock.appendChild(document.createTextNode(msg)); + const loc = document.createElement('p'); + box.appendChild(loc); + loc.appendChild(document.createTextNode(`in ${url}`)); + loc.appendChild(document.createElement('br')); + loc.appendChild(document.createTextNode(`at line ${line}:${col}`)); + const stackSummary = document.createElement('p'); + box.appendChild(stackSummary); + stackSummary.appendChild(document.createTextNode('Stack trace:')); + const stackBlock = document.createElement('blockquote'); + box.appendChild(stackBlock); + const stack = document.createElement('pre'); + stackBlock.appendChild(stack); + stack.appendChild(document.createTextNode(err.stack || err.toString())); + + if (typeof originalHandler === 'function') originalHandler(...args); + }; +})(); + +// @license-end diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 909b6a08552..6ba2ef0ab55 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -164,10 +164,16 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro return true; // break } }); - // deal with someone is the author of a line and changes one character, - // so the alines won't change + // some chars are replaced (no attributes change and no length change) + // test if there are keep ops at the start of the cs if (lineChanged === undefined) { - lineChanged = Changeset.opIterator(Changeset.unpack(changeset).ops).next().lines; + lineChanged = 0; + const opIter = Changeset.opIterator(Changeset.unpack(changeset).ops); + + if (opIter.hasNext()) { + const op = opIter.next(); + if (op.opcode === '=') lineChanged += op.lines; + } } const goToLineNumber = (lineNumber) => { diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index 6a132247cf4..94bc5071b29 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -136,15 +136,6 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { // that includes old submittedChangeset toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool); } else { - // add forEach function to Array.prototype for IE8 - if (!('forEach' in Array.prototype)) { - Array.prototype.forEach = function (action, that /* opt*/) { - for (let i = 0, n = this.length; i < n; i++) { - if (i in this) action.call(that, this[i], i, this); - } - }; - } - // Get my authorID const authorId = parent.parent.pad.myUserInfo.userId; diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 1d16e75bf0d..63c17c153f7 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -15,12 +15,16 @@ * limitations under the License. */ +const ChatMessage = require('./ChatMessage'); const padutils = require('./pad_utils').padutils; const padcookie = require('./pad_cookie').padcookie; const Tinycon = require('tinycon/tinycon'); const hooks = require('./pluginfw/hooks'); const padeditor = require('./pad_editor').padeditor; +// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463 +const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + exports.chat = (() => { let isStuck = false; let userAndChat = false; @@ -99,25 +103,28 @@ exports.chat = (() => { } } }, - send() { + async send() { const text = $('#chatinput').val(); if (text.replace(/\s+/, '').length === 0) return; - this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', text}); + const message = new ChatMessage(text); + await hooks.aCallAll('chatSendMessage', Object.freeze({message})); + this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message}); $('#chatinput').val(''); }, - addMessage(msg, increment, isHistoryAdd) { + async addMessage(msg, increment, isHistoryAdd) { + msg = ChatMessage.fromObject(msg); // correct the time msg.time += this._pad.clientTimeOffset; - if (!msg.userId) { + if (!msg.authorId) { /* * If, for a bug or a database corruption, the message coming from the - * server does not contain the userId field (see for example #3731), + * server does not contain the authorId field (see for example #3731), * let's be defensive and replace it with "unknown". */ - msg.userId = 'unknown'; + msg.authorId = 'unknown'; console.warn( - 'The "userId" field of a chat message coming from the server was not present. ' + + 'The "authorId" field of a chat message coming from the server was not present. ' + 'Replacing with "unknown". This may be a bug or a database corruption.'); } @@ -128,9 +135,11 @@ exports.chat = (() => { // the hook args const ctx = { - authorName: msg.userName != null ? msg.userName : html10n.get('pad.userlist.unnamed'), - author: msg.userId, + authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'), + author: msg.authorId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), + message: msg, + rendered: null, sticky: false, timestamp: msg.time, timeStr: (() => { @@ -149,10 +158,11 @@ exports.chat = (() => { // does the user already have the chatbox open? const chatOpen = $('#chatbox').hasClass('visible'); - // does this message contain this user's name? (is the curretn user mentioned?) - const myName = $('#myusernameedit').val(); + // does this message contain this user's name? (is the current user mentioned?) const wasMentioned = - ctx.text.toLowerCase().indexOf(myName.toLowerCase()) !== -1 && myName !== 'undefined'; + msg.authorId !== window.clientVars.userId && + ctx.authorName !== html10n.get('pad.userlist.unnamed') && + normalize(ctx.text).includes(normalize(ctx.authorName)); // If the user was mentioned, make the message sticky if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { @@ -161,54 +171,49 @@ exports.chat = (() => { ctx.sticky = true; } - // Call chat message hook - hooks.aCallAll('chatNewMessage', ctx, () => { - const cls = authorClass(ctx.author); - const chatMsg = $('

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

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

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

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

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

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

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

').html(ctx.text).contents()); + text.each((i, e) => html10n.translateElement(html10n.translations, e)); + $.gritter.add({ + text, + sticky: ctx.sticky, + time: ctx.duration, + position: 'bottom', + class_name: 'chat-gritter-msg', + }); } - }); - - // Clear the chat mentions when the user clicks on the chat input box - $('#chatinput').click(() => { - chatMentions = 0; - Tinycon.setBubble(0); - }); + } if (!isHistoryAdd) this.scrollDown(); }, init(pad) { @@ -224,6 +229,11 @@ exports.chat = (() => { return false; } }); + // Clear the chat mentions when the user clicks on the chat input box + $('#chatinput').click(() => { + chatMentions = 0; + Tinycon.setBubble(0); + }); const self = this; $('body:not(#chatinput)').on('keypress', function (evt) { @@ -238,7 +248,7 @@ exports.chat = (() => { $('#chatinput').keypress((evt) => { // if the user typed enter, fire the send - if (evt.which === 13 || evt.which === 10) { + if (evt.key === 'Enter' && !evt.shiftKey) { evt.preventDefault(); this.send(); } diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index e507d6b06ec..849fff5fcba 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -245,14 +245,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } else if (msg.type === 'USER_NEWINFO') { const userInfo = msg.userInfo; const id = userInfo.userId; - - // Avoid a race condition when setting colors. If our color was set by a - // query param, ignore our own "new user" message's color value. - if (id === initialUserInfo.userId && initialUserInfo.globalUserColor) { - msg.userInfo.colorId = initialUserInfo.globalUserColor; - } - - if (userSet[id]) { userSet[id] = userInfo; callbacks.onUpdateUserInfo(userInfo); @@ -272,7 +264,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } else if (msg.type === 'CLIENT_MESSAGE') { callbacks.onClientMessage(msg.payload); } else if (msg.type === 'CHAT_MESSAGE') { - chat.addMessage(msg, true, false); + chat.addMessage(msg.message, true, false); } else if (msg.type === 'CHAT_MESSAGES') { for (let i = msg.messages.length - 1; i >= 0; i--) { chat.addMessage(msg.messages[i], true, true); diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index c02e7388748..54c6288048c 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -31,30 +31,7 @@ const Changeset = require('./Changeset'); const hooks = require('./pluginfw/hooks'); const sanitizeUnicode = (s) => UNorm.nfc(s); - -// This file is used both in browsers and with cheerio in Node.js (for importing HTML). Cheerio's -// Node-like objects are not 100% API compatible with the DOM specification; the following functions -// abstract away the differences. - -// .nodeType works with DOM and cheerio 0.22.0, but cheerio 0.22.0 does not provide the Node.*_NODE -// constants so they cannot be used here. -const isElementNode = (n) => n.nodeType === 1; // Node.ELEMENT_NODE -const isTextNode = (n) => n.nodeType === 3; // Node.TEXT_NODE -// .tagName works with DOM and cheerio 0.22.0, but: -// * With DOM, .tagName is an uppercase string. -// * With cheerio 0.22.0, .tagName is a lowercase string. -// For consistency, this function always returns a lowercase string. const tagName = (n) => n.tagName && n.tagName.toLowerCase(); -// .childNodes works with DOM and cheerio 0.22.0, except in cheerio the .childNodes property does -// not exist on text nodes (and maybe other non-element nodes). -const childNodes = (n) => n.childNodes || []; -const getAttribute = (n, a) => { - // .getAttribute() works with DOM but not with cheerio 0.22.0. - if (n.getAttribute != null) return n.getAttribute(a); - // .attribs[] works with cheerio 0.22.0 but not with DOM. - if (n.attribs != null) return n.attribs[a]; - return null; -}; // supportedElems are Supported natively within Etherpad and don't require a plugin const supportedElems = new Set([ 'author', @@ -115,9 +92,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) attribsBuilder = Changeset.smartOpAssembler(); }, textOfLine: (i) => textArray[i], - appendText: (txt, attrString) => { + appendText: (txt, attrString = '') => { textArray[textArray.length - 1] += txt; - // dmesg(txt+" / "+attrString); op.attribs = attrString; op.chars = txt.length; attribsBuilder.append(op); @@ -147,17 +123,13 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) let selEnd = [-1, -1]; const _isEmpty = (node, state) => { // consider clean blank lines pasted in IE to be empty - if (childNodes(node).length === 0) return true; - if (childNodes(node).length === 1 && + if (node.childNodes.length === 0) return true; + if (node.childNodes.length === 1 && getAssoc(node, 'shouldBeEmpty') && - // Note: The .innerHTML property exists on DOM Element objects but not on cheerio's - // Element-like objects (cheerio v0.22.0) so this equality check will always be false. - // Cheerio's Element-like objects have no equivalent to .innerHTML. (Cheerio objects have an - // .html() method, but that isn't accessible here.) node.innerHTML === ' ' && !getAssoc(node, 'unpasted')) { if (state) { - const child = childNodes(node)[0]; + const child = node.childNodes[0]; _reachPoint(child, 0, state); _reachPoint(child, 1, state); } @@ -177,7 +149,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) }; const _reachBlockPoint = (nd, idx, state) => { - if (!isTextNode(nd)) _reachPoint(nd, idx, state); + if (nd.nodeType !== nd.TEXT_NODE) _reachPoint(nd, idx, state); }; const _reachPoint = (nd, idx, state) => { @@ -349,8 +321,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) const startLine = lines.length() - 1; _reachBlockPoint(node, 0, state); - if (isTextNode(node)) { - const tname = getAttribute(node.parentNode, 'name'); + if (node.nodeType === node.TEXT_NODE) { + const tname = node.parentNode.getAttribute('name'); const context = {cc: this, state, tname, node, text: node.nodeValue}; // Hook functions may either return a string (deprecated) or modify context.text. If any hook // function modifies context.text then all returned strings are ignored. If no hook functions @@ -407,7 +379,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) cc.startNewLine(state); } } - } else if (isElementNode(node)) { + } else if (node.nodeType === node.ELEMENT_NODE) { const tname = tagName(node) || ''; if (tname === 'img') { @@ -426,7 +398,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) if (tname === 'br') { this.breakLine = true; - const tvalue = getAttribute(node, 'value'); + const tvalue = node.getAttribute('value'); const [startNewLine = true] = hooks.callAll('collectContentLineBreak', { cc: this, state, @@ -441,8 +413,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) } else if (tname === 'script' || tname === 'style') { // ignore } else if (!isEmpty) { - let styl = getAttribute(node, 'style'); - let cls = getAttribute(node, 'class'); + let styl = node.getAttribute('style'); + let cls = node.getAttribute('class'); let isPre = (tname === 'pre'); if ((!isPre) && abrowser && abrowser.safari) { isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); @@ -489,14 +461,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) cc.doAttrib(state, 'strikethrough'); } if (tname === 'ul' || tname === 'ol') { - let type = getAttribute(node, 'class'); + let type = node.getAttribute('class'); const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls); // lists do not need to have a type, so before we make a wrong guess // check if we find a better hint within the node's children if (!rr && !type) { - for (const child of childNodes(node)) { + for (const child of node.childNodes) { if (tagName(child) !== 'ul') continue; - type = getAttribute(child, 'class'); + type = child.getAttribute('class'); if (type) break; } } @@ -504,7 +476,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) type = rr[1]; } else { if (tname === 'ul') { - const cls = getAttribute(node, 'class'); + const cls = node.getAttribute('class'); if ((type && type.match('indent')) || (cls && cls.match('indent'))) { type = 'indent'; } else { @@ -576,7 +548,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) } } - for (const c of childNodes(node)) { + for (const c of node.childNodes) { cc.collectContent(c, state); } diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 324e135359f..af786b2dc40 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -61,6 +61,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { if (document) { result.node = document.createElement('div'); + // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. + result.node.setAttribute('aria-live', 'assertive'); } else { result.node = { innerHTML: '', @@ -224,7 +226,6 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { }; result.prepareForAdd = writeHTML; result.finishUpdate = writeHTML; - result.getInnerHTML = () => curHTML || ''; return result; }; diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 254168990c7..84668ea46eb 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -108,7 +108,7 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool let nextOp, nextOpClasses; const goNextOp = () => { - nextOp = attributionIter.next(); + nextOp = attributionIter.hasNext() ? attributionIter.next() : Changeset.newOp(); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); }; goNextOp(); @@ -131,7 +131,7 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool linestylefilter, text: txt, class: cls, - }, ' ', ' ', ''); + }); const disableAuthors = (disableAuthColorForThisLine == null || disableAuthColorForThisLine.length === 0) ? false : disableAuthColorForThisLine[0]; while (txt.length > 0) { diff --git a/src/static/js/pad.js b/src/static/js/pad.js index ec156eb46d5..306c2b191e4 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -48,8 +48,6 @@ const socketio = require('./socketio'); const hooks = require('./pluginfw/hooks'); -let receivedClientVars = false; - // This array represents all GET-parameters which can be used to change a setting. // name: the parameter-name, eg `?noColors=true` => `noColors` // checkVal: the callback is only executed when @@ -159,30 +157,17 @@ const getParams = () => { // Then URL applied stuff const params = getUrlVars(); - for (const setting of getParameters) { - const value = params[setting.name]; - + const value = params.get(setting.name); if (value && (value === setting.checkVal || setting.checkVal == null)) { setting.callback(value); } } }; -const getUrlVars = () => { - const vars = []; - let hash; - const hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); - for (let i = 0; i < hashes.length; i++) { - hash = hashes[i].split('='); - vars.push(hash[0]); - vars[hash[0]] = hash[1]; - } - return vars; -}; +const getUrlVars = () => new URL(window.location.href).searchParams; -const sendClientReady = (isReconnect, messageType) => { - messageType = typeof messageType !== 'undefined' ? messageType : 'CLIENT_READY'; +const sendClientReady = (isReconnect) => { let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); @@ -199,13 +184,23 @@ const sendClientReady = (isReconnect, messageType) => { Cookies.set('token', token, {expires: 60}); } + // If known, propagate the display name and color to the server in the CLIENT_READY message. This + // allows the server to include the values in its reply CLIENT_VARS message (which avoids + // initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the + // pad (which enables them to display a user join notification with the correct name). + const params = getUrlVars(); + const userInfo = { + colorId: params.get('userColor'), + name: params.get('userName'), + }; + const msg = { component: 'pad', - type: messageType, + type: 'CLIENT_READY', padId, sessionID: Cookies.get('sessionID'), token, - protocolVersion: 2, + userInfo, }; // this is a reconnect, lets tell the server our revisionnumber @@ -217,7 +212,8 @@ const sendClientReady = (isReconnect, messageType) => { socket.json.send(msg); }; -const handshake = () => { +const handshake = async () => { + let receivedClientVars = false; let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); @@ -297,63 +293,8 @@ const handshake = () => { } } } else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { - // if we haven't recieved the clientVars yet, then this message should it be receivedClientVars = true; - - // set some client vars window.clientVars = obj.data; - - // initialize the pad - pad._afterHandshake(); - - if (clientVars.readonly) { - chat.hide(); - $('#myusernameedit').attr('disabled', true); - $('#chatinput').attr('disabled', true); - $('#chaticon').hide(); - $('#options-chatandusers').parent().hide(); - $('#options-stickychat').parent().hide(); - } else if (!settings.hideChat) { $('#chaticon').show(); } - - $('body').addClass(clientVars.readonly ? 'readonly' : 'readwrite'); - - padeditor.ace.callWithAce((ace) => { - ace.ace_setEditable(!clientVars.readonly); - }); - - // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers - if (settings.LineNumbersDisabled === true) { - pad.changeViewOption('showLineNumbers', false); - } - - // If the noColors value is set to true then we need to - // hide the background colors on the ace spans - if (settings.noColors === true) { - pad.changeViewOption('noColors', true); - } - - if (settings.rtlIsTrue === true) { - pad.changeViewOption('rtlIsTrue', true); - } - - // If the Monospacefont value is set to true then change it to monospace. - if (settings.useMonospaceFontGlobal === true) { - pad.changeViewOption('padFontFamily', 'monospace'); - } - // if the globalUserName value is set we need to tell the server and - // the client about the new authorname - if (settings.globalUserName !== false) { - pad.notifyChangeName(settings.globalUserName); // Notifies the server - pad.myUserInfo.name = settings.globalUserName; - $('#myusernameedit').val(settings.globalUserName); // Updates the current users UI - } - if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { - // Add a 'globalUserColor' property to myUserInfo, - // so collabClient knows we have a query parameter. - pad.myUserInfo.globalUserColor = settings.globalUserColor; - pad.notifyChangeColor(settings.globalUserColor); // Updates pad.myUserInfo.colorId - paduserlist.setMyUserInfo(pad.myUserInfo); - } } else if (obj.disconnect) { padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); @@ -365,17 +306,49 @@ const handshake = () => { return; } else { - pad.collabClient.handleMessageFromServer(obj); + pad._messageQ.enqueue(obj); } }); - // Bind the colorpicker - $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); - // Bind the read only button - $('#readonlyinput').on('click', () => { - padeditbar.setEmbedLinks(); - }); + + await Promise.all([ + new Promise((resolve) => { + const h = (obj) => { + if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; + socket.off('message', h); + resolve(); + }; + socket.on('message', h); + }), + // This hook is only intended to be used by test code. If a plugin would like to use this hook, + // the hook must first be promoted to officially supported by deleting the leading underscore + // from the name, adding documentation to `doc/api/hooks_client-side.md`, and deleting this + // comment. + hooks.aCallAll('_socketCreated', {socket}), + ]); }; +/** Defers message handling until setCollabClient() is called with a non-null value. */ +class MessageQueue { + constructor() { + this._q = []; + this._cc = null; + } + + setCollabClient(cc) { + this._cc = cc; + this.enqueue(); // Flush. + } + + enqueue(...msgs) { + if (this._cc == null) { + this._q.push(...msgs); + } else { + while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift()); + for (const msg of msgs) this._cc.handleMessageFromServer(msg); + } + } +} + const pad = { // don't access these directly from outside this file, except // for debugging @@ -385,65 +358,34 @@ const pad = { initTime: 0, clientTimeOffset: null, padOptions: {}, + _messageQ: new MessageQueue(), // these don't require init; clientVars should all go through here getPadId: () => clientVars.padId, getClientIp: () => clientVars.clientIp, getColorPalette: () => clientVars.colorPalette, - getIsDebugEnabled: () => clientVars.debugEnabled, getPrivilege: (name) => clientVars.accountPrivs[name], getUserId: () => pad.myUserInfo.userId, getUserName: () => pad.myUserInfo.name, userList: () => paduserlist.users(), - switchToPad: (padId) => { - let newHref = new RegExp(/.*\/p\/[^/]+/).exec(document.location.pathname) || clientVars.padId; - newHref = newHref[0]; - - const options = clientVars.padOptions; - if (typeof options !== 'undefined' && options != null) { - const optionArr = []; - $.each(options, (k, v) => { - const str = `${k}=${v}`; - optionArr.push(str); - }); - const optionStr = optionArr.join('&'); - - newHref = `${newHref}?${optionStr}`; - } - - // destroy old pad from DOM - // See https://github.com/ether/etherpad-lite/pull/3915 - // TODO: Check if Destroying is enough and doesn't leave negative stuff - // See ace.js "editor.destroy" for a reference of how it was done before - $('#editorcontainer').find('iframe')[0].remove(); - - if (window.history && window.history.pushState) { - $('#chattext p').remove(); // clear the chat messages - window.history.pushState('', '', newHref); - receivedClientVars = false; - sendClientReady(false, 'SWITCH_TO_PAD'); - } else { - // fallback - window.location.href = newHref; - } - }, sendClientMessage: (msg) => { pad.collabClient.sendClientMessage(msg); }, - init: () => { + init() { padutils.setupGlobalExceptionHandler(); - $(document).ready(() => { - // start the custom js - if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef - handshake(); - - // To use etherpad you have to allow cookies. - // This will check if the prefs-cookie is set. - // Otherwise it shows up a message to the user. + // $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is + // an async function for some bizarre reason, so the async function is wrapped in a non-async + // function. + $(() => (async () => { + if (window.customStart != null) window.customStart(); + $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); + $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); padcookie.init(); - }); + await handshake(); + this._afterHandshake(); + })()); }, _afterHandshake() { pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp; @@ -502,7 +444,7 @@ const pad = { $('#editorcontainer').addClass('initialized'); - hooks.aCallAll('postAceInit', {ace: padeditor.ace, pad}); + hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars, pad}); }; // order of inits is important here: @@ -516,6 +458,7 @@ const pad = { pad.collabClient = getCollabClient( padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, {colorPalette: pad.getColorPalette()}, pad); + this._messageQ.setCollabClient(this.collabClient); pad.collabClient.setOnUserJoin(pad.handleUserJoin); pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); pad.collabClient.setOnUserLeave(pad.handleUserLeave); @@ -532,7 +475,57 @@ const pad = { // there are no messages $('#chatloadmessagesbutton').css('display', 'none'); } + + if (window.clientVars.readonly) { + chat.hide(); + $('#myusernameedit').attr('disabled', true); + $('#chatinput').attr('disabled', true); + $('#chaticon').hide(); + $('#options-chatandusers').parent().hide(); + $('#options-stickychat').parent().hide(); + } else if (!settings.hideChat) { $('#chaticon').show(); } + + $('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite'); + + padeditor.ace.callWithAce((ace) => { + ace.ace_setEditable(!window.clientVars.readonly); + }); + + // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers + if (settings.LineNumbersDisabled === true) { + this.changeViewOption('showLineNumbers', false); + } + + // If the noColors value is set to true then we need to + // hide the background colors on the ace spans + if (settings.noColors === true) { + this.changeViewOption('noColors', true); + } + + if (settings.rtlIsTrue === true) { + this.changeViewOption('rtlIsTrue', true); + } + + // If the Monospacefont value is set to true then change it to monospace. + if (settings.useMonospaceFontGlobal === true) { + this.changeViewOption('padFontFamily', 'RobotoMono'); + } + // if the globalUserName value is set we need to tell the server and + // the client about the new authorname + if (settings.globalUserName !== false) { + this.notifyChangeName(settings.globalUserName); // Notifies the server + this.myUserInfo.name = settings.globalUserName; + $('#myusernameedit').val(settings.globalUserName); // Updates the current users UI + } + if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { + // Add a 'globalUserColor' property to myUserInfo, + // so collabClient knows we have a query parameter. + this.myUserInfo.globalUserColor = settings.globalUserColor; + this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId + paduserlist.setMyUserInfo(this.myUserInfo); + } }, + dispose: () => { padeditor.dispose(); }, @@ -610,16 +603,6 @@ const pad = { pad.handleOptionsChange(opts); } }, - dmesg: (m) => { - if (pad.getIsDebugEnabled()) { - const djs = $('#djs').get(0); - const wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height()) >= -20); - $('#djs').append(`

${m}

`); - if (wasAtBottom) { - djs.scrollTop = djs.scrollHeight; - } - } - }, handleChannelStateChange: (newState, message) => { const oldFullyConnected = !!padconnectionstatus.isFullyConnected(); const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting'); @@ -760,7 +743,5 @@ exports.baseURL = ''; exports.settings = settings; exports.randomString = randomString; exports.getParams = getParams; -exports.getUrlVars = getUrlVars; -exports.handshake = handshake; exports.pad = pad; exports.init = init; diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 7305608fd5e..9d006d2c7c3 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -30,182 +30,184 @@ const padsavedrevs = require('./pad_savedrevs'); const _ = require('underscore'); require('./vendors/nice-select'); -const ToolbarItem = function (element) { - this.$el = element; -}; - -ToolbarItem.prototype.getCommand = function () { - return this.$el.attr('data-key'); -}; - -ToolbarItem.prototype.getValue = function () { - if (this.isSelect()) { - return this.$el.find('select').val(); +class ToolbarItem { + constructor(element) { + this.$el = element; } -}; -ToolbarItem.prototype.setValue = function (val) { - if (this.isSelect()) { - return this.$el.find('select').val(val); + getCommand() { + return this.$el.attr('data-key'); } -}; + getValue() { + if (this.isSelect()) { + return this.$el.find('select').val(); + } + } -ToolbarItem.prototype.getType = function () { - return this.$el.attr('data-type'); -}; + setValue(val) { + if (this.isSelect()) { + return this.$el.find('select').val(val); + } + } -ToolbarItem.prototype.isSelect = function () { - return this.getType() === 'select'; -}; + getType() { + return this.$el.attr('data-type'); + } -ToolbarItem.prototype.isButton = function () { - return this.getType() === 'button'; -}; + isSelect() { + return this.getType() === 'select'; + } -ToolbarItem.prototype.bind = function (callback) { - const self = this; + isButton() { + return this.getType() === 'button'; + } - if (self.isButton()) { - self.$el.click((event) => { - $(':focus').blur(); - callback(self.getCommand(), self); - event.preventDefault(); - }); - } else if (self.isSelect()) { - self.$el.find('select').change(() => { - callback(self.getCommand(), self); - }); + bind(callback) { + if (this.isButton()) { + this.$el.click((event) => { + $(':focus').blur(); + callback(this.getCommand(), this); + event.preventDefault(); + }); + } else if (this.isSelect()) { + this.$el.find('select').change(() => { + callback(this.getCommand(), this); + }); + } } -}; - - -const padeditbar = (function () { - const syncAnimationFn = () => { - const SYNCING = -100; - const DONE = 100; - let state = DONE; - const fps = 25; - const step = 1 / fps; - const T_START = -0.5; - const T_FADE = 1.0; - const T_GONE = 1.5; - const animator = padutils.makeAnimationScheduler(() => { - if (state === SYNCING || state === DONE) { - return false; - } else if (state >= T_GONE) { - state = DONE; +} + +const syncAnimation = (() => { + const SYNCING = -100; + const DONE = 100; + let state = DONE; + const fps = 25; + const step = 1 / fps; + const T_START = -0.5; + const T_FADE = 1.0; + const T_GONE = 1.5; + const animator = padutils.makeAnimationScheduler(() => { + if (state === SYNCING || state === DONE) { + return false; + } else if (state >= T_GONE) { + state = DONE; + $('#syncstatussyncing').css('display', 'none'); + $('#syncstatusdone').css('display', 'none'); + return false; + } else if (state < 0) { + state += step; + if (state >= 0) { $('#syncstatussyncing').css('display', 'none'); - $('#syncstatusdone').css('display', 'none'); - return false; - } else if (state < 0) { - state += step; - if (state >= 0) { - $('#syncstatussyncing').css('display', 'none'); - $('#syncstatusdone').css('display', 'block').css('opacity', 1); - } - return true; - } else { - state += step; - if (state >= T_FADE) { - $('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE)); - } - return true; + $('#syncstatusdone').css('display', 'block').css('opacity', 1); } - }, step * 1000); - return { - syncing: () => { - state = SYNCING; - $('#syncstatussyncing').css('display', 'block'); - $('#syncstatusdone').css('display', 'none'); - }, - done: () => { - state = T_START; - animator.scheduleAnimation(); - }, - }; + return true; + } else { + state += step; + if (state >= T_FADE) { + $('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE)); + } + return true; + } + }, step * 1000); + return { + syncing: () => { + state = SYNCING; + $('#syncstatussyncing').css('display', 'block'); + $('#syncstatusdone').css('display', 'none'); + }, + done: () => { + state = T_START; + animator.scheduleAnimation(); + }, }; - const syncAnimation = syncAnimationFn(); - - const self = { - init() { - const self = this; - self.dropdowns = []; - - $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE - this.enable(); - $('#editbar [data-key]').each(function () { - $(this).unbind('click'); - (new ToolbarItem($(this))).bind((command, item) => { - self.triggerCommand(command, item); - }); - }); +})(); - $('body:not(#editorcontainerbox)').on('keydown', (evt) => { - bodyKeyEvent(evt); - }); +exports.padeditbar = new class { + constructor() { + this._editbarPosition = 0; + this.commands = {}; + this.dropdowns = []; + } - $('.show-more-icon-btn').click(() => { - $('.toolbar').toggleClass('full-icons'); + init() { + $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE + this.enable(); + $('#editbar [data-key]').each((i, elt) => { + $(elt).unbind('click'); + new ToolbarItem($(elt)).bind((command, item) => { + this.triggerCommand(command, item); }); - self.checkAllIconsAreDisplayedInToolbar(); - $(window).resize(_.debounce(self.checkAllIconsAreDisplayedInToolbar, 100)); + }); - registerDefaultCommands(self); + $('body:not(#editorcontainerbox)').on('keydown', (evt) => { + this._bodyKeyEvent(evt); + }); - hooks.callAll('postToolbarInit', { - toolbar: self, - ace: padeditor.ace, - }); + $('.show-more-icon-btn').click(() => { + $('.toolbar').toggleClass('full-icons'); + }); + this.checkAllIconsAreDisplayedInToolbar(); + $(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100)); - /* - * On safari, the dropdown in the toolbar gets hidden because of toolbar - * overflow:hidden property. This is a bug from Safari: any children with - * position:fixed (like the dropdown) should be displayed no matter - * overflow:hidden on parent - */ - if (!browser.safari) { - $('select').niceSelect(); - } + this._registerDefaultCommands(); - // When editor is scrolled, we add a class to style the editbar differently - $('iframe[name="ace_outer"]').contents().scroll(function () { - $('#editbar').toggleClass('editor-scrolled', $(this).scrollTop() > 2); - }); - }, - isEnabled: () => true, - disable: () => { - $('#editbar').addClass('disabledtoolbar').removeClass('enabledtoolbar'); - }, - enable: () => { - $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); - }, - commands: {}, - registerCommand(cmd, callback) { - this.commands[cmd] = callback; - return this; - }, - registerDropdownCommand(cmd, dropdown) { - dropdown = dropdown || cmd; - self.dropdowns.push(dropdown); - this.registerCommand(cmd, () => { - self.toggleDropDown(dropdown); - }); - }, - registerAceCommand(cmd, callback) { - this.registerCommand(cmd, (cmd, ace, item) => { - ace.callWithAce((ace) => { - callback(cmd, ace, item); - }, cmd, true); - }); - }, - triggerCommand(cmd, item) { - if (self.isEnabled() && this.commands[cmd]) { - this.commands[cmd](cmd, padeditor.ace, item); - } - if (padeditor.ace) padeditor.ace.focus(); - }, - toggleDropDown: (moduleName, cb) => { + hooks.callAll('postToolbarInit', { + toolbar: this, + ace: padeditor.ace, + }); + + /* + * On safari, the dropdown in the toolbar gets hidden because of toolbar + * overflow:hidden property. This is a bug from Safari: any children with + * position:fixed (like the dropdown) should be displayed no matter + * overflow:hidden on parent + */ + if (!browser.safari) { + $('select').niceSelect(); + } + + // When editor is scrolled, we add a class to style the editbar differently + $('iframe[name="ace_outer"]').contents().scroll((ev) => { + $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); + }); + } + isEnabled() { return true; } + disable() { + $('#editbar').addClass('disabledtoolbar').removeClass('enabledtoolbar'); + } + enable() { + $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); + } + registerCommand(cmd, callback) { + this.commands[cmd] = callback; + return this; + } + registerDropdownCommand(cmd, dropdown) { + dropdown = dropdown || cmd; + this.dropdowns.push(dropdown); + this.registerCommand(cmd, () => { + this.toggleDropDown(dropdown); + }); + } + registerAceCommand(cmd, callback) { + this.registerCommand(cmd, (cmd, ace, item) => { + ace.callWithAce((ace) => { + callback(cmd, ace, item); + }, cmd, true); + }); + } + triggerCommand(cmd, item) { + if (this.isEnabled() && this.commands[cmd]) { + this.commands[cmd](cmd, padeditor.ace, item); + } + if (padeditor.ace) padeditor.ace.focus(); + } + + // cb is deprecated (this function is synchronous so a callback is unnecessary). + toggleDropDown(moduleName, cb = null) { + let cbErr = null; + try { // do nothing if users are sticked if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { return; @@ -216,10 +218,7 @@ const padeditbar = (function () { // hide all modules and remove highlighting of all buttons if (moduleName === 'none') { - const returned = false; - for (let i = 0; i < self.dropdowns.length; i++) { - const thisModuleName = self.dropdowns[i]; - + for (const thisModuleName of this.dropdowns) { // skip the userlist if (thisModuleName === 'users') continue; @@ -233,13 +232,10 @@ const padeditbar = (function () { module.removeClass('popup-show'); } } - - if (!returned && cb) return cb(); } else { // hide all modules that are not selected and remove highlighting // respectively add highlighting to the corresponding button - for (let i = 0; i < self.dropdowns.length; i++) { - const thisModuleName = self.dropdowns[i]; + for (const thisModuleName of this.dropdowns) { const module = $(`#${thisModuleName}`); if (module.hasClass('popup-show')) { @@ -248,77 +244,74 @@ const padeditbar = (function () { } else if (thisModuleName === moduleName) { $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); module.addClass('popup-show'); - if (cb) { - cb(); - } } } } - }, - setSyncStatus: (status) => { - if (status === 'syncing') { - syncAnimation.syncing(); - } else if (status === 'done') { - syncAnimation.done(); - } - }, - setEmbedLinks: () => { - const padUrl = window.location.href.split('?')[0]; - const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; - const props = 'width="100%" height="600" frameborder="0"'; - - if ($('#readonlyinput').is(':checked')) { - const urlParts = padUrl.split('/'); - urlParts.pop(); - const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`; - $('#embedinput') - .val(``); - $('#linkinput').val(readonlyLink); - } else { - $('#embedinput') - .val(``); - $('#linkinput').val(padUrl); - } - }, - checkAllIconsAreDisplayedInToolbar: () => { - // reset style - $('.toolbar').removeClass('cropped'); - $('body').removeClass('mobile-layout'); - const menu_left = $('.toolbar .menu_left')[0]; - - // this is approximate, we cannot measure it because on mobile - // Layout it takes the full width on the bottom of the page - const menuRightWidth = 280; - if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || - $('.toolbar').width() < 1000) { - $('body').addClass('mobile-layout'); - } - if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { - $('.toolbar').addClass('cropped'); - } - }, - }; - - let editbarPosition = 0; + } catch (err) { + cbErr = err || new Error(err); + } finally { + if (cb) Promise.resolve().then(() => cb(cbErr)); + } + } + setSyncStatus(status) { + if (status === 'syncing') { + syncAnimation.syncing(); + } else if (status === 'done') { + syncAnimation.done(); + } + } + setEmbedLinks() { + const padUrl = window.location.href.split('?')[0]; + const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; + const props = 'width="100%" height="600" frameborder="0"'; + + if ($('#readonlyinput').is(':checked')) { + const urlParts = padUrl.split('/'); + urlParts.pop(); + const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`; + $('#embedinput') + .val(``); + $('#linkinput').val(readonlyLink); + } else { + $('#embedinput') + .val(``); + $('#linkinput').val(padUrl); + } + } + checkAllIconsAreDisplayedInToolbar() { + // reset style + $('.toolbar').removeClass('cropped'); + $('body').removeClass('mobile-layout'); + const menuLeft = $('.toolbar .menu_left')[0]; + + // this is approximate, we cannot measure it because on mobile + // Layout it takes the full width on the bottom of the page + const menuRightWidth = 280; + if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width() - menuRightWidth || + $('.toolbar').width() < 1000) { + $('body').addClass('mobile-layout'); + } + if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()) { + $('.toolbar').addClass('cropped'); + } + } - const bodyKeyEvent = (evt) => { + _bodyKeyEvent(evt) { // If the event is Alt F9 or Escape & we're already in the editbar menu // Send the users focus back to the pad if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) { if ($(':focus').parents('.toolbar').length === 1) { // If we're in the editbar already.. // Close any dropdowns we have open.. - padeditbar.toggleDropDown('none'); + this.toggleDropDown('none'); + // Shift focus away from any drop downs + $(':focus').blur(); // required to do not try to remove! // Check we're on a pad and not on the timeslider // Or some other window I haven't thought about! if (typeof pad === 'undefined') { // Timeslider probably.. - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! $('#editorcontainerbox').focus(); // Focus back onto the pad } else { - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! padeditor.ace.focus(); // Sends focus back to pad // The above focus doesn't always work in FF, you have to hit enter afterwards evt.preventDefault(); @@ -327,7 +320,7 @@ const padeditbar = (function () { // Focus on the editbar :) const firstEditbarElement = parent.parent.$('#editbar button').first(); - $(this).blur(); + $(evt.currentTarget).blur(); firstEditbarElement.focus(); evt.preventDefault(); } @@ -345,10 +338,10 @@ const padeditbar = (function () { // If a dropdown is visible or we're in an input don't move to the next button if ($('.popup').is(':visible') || evt.target.localName === 'input') return; - editbarPosition--; + this._editbarPosition--; // Allow focus to shift back to end of row and start of row - if (editbarPosition === -1) editbarPosition = focusItems.length - 1; - $(focusItems[editbarPosition]).focus(); + if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1; + $(focusItems[this._editbarPosition]).focus(); } // On right arrow move to next button in editbar @@ -356,97 +349,92 @@ const padeditbar = (function () { // If a dropdown is visible or we're in an input don't move to the next button if ($('.popup').is(':visible') || evt.target.localName === 'input') return; - editbarPosition++; + this._editbarPosition++; // Allow focus to shift back to end of row and start of row - if (editbarPosition >= focusItems.length) editbarPosition = 0; - $(focusItems[editbarPosition]).focus(); + if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0; + $(focusItems[this._editbarPosition]).focus(); } } - }; - - const aceAttributeCommand = (cmd, ace) => { - ace.ace_toggleAttributeOnSelection(cmd); - }; + } - const registerDefaultCommands = (toolbar) => { - toolbar.registerDropdownCommand('showusers', 'users'); - toolbar.registerDropdownCommand('settings'); - toolbar.registerDropdownCommand('connectivity'); - toolbar.registerDropdownCommand('import_export'); - toolbar.registerDropdownCommand('embed'); + _registerDefaultCommands() { + this.registerDropdownCommand('showusers', 'users'); + this.registerDropdownCommand('settings'); + this.registerDropdownCommand('connectivity'); + this.registerDropdownCommand('import_export'); + this.registerDropdownCommand('embed'); - toolbar.registerCommand('settings', () => { - toolbar.toggleDropDown('settings', () => { - $('#options-stickychat').focus(); - }); + this.registerCommand('settings', () => { + this.toggleDropDown('settings'); + $('#options-stickychat').focus(); }); - toolbar.registerCommand('import_export', () => { - toolbar.toggleDropDown('import_export', () => { - // If Import file input exists then focus on it.. - if ($('#importfileinput').length !== 0) { - setTimeout(() => { - $('#importfileinput').focus(); - }, 100); - } else { - $('.exportlink').first().focus(); - } - }); + this.registerCommand('import_export', () => { + this.toggleDropDown('import_export'); + // If Import file input exists then focus on it.. + if ($('#importfileinput').length !== 0) { + setTimeout(() => { + $('#importfileinput').focus(); + }, 100); + } else { + $('.exportlink').first().focus(); + } }); - toolbar.registerCommand('showusers', () => { - toolbar.toggleDropDown('users', () => { - $('#myusernameedit').focus(); - }); + this.registerCommand('showusers', () => { + this.toggleDropDown('users'); + $('#myusernameedit').focus(); }); - toolbar.registerCommand('embed', () => { - toolbar.setEmbedLinks(); - toolbar.toggleDropDown('embed', () => { - $('#linkinput').focus().select(); - }); + this.registerCommand('embed', () => { + this.setEmbedLinks(); + this.toggleDropDown('embed'); + $('#linkinput').focus().select(); }); - toolbar.registerCommand('savedRevision', () => { + this.registerCommand('savedRevision', () => { padsavedrevs.saveNow(); }); - toolbar.registerCommand('showTimeSlider', () => { + this.registerCommand('showTimeSlider', () => { document.location = `${document.location.pathname}/timeslider`; }); - toolbar.registerAceCommand('bold', aceAttributeCommand); - toolbar.registerAceCommand('italic', aceAttributeCommand); - toolbar.registerAceCommand('underline', aceAttributeCommand); - toolbar.registerAceCommand('strikethrough', aceAttributeCommand); + const aceAttributeCommand = (cmd, ace) => { + ace.ace_toggleAttributeOnSelection(cmd); + }; + this.registerAceCommand('bold', aceAttributeCommand); + this.registerAceCommand('italic', aceAttributeCommand); + this.registerAceCommand('underline', aceAttributeCommand); + this.registerAceCommand('strikethrough', aceAttributeCommand); - toolbar.registerAceCommand('undo', (cmd, ace) => { + this.registerAceCommand('undo', (cmd, ace) => { ace.ace_doUndoRedo(cmd); }); - toolbar.registerAceCommand('redo', (cmd, ace) => { + this.registerAceCommand('redo', (cmd, ace) => { ace.ace_doUndoRedo(cmd); }); - toolbar.registerAceCommand('insertunorderedlist', (cmd, ace) => { + this.registerAceCommand('insertunorderedlist', (cmd, ace) => { ace.ace_doInsertUnorderedList(); }); - toolbar.registerAceCommand('insertorderedlist', (cmd, ace) => { + this.registerAceCommand('insertorderedlist', (cmd, ace) => { ace.ace_doInsertOrderedList(); }); - toolbar.registerAceCommand('indent', (cmd, ace) => { + this.registerAceCommand('indent', (cmd, ace) => { if (!ace.ace_doIndentOutdent(false)) { ace.ace_doInsertUnorderedList(); } }); - toolbar.registerAceCommand('outdent', (cmd, ace) => { + this.registerAceCommand('outdent', (cmd, ace) => { ace.ace_doIndentOutdent(true); }); - toolbar.registerAceCommand('clearauthorship', (cmd, ace) => { + this.registerAceCommand('clearauthorship', (cmd, ace) => { // If we have the whole document selected IE control A has been hit const rep = ace.ace_getRep(); let doPrompt = false; @@ -459,13 +447,13 @@ const padeditbar = (function () { } } /* - * NOTICE: This command isn't fired on Control Shift C. - * I intentionally didn't create duplicate code because if you are hitting - * Control Shift C we make the assumption you are a "power user" - * and as such we assume you don't need the prompt to bug you each time! - * This does make wonder if it's worth having a checkbox to avoid being - * prompted again but that's probably overkill for this contribution. - */ + * NOTICE: This command isn't fired on Control Shift C. + * I intentionally didn't create duplicate code because if you are hitting + * Control Shift C we make the assumption you are a "power user" + * and as such we assume you don't need the prompt to bug you each time! + * This does make wonder if it's worth having a checkbox to avoid being + * prompted again but that's probably overkill for this contribution. + */ // if we don't have any text selected, we have a caret or we have already said to prompt if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) { @@ -479,19 +467,15 @@ const padeditbar = (function () { } }); - toolbar.registerCommand('timeslider_returnToPad', (cmd) => { + this.registerCommand('timeslider_returnToPad', (cmd) => { if (document.referrer.length > 0 && - document.referrer.substring(document.referrer.lastIndexOf('/') - 1, - document.referrer.lastIndexOf('/')) === 'p') { + document.referrer.substring(document.referrer.lastIndexOf('/') - 1, + document.referrer.lastIndexOf('/')) === 'p') { document.location = document.referrer; } else { document.location = document.location.href .substring(0, document.location.href.lastIndexOf('/')); } }); - }; - - return self; -}()); - -exports.padeditbar = padeditbar; + } +}(); diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index d8c3ae5ac2f..e39f73fee99 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -49,9 +49,6 @@ const padeditor = (() => { }); exports.focusOnLine(self.ace); self.ace.setProperty('wraps', true); - if (pad.getIsDebugEnabled()) { - self.ace.setProperty('dmesg', pad.dmesg); - } self.initViewOptions(); self.setViewOptions(initialViewOptions); // view bar diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index 8689b5b356c..c85a791eb17 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -67,7 +67,7 @@ const padimpexp = (() => { importErrorMessage(message); } else { $('#import_export').removeClass('popup-show'); - if (directDatabaseAccess) pad.switchToPad(clientVars.padId); + if (directDatabaseAccess) window.location.reload(); } $('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton')); window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0); diff --git a/src/static/js/pad_modals.js b/src/static/js/pad_modals.js index 99d4dd6ac4f..54bd838772d 100644 --- a/src/static/js/pad_modals.js +++ b/src/static/js/pad_modals.js @@ -32,15 +32,14 @@ const padmodals = (() => { pad = _pad; }, showModal: (messageId) => { - padeditbar.toggleDropDown('none', () => { - $('#connectivity .visible').removeClass('visible'); - $(`#connectivity .${messageId}`).addClass('visible'); + padeditbar.toggleDropDown('none'); + $('#connectivity .visible').removeClass('visible'); + $(`#connectivity .${messageId}`).addClass('visible'); - const $modal = $(`#connectivity .${messageId}`); - automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); + const $modal = $(`#connectivity .${messageId}`); + automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); - padeditbar.toggleDropDown('connectivity'); - }); + padeditbar.toggleDropDown('connectivity'); }, showOverlay: () => { // Prevent the user to interact with the toolbar. Useful when user is disconnected for example diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 48f0624e9d8..9bea959da88 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -60,10 +60,10 @@ const wordCharRegex = new RegExp(`[${[ const urlRegex = (() => { // TODO: wordCharRegex matches many characters that are not permitted in URIs. Are they included // here as an attempt to support IRIs? (See https://tools.ietf.org/html/rfc3987.) - const urlChar = `[-:@_.,~%+/?=&#!;()$'*${wordCharRegex.source.slice(1, -1)}]`; + const urlChar = `[-:@_.,~%+/?=&#!;()\\[\\]$'*${wordCharRegex.source.slice(1, -1)}]`; // Matches a single character that should not be considered part of the URL if it is the last // character that matches urlChar. - const postUrlPunct = '[:.,;?!)\'*]'; + const postUrlPunct = '[:.,;?!)\\]\'*]'; // Schemes that must be followed by :// const withAuth = `(?:${[ '(?:x-)?man', @@ -89,6 +89,25 @@ const urlRegex = (() => { })(); const padutils = { + /** + * Prints a warning message followed by a stack trace (to make it easier to figure out what code + * is using the deprecated function). + * + * Most browsers include UI widget to examine the stack at the time of the warning, but this + * includes the stack in the log message for a couple of reasons: + * - This makes it possible to see the stack if the code runs in Node.js. + * - Users are more likely to paste the stack in bug reports they might file. + * + * @param {...*} args - Passed to `console.warn`, with a stack trace appended. + */ + warnWithStack: (...args) => { + const err = new Error(); + if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnWithStack); + err.name = ''; + if (err.stack) args.push(err.stack); + console.warn(...args); + }, + escapeHtml: (x) => Security.escapeHTML(String(x)), uniqueId: () => { const pad = require('./pad').pad; // Sidestep circular dependency @@ -296,6 +315,7 @@ const padutils = { let globalExceptionHandler = null; padutils.setupGlobalExceptionHandler = () => { if (globalExceptionHandler == null) { + require('./vendors/gritter'); globalExceptionHandler = (e) => { let type; let err; @@ -382,17 +402,18 @@ const inThirdPartyIframe = () => { // This file is included from Node so that it can reuse randomString, but Node doesn't have a global // window object. if (typeof window !== 'undefined') { - exports.Cookies = require('js-cookie/src/js.cookie'); - // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case - // use `SameSite=None`. For iframes from another site, only `None` has a chance of working - // because the cookies are third-party (not same-site). Many browsers/users block third-party - // cookies, but maybe blocked is better than definitely blocked (which would happen with `Lax` - // or `Strict`). Note: `None` will not work unless secure is true. - // - // `Strict` is not used because it has few security benefits but significant usability drawbacks - // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. - exports.Cookies.defaults.sameSite = inThirdPartyIframe() ? 'None' : 'Lax'; - exports.Cookies.defaults.secure = window.location.protocol === 'https:'; + exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({ + // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case + // use `SameSite=None`. For iframes from another site, only `None` has a chance of working + // because the cookies are third-party (not same-site). Many browsers/users block third-party + // cookies, but maybe blocked is better than definitely blocked (which would happen with `Lax` + // or `Strict`). Note: `None` will not work unless secure is true. + // + // `Strict` is not used because it has few security benefits but significant usability drawbacks + // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. + sameSite: inThirdPartyIframe() ? 'None' : 'Lax', + secure: window.location.protocol === 'https:', + }); } exports.randomString = randomString; exports.padutils = padutils; diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 74fbbafc8c7..a4a2df40dfe 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -72,19 +72,6 @@ exports.formatHooks = (hookSetName, html) => { return lines.join('\n'); }; -const callInit = async () => { - await Promise.all(Object.keys(defs.plugins).map(async (pluginName) => { - const plugin = defs.plugins[pluginName]; - const epInit = path.join(plugin.package.path, '.ep_initialized'); - try { - await fs.stat(epInit); - } catch (err) { - await fs.writeFile(epInit, 'done'); - await hooks.aCallAll(`init_${pluginName}`, {}); - } - })); -}; - exports.pathNormalization = (part, hookFnName, hookName) => { const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. @@ -111,7 +98,7 @@ exports.update = async () => { defs.parts = sortParts(parts); defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); defs.loaded = true; - await callInit(); + await Promise.all(Object.keys(defs.plugins).map((p) => hooks.aCallAll(`init_${p}`, {}))); }; exports.getPackages = async () => { diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.js index aeb67733607..f10a4e7a8ce 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.js @@ -291,7 +291,6 @@ class SkipList { if (end < 0) end = 0; if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size; - window.dmesg(String([start, end, this._keyToNodeMap.size])); if (end <= start) return []; let n = this.atIndex(start); const array = [n]; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 47246b4a1b4..7268f95f021 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -29,11 +29,13 @@ require('./vendors/jquery'); const Cookies = require('./pad_utils').Cookies; const randomString = require('./pad_utils').randomString; const hooks = require('./pluginfw/hooks'); +const padutils = require('./pad_utils').padutils; const socketio = require('./socketio'); let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider; const init = () => { + padutils.setupGlobalExceptionHandler(); $(document).ready(() => { // start the custom js if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef @@ -100,7 +102,6 @@ const sendSocketMsg = (type, data) => { padId, token, sessionID: Cookies.get('sessionID'), - protocolVersion: 2, }); }; diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.js index b8270b805aa..d0b83419dd9 100644 --- a/src/static/js/undomodule.js +++ b/src/static/js/undomodule.js @@ -55,7 +55,6 @@ const undoModule = (() => { e.elementType = UNDOABLE_EVENT; stackElements.push(e); numUndoableEvents++; - // dmesg("pushEvent backset: "+event.backset); }; const pushExternalChange = (cs) => { @@ -207,7 +206,6 @@ const undoModule = (() => { const merge = _mergeChangesets(event.backset, topEvent.backset); if (merge) { topEvent.backset = merge; - // dmesg("reportEvent merge: "+merge); applySelectionToTop(); merged = true; } diff --git a/src/templates/pad.html b/src/templates/pad.html index d7c3c082309..7ff447dc938 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -5,11 +5,10 @@ ; %> - -<% e.begin_block("htmlHead"); %> -<% e.end_block(); %> - + + <% e.begin_block("htmlHead"); %> + <% e.end_block(); %> <%=settings.title%> + @@ -54,9 +54,8 @@ - - - + + <% e.begin_block("body"); %> @@ -388,7 +387,7 @@

- +
@@ -443,27 +442,6 @@

Skin Builder

<% e.begin_block("scripts"); %> - - - @@ -500,6 +478,10 @@

Skin Builder

plugins.baseURL = baseURL; plugins.update(function () { + // Mechanism for tests to register hook functions (install fake plugins). + window._postPluginUpdateForTestingDone = false; + if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting(); + window._postPluginUpdateForTestingDone = true; // Call documentReady hook $(function() { hooks.aCallAll('documentReady'); @@ -522,4 +504,5 @@

Skin Builder

<% e.end_block(); %> + diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index fe43668c8c8..dc351b1d0c5 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -4,31 +4,32 @@ %> -<%=settings.title%> Timeslider - + <%=settings.title%> Timeslider + + diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index aabc48a54d5..793828ac7be 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -1,9 +1,11 @@ 'use strict'; const apiHandler = require('../../node/handler/APIHandler'); +const io = require('socket.io-client'); const log4js = require('log4js'); const process = require('process'); const server = require('../../node/server'); +const setCookieParser = require('set-cookie-parser'); const settings = require('../../node/utils/Settings'); const supertest = require('supertest'); const webaccess = require('../../node/hooks/express/webaccess'); @@ -17,7 +19,8 @@ exports.baseUrl = null; exports.httpServer = null; exports.logger = log4js.getLogger('test'); -const logLevel = exports.logger.level; +const logger = exports.logger; +const logLevel = logger.level; // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // https://github.com/mochajs/mocha/issues/2640 @@ -34,10 +37,10 @@ exports.init = async function () { agentPromise = new Promise((resolve) => { agentResolve = resolve; }); if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) { - exports.logger.warn('Disabling non-test logging for the duration of the test. ' + - 'To enable non-test logging, change the loglevel setting to DEBUG.'); + logger.warn('Disabling non-test logging for the duration of the test. ' + + 'To enable non-test logging, change the loglevel setting to DEBUG.'); log4js.setGlobalLogLevel(log4js.levels.OFF); - exports.logger.setLevel(logLevel); + logger.setLevel(logLevel); } // Note: This is only a shallow backup. @@ -49,7 +52,7 @@ exports.init = async function () { settings.commitRateLimiting = {duration: 0.001, points: 1e6}; exports.httpServer = await server.start(); exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`; - exports.logger.debug(`HTTP server at ${exports.baseUrl}`); + logger.debug(`HTTP server at ${exports.baseUrl}`); // Create a supertest user agent for the HTTP server. exports.agent = supertest(exports.baseUrl); // Speed up authn tests. @@ -67,3 +70,117 @@ exports.init = async function () { agentResolve(exports.agent); return exports.agent; }; + +/** + * Waits for the next named socket.io event. Rejects if there is an error event while waiting + * (unless waiting for that error event). + * + * @param {io.Socket} socket - The socket.io Socket object to listen on. + * @param {string} event - The socket.io Socket event to listen for. + * @returns The argument(s) passed to the event handler. + */ +exports.waitForSocketEvent = async (socket, event) => { + const errorEvents = [ + 'error', + 'connect_error', + 'connect_timeout', + 'reconnect_error', + 'reconnect_failed', + ]; + const handlers = new Map(); + let cancelTimeout; + try { + const timeoutP = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`timed out waiting for ${event} event`)); + cancelTimeout = () => {}; + }, 1000); + cancelTimeout = () => { + clearTimeout(timeout); + resolve(); + cancelTimeout = () => {}; + }; + }); + const errorEventP = Promise.race(errorEvents.map((event) => new Promise((resolve, reject) => { + handlers.set(event, (errorString) => { + logger.debug(`socket.io ${event} event: ${errorString}`); + reject(new Error(errorString)); + }); + }))); + const eventP = new Promise((resolve) => { + // This will overwrite one of the above handlers if the user is waiting for an error event. + handlers.set(event, (...args) => { + logger.debug(`socket.io ${event} event`); + if (args.length > 1) return resolve(args); + resolve(args[0]); + }); + }); + for (const [event, handler] of handlers) socket.on(event, handler); + // timeoutP and errorEventP are guaranteed to never resolve here (they can only reject), so the + // Promise returned by Promise.race() is guaranteed to resolve to the eventP value (if + // the event arrives). + return await Promise.race([timeoutP, errorEventP, eventP]); + } finally { + cancelTimeout(); + for (const [event, handler] of handlers) socket.off(event, handler); + } +}; + +/** + * Establishes a new socket.io connection. + * + * @param {object} [res] - Optional HTTP response object. The cookies from this response's + * `set-cookie` header(s) are passed to the server when opening the socket.io connection. If + * nullish, no cookies are passed to the server. + * @returns {io.Socket} A socket.io client Socket object. + */ +exports.connect = async (res = null) => { + // Convert the `set-cookie` header(s) into a `cookie` header. + const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); + const reqCookieHdr = Object.entries(resCookies).map( + ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); + + logger.debug('socket.io connecting...'); + let padId = null; + if (res) { + padId = res.req.path.split('/p/')[1]; + } + const socket = io(`${exports.baseUrl}/`, { + forceNew: true, // Different tests will have different query parameters. + path: '/socket.io', + // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the + // express_sid cookie must be passed as a query parameter. + query: {cookie: reqCookieHdr, padId}, + }); + try { + await exports.waitForSocketEvent(socket, 'connect'); + } catch (e) { + socket.close(); + throw e; + } + logger.debug('socket.io connected'); + + return socket; +}; + +/** + * Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad. + * + * @param {io.Socket} socket - Connected socket.io Socket object. + * @param {string} padId - Which pad to join. + * @returns The CLIENT_VARS message from the server. + */ +exports.handshake = async (socket, padId) => { + logger.debug('sending CLIENT_READY...'); + socket.send({ + component: 'pad', + type: 'CLIENT_READY', + padId, + sessionID: null, + token: 't.12345', + }); + logger.debug('waiting for CLIENT_VARS response...'); + const msg = await exports.waitForSocketEvent(socket, 'message'); + logger.debug('received CLIENT_VARS message'); + return msg; +}; diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.js index 3080425ec31..2e579136ba2 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.js @@ -6,8 +6,10 @@ * TODO: maybe unify those two files and merge in a single one. */ +const assert = require('assert').strict; const common = require('../../common'); const fs = require('fs'); +const fsp = fs.promises; let agent; const apiKey = common.apiKey; @@ -19,80 +21,52 @@ const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?api describe(__filename, function () { before(async function () { agent = await common.init(); }); - describe('Connectivity For Character Encoding', function () { - it('can connect', function (done) { - this.timeout(250); - agent.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done); + describe('Sanity checks', function () { + it('can connect', async function () { + await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); }); - }); - describe('API Versioning', function () { - this.timeout(150); - it('finds the version tag', function (done) { - agent.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .expect(200, done); + it('finds the version tag', async function () { + const res = await agent.get('/api/') + .expect(200); + apiVersion = res.body.currentVersion; + assert(apiVersion); }); - }); - describe('Permission', function () { - it('errors with invalid APIKey', function (done) { - this.timeout(150); + it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway - const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; - agent.get(permErrorURL) - .expect(401, done); + await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`) + .expect(401); }); }); - describe('createPad', function () { - it('creates a new Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + describe('Tests', function () { + it('creates a new Pad', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('setHTML', function () { - it('Sets the HTML of a Pad attempting to weird utf8 encoded content', function (done) { - this.timeout(1000); - fs.readFile('tests/backend/specs/api/emojis.html', 'utf8', (err, html) => { - agent.post(endPoint('setHTML')) - .send({ - padID: testPadId, - html, - }) - .expect((res) => { - if (res.body.code !== 0) throw new Error("Can't set HTML properly"); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); + it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () { + const res = await agent.post(endPoint('setHTML')) + .send({ + padID: testPadId, + html: await fsp.readFile('tests/backend/specs/api/emojis.html', 'utf8'), + }) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getHTML', function () { - it('get the HTML of Pad with emojis', function (done) { - this.timeout(400); - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.html.indexOf('🇼') === -1) { - throw new Error('Unable to get the HTML'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of Pad with emojis', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.match(res.body.data.html, /🇼/); }); }); }); diff --git a/src/tests/backend/specs/api/chat.js b/src/tests/backend/specs/api/chat.js index 17f5bf4ef22..fcc69a3635d 100644 --- a/src/tests/backend/specs/api/chat.js +++ b/src/tests/backend/specs/api/chat.js @@ -39,7 +39,6 @@ describe(__filename, function () { */ describe('createPad', function () { - this.timeout(400); it('creates a new Pad', function (done) { agent.get(`${endPoint('createPad')}&padID=${padID}`) .expect((res) => { @@ -51,7 +50,6 @@ describe(__filename, function () { }); describe('createAuthor', function () { - this.timeout(100); it('Creates an author with a name set', function (done) { agent.get(endPoint('createAuthor')) .expect((res) => { @@ -66,7 +64,6 @@ describe(__filename, function () { }); describe('appendChatMessage', function () { - this.timeout(100); it('Adds a chat message to the pad', function (done) { agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + `&authorID=${authorID}&time=${timestamp}`) @@ -80,7 +77,6 @@ describe(__filename, function () { describe('getChatHead', function () { - this.timeout(100); it('Gets the head of chat', function (done) { agent.get(`${endPoint('getChatHead')}&padID=${padID}`) .expect((res) => { @@ -94,7 +90,6 @@ describe(__filename, function () { }); describe('getChatHistory', function () { - this.timeout(40); it('Gets Chat History of a Pad', function (done) { agent.get(`${endPoint('getChatHistory')}&padID=${padID}`) .expect((res) => { diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index a68ba40110e..584341cc03a 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -31,7 +31,6 @@ describe(__filename, function () { describe('Connectivity', function () { it('can connect', async function () { - this.timeout(250); await agent.get('/api/') .expect(200) .expect('Content-Type', /json/); @@ -40,7 +39,6 @@ describe(__filename, function () { describe('API Versioning', function () { it('finds the version tag', async function () { - this.timeout(250); await agent.get('/api/') .expect(200) .expect((res) => assert(res.body.currentVersion)); @@ -96,7 +94,6 @@ describe(__filename, function () { }); it('creates a new Pad, imports content to it, checks that content', async function () { - this.timeout(500); await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/) @@ -109,28 +106,80 @@ describe(__filename, function () { .expect((res) => assert.equal(res.body.data.text, padText.toString())); }); - for (const authn of [false, true]) { - it(`can export from read-only pad ID, authn ${authn}`, async function () { - this.timeout(250); - settings.requireAuthentication = authn; - const get = (ep) => { - let req = agent.get(ep); - if (authn) req = req.auth('user', 'user-password'); - return req.expect(200); - }; - const ro = await get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) - .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); - const readOnlyId = JSON.parse(ro.text).data.readOnlyID; - await get(`/p/${readOnlyId}/export/html`) - .expect((res) => assert(res.text.indexOf('This is the') !== -1)); - await get(`/p/${readOnlyId}/export/txt`) - .expect((res) => assert(res.text.indexOf('This is the') !== -1)); + describe('export from read-only pad ID', function () { + let readOnlyId; + + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + if (readOnlyId != null) return; + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => assert.equal(res.body.code, 0)); + readOnlyId = res.body.data.readOnlyID; }); - } - describe('Import/Export tests requiring AbiWord/LibreOffice', function () { - this.timeout(10000); + for (const authn of [false, true]) { + describe(`requireAuthentication = ${authn}`, function () { + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + settings.requireAuthentication = authn; + }); + + for (const exportType of ['html', 'txt', 'etherpad']) { + describe(`export to ${exportType}`, function () { + let text; + + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + if (text != null) return; + let req = agent.get(`/p/${readOnlyId}/export/${exportType}`); + if (authn) req = req.auth('user', 'user-password'); + const res = await req + .expect(200) + .buffer(true).parse(superagent.parse.text); + text = res.text; + }); + + it('export OK', async function () { + assert.match(text, /This is the/); + }); + + it('writable pad ID is not leaked', async function () { + assert(!text.includes(testPadId)); + }); + + it('re-import to read-only pad ID gives 403 forbidden', async function () { + let req = agent.post(`/p/${readOnlyId}/import`) + .attach('file', Buffer.from(text), { + filename: `/test.${exportType}`, + contentType: 'text/plain', + }); + if (authn) req = req.auth('user', 'user-password'); + await req.expect(403); + }); + + it('re-import to read-write pad ID gives 200 OK', async function () { + // The new pad ID must differ from testPadId because Etherpad refuses to import + // .etherpad files on top of a pad that already has edits. + let req = agent.post(`/p/${testPadId}_import/import`) + .attach('file', Buffer.from(text), { + filename: `/test.${exportType}`, + contentType: 'text/plain', + }); + if (authn) req = req.auth('user', 'user-password'); + await req.expect(200); + }); + }); + } + }); + } + }); + describe('Import/Export tests requiring AbiWord/LibreOffice', function () { before(async function () { if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { diff --git a/src/tests/backend/specs/api/instance.js b/src/tests/backend/specs/api/instance.js index 64a9ea5e047..f0180e203df 100644 --- a/src/tests/backend/specs/api/instance.js +++ b/src/tests/backend/specs/api/instance.js @@ -18,7 +18,6 @@ describe(__filename, function () { describe('Connectivity for instance-level API tests', function () { it('can connect', function (done) { - this.timeout(150); agent.get('/api/') .expect('Content-Type', /json/) .expect(200, done); @@ -27,7 +26,6 @@ describe(__filename, function () { describe('getStats', function () { it('Gets the stats of a running instance', function (done) { - this.timeout(100); agent.get(endPoint('getStats')) .expect((res) => { if (res.body.code !== 0) throw new Error('getStats() failed'); diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index bb0ecdd9add..e9168985167 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -8,13 +8,14 @@ */ const assert = require('assert').strict; -const async = require('async'); const common = require('../../common'); let agent; const apiKey = common.apiKey; let apiVersion = 1; const testPadId = makeid(); +const newPadId = makeid(); +const copiedPadId = makeid(); let lastEdited = ''; const text = generateLongText(); @@ -49,36 +50,25 @@ const expectedSpaceHtml = '
  • one describe(__filename, function () { before(async function () { agent = await common.init(); }); - describe('Connectivity', function () { - it('can connect', function (done) { - this.timeout(200); - agent.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done); + describe('Sanity checks', function () { + it('can connect', async function () { + await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); }); - }); - describe('API Versioning', function () { - it('finds the version tag', function (done) { - this.timeout(150); - agent.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .expect(200, done); + it('finds the version tag', async function () { + const res = await agent.get('/api/') + .expect(200); + apiVersion = res.body.currentVersion; + assert(apiVersion); }); - }); - describe('Permission', function () { - it('errors with invalid APIKey', function (done) { - this.timeout(150); + it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway - const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; - agent.get(permErrorURL) - .expect(401, done); + await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`) + .expect(401); }); }); @@ -124,633 +114,381 @@ describe(__filename, function () { */ - describe('deletePad', function () { - it('deletes a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect('Content-Type', /json/) - .expect(200, done); // @TODO: we shouldn't expect 200 here since the pad may not exist + describe('Tests', function () { + it('deletes a Pad that does not exist', async function () { + await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) // @TODO: we shouldn't expect 200 here since the pad may not exist + .expect('Content-Type', /json/); }); - }); - describe('createPad', function () { - it('creates a new Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getRevisionsCount', function () { - it('gets revision count of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); - if (res.body.data.revisions !== 0) throw new Error('Incorrect Revision Count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets revision count of Pad', async function () { + const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.revisions, 0); }); - }); - describe('getSavedRevisionsCount', function () { - it('gets saved revisions count of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 0) { - throw new Error('Incorrect Saved Revisions Count'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revisions count of Pad', async function () { + const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.savedRevisions, 0); }); - }); - describe('listSavedRevisions', function () { - it('gets saved revision list of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); - assert.deepEqual(res.body.data.savedRevisions, []); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revision list of Pad', async function () { + const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.deepEqual(res.body.data.savedRevisions, []); }); - }); - describe('getHTML', function () { - it('get the HTML of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of Pad', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert(res.body.data.html.length > 1); }); - }); - describe('listAllPads', function () { - it('list all pads', function (done) { - this.timeout(150); - agent.get(endPoint('listAllPads')) - .expect((res) => { - if (res.body.data.padIDs.includes(testPadId) !== true) { - throw new Error('Unable to find pad in pad list'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('list all pads', async function () { + const res = await agent.get(endPoint('listAllPads')) + .expect(200) + .expect('Content-Type', /json/); + assert(res.body.data.padIDs.includes(testPadId)); }); - }); - describe('deletePad', function () { - it('deletes a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('deletes the Pad', async function () { + const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('listAllPads', function () { - it('list all pads', function (done) { - this.timeout(150); - agent.get(endPoint('listAllPads')) - .expect((res) => { - if (res.body.data.padIDs.includes(testPadId) !== false) { - throw new Error('Test pad should not be in pads list'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('list all pads again', async function () { + const res = await agent.get(endPoint('listAllPads')) + .expect(200) + .expect('Content-Type', /json/); + assert(!res.body.data.padIDs.includes(testPadId)); }); - }); - describe('getHTML', function () { - it('get the HTML of a Pad -- Should return a failure', function (done) { - this.timeout(150); - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 1) throw new Error('Pad deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of a Pad -- Should return a failure', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 1); }); - }); - describe('createPad', function () { - it('creates a new Pad with text', function (done) { - this.timeout(200); - agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Creation failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad with text', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it('gets the Pad text and expect it to be testText with \n which is a line break', function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets the Pad text and expect it to be testText with trailing \\n', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, 'testText\n'); }); - }); - describe('setText', function () { - it('creates a new Pad with text', function (done) { - this.timeout(200); - agent.post(endPoint('setText')) + it('set text', async function () { + const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, text: 'testTextTwo', }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad setting text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it('gets the Pad text', function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets the Pad text', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, 'testTextTwo\n'); }); - }); - describe('getRevisionsCount', function () { - it('gets Revision Count of a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets Revision Count of a Pad', async function () { + const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.revisions, 1); }); - }); - describe('saveRevision', function () { - it('saves Revision', function (done) { - this.timeout(150); - agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to save Revision'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('saves Revision', async function () { + const res = await agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getSavedRevisionsCount', function () { - it('gets saved revisions count of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 1) { - throw new Error('Incorrect Saved Revisions Count'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revisions count of Pad again', async function () { + const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.savedRevisions, 1); }); - }); - describe('listSavedRevisions', function () { - it('gets saved revision list of Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); - assert.deepEqual(res.body.data.savedRevisions, [1]); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revision list of Pad again', async function () { + const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.deepEqual(res.body.data.savedRevisions, [1]); }); - }); - describe('padUsersCount', function () { - it('gets User Count of a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + + it('gets User Count of a Pad', async function () { + const res = await agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.padUsersCount, 0); }); - }); - describe('getReadOnlyID', function () { - it('Gets the Read Only ID of a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) - .expect((res) => { - if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets the Read Only ID of a Pad', async function () { + const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert(res.body.data.readOnlyID); }); - }); - describe('listAuthorsOfPad', function () { - it('Get Authors of the Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.authorIDs.length !== 0) { - throw new Error('# of Authors of pad is not 0'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get Authors of the Pad', async function () { + const res = await agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.authorIDs.length, 0); }); - }); - describe('getLastEdited', function () { - it('Get When Pad was left Edited', function (done) { - this.timeout(150); - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (!res.body.data.lastEdited) { - throw new Error('# of Authors of pad is not 0'); - } else { - lastEdited = res.body.data.lastEdited; - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get When Pad was left Edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert(res.body.data.lastEdited); + lastEdited = res.body.data.lastEdited; }); - }); - describe('setText', function () { - it('creates a new Pad with text', function (done) { - this.timeout(200); - agent.post(endPoint('setText')) + it('set text again', async function () { + const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, text: 'testTextTwo', }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad setting text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getLastEdited', function () { - it('Get When Pad was left Edited', function (done) { - this.timeout(150); - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.lastEdited <= lastEdited) { - throw new Error('Editing A Pad is not updating when it was last edited'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get When Pad was left Edited again', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert(res.body.data.lastEdited > lastEdited); }); - }); - describe('padUsers', function () { - it('gets User Count of a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets User Count of a Pad again', async function () { + const res = await agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.padUsers.length, 0); }); - }); - describe('deletePad', function () { - it('deletes a Pad', function (done) { - this.timeout(150); - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('deletes a Pad', async function () { + const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - const newPadId = makeid(); - const copiedPadId = makeid(); - - describe('createPad', function () { - it('creates a new Pad with text', function (done) { - this.timeout(200); - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Creation failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates the Pad again', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('setText', function () { - it('Sets text on a pad Id', function (done) { - this.timeout(150); - agent.post(`${endPoint('setText')}&padID=${testPadId}`) + it('Sets text on a pad Id', async function () { + const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text}) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', function (done) { - this.timeout(200); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text on a pad Id', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}\n`); }); - }); - describe('setText', function () { - it('Sets text on a pad Id including an explicit newline', function (done) { - this.timeout(200); - agent.post(`${endPoint('setText')}&padID=${testPadId}`) + it('Sets text on a pad Id including an explicit newline', async function () { + const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text: `${text}\n`}) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it("Gets text on a pad Id and doesn't have an excess newline", function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it("Gets text on a pad Id and doesn't have an excess newline", async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}\n`); }); - }); - describe('getLastEdited', function () { - it('Gets when pad was last edited', function (done) { - this.timeout(150); - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets when pad was last edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.notEqual(res.body.lastEdited, 0); }); - }); - describe('movePad', function () { - it('Move a Pad to a different Pad ID', function (done) { - this.timeout(200); - agent.get(`${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Move a Pad to a different Pad ID', async function () { + const res = await agent.get( + `${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${newPadId}`) - .expect((res) => { - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text from new pad', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${newPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, `${text}\n`); }); - }); - describe('movePad', function () { - it('Move a Pad to a different Pad ID', function (done) { - this.timeout(200); - agent.get(`${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}` + - '&force=false') - .expect((res) => { - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Move pad back to original ID', async function () { + const res = await agent.get( + `${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}&force=false`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get text using original ID', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, `${text}\n`); }); - }); - - describe('getLastEdited', function () { - it('Gets when pad was last edited', function (done) { - this.timeout(150); - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - describe('appendText', function () { - it('Append text to a pad Id', function (done) { - this.timeout(150); - agent.get(`${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Append Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get last edit of original ID', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.notEqual(res.body.lastEdited, 0); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', function (done) { - this.timeout(150); - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}hello\n`) { - throw new Error('Pad Text not set properly'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Append text to a pad Id', async function () { + let res = await agent.get( + `${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}hello\n`); }); - }); - - describe('setHTML', function () { - it('Sets the HTML of a Pad attempting to pass ugly HTML', function (done) { - this.timeout(200); + it('Sets the HTML of a Pad attempting to pass ugly HTML', async function () { const html = '
    Hello HTML
    '; - agent.post(endPoint('setHTML')) + const res = await agent.post(endPoint('setHTML')) .send({ padID: testPadId, html, }) - .expect((res) => { - if (res.body.code !== 0) { - throw new Error("Crappy HTML Can't be Imported[we weren't able to sanitize it']"); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); - }); - describe('setHTML', function () { - it('Sets the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(200); - agent.post(endPoint('setHTML')) + it('Pad with complex nested lists of different types', async function () { + let res = await agent.post(endPoint('setHTML')) .send({ padID: testPadId, html: ulHtml, }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(150); - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); - - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedHtml} - - Which is a slightly modified version of the originally imported one: - ${ulHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe('setHTML', function () { - it('Sets the HTML of a Pad with white space between list items', function (done) { - this.timeout(200); - agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(150); - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); - if (receivedHtml !== expectedSpaceHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedSpaceHtml} - - Which is a slightly modified version of the originally imported one: - ${ulSpaceHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); - }); - - describe('createPad', function () { - it('errors if pad can be created', function (done) { - this.timeout(150); - const badUrlChars = ['/', '%23', '%3F', '%26']; - async.map( - badUrlChars, - (badUrlChar, cb) => { - agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) - .expect((res) => { - if (res.body.code !== 1) throw new Error('Pad with bad characters was created'); - }) - .expect('Content-Type', /json/) - .end(cb); - }, - done); - }); - }); - - describe('copyPad', function () { - it('copies the content of a existent pad', function (done) { - this.timeout(200); - agent.get(`${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}` + - '&force=true') - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); + assert.equal(receivedHtml, expectedHtml); + }); + + it('Pad with white space between list items', async function () { + let res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); + assert.equal(receivedHtml, expectedSpaceHtml); + }); + + it('errors if pad can be created', async function () { + await Promise.all(['/', '%23', '%3F', '%26'].map(async (badUrlChar) => { + const res = await agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 1); + })); + }); + + it('copies the content of a existent pad', async function () { + const res = await agent.get( + `${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + }); + + it('does not add an useless revision', async function () { + let res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) + .field({text: 'identical text\n'}) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + + res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, 'identical text\n'); + + res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const revCount = res.body.data.revisions; + + res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) + .field({text: 'identical text\n'}) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + + res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.revisions, revCount); }); }); @@ -758,103 +496,66 @@ describe(__filename, function () { const sourcePadId = makeid(); let newPad; - before(function (done) { - createNewPadWithHtml(sourcePadId, ulHtml, done); + before(async function () { + await createNewPadWithHtml(sourcePadId, ulHtml); }); beforeEach(async function () { newPad = makeid(); }); - it('returns a successful response', function (done) { - this.timeout(200); - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${newPad}&force=false`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('returns a successful response', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + + `&destinationID=${newPad}&force=false`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); }); // this test validates if the source pad's text and attributes are kept - it('creates a new pad with the same content as the source pad', function (done) { - this.timeout(200); - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${newPad}&force=false`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); - }) - .end(() => { - agent.get(`${endPoint('getHTML')}&padID=${newPad}`) - .expect((res) => { - const receivedHtml = - res.body.data.html.replace('

    ', '').toLowerCase(); - - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedHtml} - - Which is a slightly modified version of the originally imported one: - ${ulHtml}`); - } - }) - .expect(200, done); - }); + it('creates a new pad with the same content as the source pad', async function () { + let res = await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + + `&destinationID=${newPad}&force=false`); + assert.equal(res.body.code, 0); + res = await agent.get(`${endPoint('getHTML')}&padID=${newPad}`) + .expect(200); + const receivedHtml = res.body.data.html.replace('

    ', '').toLowerCase(); + assert.equal(receivedHtml, expectedHtml); }); - context('when try copy a pad with a group that does not exist', function () { + describe('when try copy a pad with a group that does not exist', function () { const padId = makeid(); const padWithNonExistentGroup = `notExistentGroup$${padId}`; - it('throws an error', function (done) { - this.timeout(150); - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}&` + - `destinationID=${padWithNonExistentGroup}&force=true`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); - }) - .expect(200, done); + it('throws an error', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padWithNonExistentGroup}&force=true`) + .expect(200); + assert.equal(res.body.code, 1); }); }); - context('when try copy a pad and destination pad already exist', function () { + describe('when try copy a pad and destination pad already exist', function () { const padIdExistent = makeid(); - before(function (done) { - createNewPadWithHtml(padIdExistent, ulHtml, done); + before(async function () { + await createNewPadWithHtml(padIdExistent, ulHtml); }); - context('and force is false', function () { - it('throws an error', function (done) { - this.timeout(150); - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=false`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); - }) - .expect(200, done); - }); + it('force=false throws an error', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=false`) + .expect(200); + assert.equal(res.body.code, 1); }); - context('and force is true', function () { - it('returns a successful response', function (done) { - this.timeout(200); - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=true`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 0) { - throw new Error('Copy pad without history with force true failed'); - } - }) - .expect(200, done); - }); + it('force=true returns a successful response', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=true`) + .expect(200); + assert.equal(res.body.code, 0); }); }); }); @@ -865,15 +566,12 @@ describe(__filename, function () { */ -const createNewPadWithHtml = (padId, html, cb) => { - agent.get(`${endPoint('createPad')}&padID=${padId}`) - .end(() => { - agent.post(endPoint('setHTML')) - .send({ - padID: padId, - html, - }) - .end(cb); +const createNewPadWithHtml = async (padId, html) => { + await agent.get(`${endPoint('createPad')}&padID=${padId}`); + await agent.post(endPoint('setHTML')) + .send({ + padID: padId, + html, }); }; diff --git a/src/tests/backend/specs/api/sessionsAndGroups.js b/src/tests/backend/specs/api/sessionsAndGroups.js index 238353d0de2..83fdac69839 100644 --- a/src/tests/backend/specs/api/sessionsAndGroups.js +++ b/src/tests/backend/specs/api/sessionsAndGroups.js @@ -18,7 +18,6 @@ describe(__filename, function () { describe('API Versioning', function () { it('errors if can not connect', async function () { - this.timeout(200); await agent.get('/api/') .expect(200) .expect((res) => { @@ -60,7 +59,6 @@ describe(__filename, function () { describe('API: Group creation and deletion', function () { it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -72,7 +70,6 @@ describe(__filename, function () { }); it('listSessionsOfGroup for empty group', async function () { - this.timeout(100); await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -83,7 +80,6 @@ describe(__filename, function () { }); it('deleteGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -93,7 +89,6 @@ describe(__filename, function () { }); it('createGroupIfNotExistsFor', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupIfNotExistsFor')}&groupMapper=management`) .expect(200) .expect('Content-Type', /json/) @@ -106,7 +101,6 @@ describe(__filename, function () { // Test coverage for https://github.com/ether/etherpad-lite/issues/4227 // Creates a group, creates 2 sessions, 2 pads and then deletes the group. it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -118,7 +112,6 @@ describe(__filename, function () { }); it('createAuthor', async function () { - this.timeout(100); await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) @@ -130,7 +123,6 @@ describe(__filename, function () { }); it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -143,7 +135,6 @@ describe(__filename, function () { }); it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -156,7 +147,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x1234567`) .expect(200) .expect('Content-Type', /json/) @@ -166,7 +156,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x12345678`) .expect(200) .expect('Content-Type', /json/) @@ -176,7 +165,6 @@ describe(__filename, function () { }); it('deleteGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -189,7 +177,6 @@ describe(__filename, function () { describe('API: Author creation', function () { it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -201,7 +188,6 @@ describe(__filename, function () { }); it('createAuthor', async function () { - this.timeout(100); await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) @@ -212,7 +198,6 @@ describe(__filename, function () { }); it('createAuthor with name', async function () { - this.timeout(100); await agent.get(`${endPoint('createAuthor')}&name=john`) .expect(200) .expect('Content-Type', /json/) @@ -224,7 +209,6 @@ describe(__filename, function () { }); it('createAuthorIfNotExistsFor', async function () { - this.timeout(100); await agent.get(`${endPoint('createAuthorIfNotExistsFor')}&authorMapper=chris`) .expect(200) .expect('Content-Type', /json/) @@ -235,7 +219,6 @@ describe(__filename, function () { }); it('getAuthorName', async function () { - this.timeout(100); await agent.get(`${endPoint('getAuthorName')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) @@ -248,7 +231,6 @@ describe(__filename, function () { describe('API: Sessions', function () { it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -261,7 +243,6 @@ describe(__filename, function () { }); it('getSessionInfo', async function () { - this.timeout(100); await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -274,7 +255,6 @@ describe(__filename, function () { }); it('listSessionsOfGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -285,7 +265,6 @@ describe(__filename, function () { }); it('deleteSession', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteSession')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -295,7 +274,6 @@ describe(__filename, function () { }); it('getSessionInfo of deleted session', async function () { - this.timeout(100); await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -307,7 +285,6 @@ describe(__filename, function () { describe('API: Group pad management', function () { it('listPads', async function () { - this.timeout(100); await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -318,7 +295,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -329,7 +305,6 @@ describe(__filename, function () { }); it('listPads after creating a group pad', async function () { - this.timeout(100); await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -342,7 +317,6 @@ describe(__filename, function () { describe('API: Pad security', function () { it('getPublicStatus', async function () { - this.timeout(100); await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -353,7 +327,6 @@ describe(__filename, function () { }); it('setPublicStatus', async function () { - this.timeout(100); await agent.get(`${endPoint('setPublicStatus')}&padID=${padID}&publicStatus=true`) .expect(200) .expect('Content-Type', /json/) @@ -363,7 +336,6 @@ describe(__filename, function () { }); it('getPublicStatus after changing public status', async function () { - this.timeout(100); await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -380,7 +352,6 @@ describe(__filename, function () { describe('API: Misc', function () { it('listPadsOfAuthor', async function () { - this.timeout(100); await agent.get(`${endPoint('listPadsOfAuthor')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) diff --git a/src/tests/backend/specs/chat.js b/src/tests/backend/specs/chat.js new file mode 100644 index 00000000000..aefa64183f6 --- /dev/null +++ b/src/tests/backend/specs/chat.js @@ -0,0 +1,160 @@ +'use strict'; + +const ChatMessage = require('../../../static/js/ChatMessage'); +const {Pad} = require('../../../node/db/Pad'); +const assert = require('assert').strict; +const common = require('../common'); +const padManager = require('../../../node/db/PadManager'); +const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); + +const logger = common.logger; + +const checkHook = async (hookName, checkFn) => { + if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = []; + await new Promise((resolve, reject) => { + pluginDefs.hooks[hookName].push({ + hook_fn: async (hookName, context) => { + if (checkFn == null) return; + logger.debug(`hook ${hookName} invoked`); + try { + // Make sure checkFn is called only once. + const _checkFn = checkFn; + checkFn = null; + await _checkFn(context); + } catch (err) { + reject(err); + return; + } + resolve(); + }, + }); + }); +}; + +const sendMessage = (socket, data) => { + socket.send({ + type: 'COLLABROOM', + component: 'pad', + data, + }); +}; + +const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message}); + +describe(__filename, function () { + const padId = 'testChatPad'; + const hooksBackup = {}; + + before(async function () { + for (const [name, defs] of Object.entries(pluginDefs.hooks)) { + if (defs == null) continue; + hooksBackup[name] = defs; + } + }); + + beforeEach(async function () { + for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs]; + for (const name of Object.keys(pluginDefs.hooks)) { + if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; + } + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + }); + + after(async function () { + Object.assign(pluginDefs.hooks, hooksBackup); + for (const name of Object.keys(pluginDefs.hooks)) { + if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; + } + }); + + describe('chatNewMessage hook', function () { + let authorId; + let socket; + + beforeEach(async function () { + socket = await common.connect(); + const {data: clientVars} = await common.handshake(socket, padId); + authorId = clientVars.userId; + }); + + afterEach(async function () { + socket.close(); + }); + + it('message', async function () { + const start = Date.now(); + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + assert(message != null); + assert(message instanceof ChatMessage); + assert.equal(message.authorId, authorId); + assert.equal(message.text, this.test.title); + assert(message.time >= start); + assert(message.time <= Date.now()); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('pad', async function () { + await Promise.all([ + checkHook('chatNewMessage', ({pad}) => { + assert(pad != null); + assert(pad instanceof Pad); + assert.equal(pad.id, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('padId', async function () { + await Promise.all([ + checkHook('chatNewMessage', (context) => { + assert.equal(context.padId, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('mutations propagate', async function () { + const listen = async (type) => await new Promise((resolve) => { + const handler = (msg) => { + if (msg.type !== 'COLLABROOM') return; + if (msg.data == null || msg.data.type !== type) return; + resolve(msg.data); + socket.off('message', handler); + }; + socket.on('message', handler); + }); + + const modifiedText = `${this.test.title} `; + const customMetadata = {foo: this.test.title}; + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + message.text = modifiedText; + message.customMetadata = customMetadata; + }), + (async () => { + const {message} = await listen('CHAT_MESSAGE'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendChat(socket, {text: this.test.title}), + ]); + // Simulate fetch of historical chat messages when a pad is first loaded. + await Promise.all([ + (async () => { + const {messages: [message]} = await listen('CHAT_MESSAGES'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}), + ]); + }); + }); +}); diff --git a/src/tests/backend/specs/contentcollector.js b/src/tests/backend/specs/contentcollector.js index 1739bd7a042..f7bc539e627 100644 --- a/src/tests/backend/specs/contentcollector.js +++ b/src/tests/backend/specs/contentcollector.js @@ -11,8 +11,8 @@ const AttributePool = require('../../../static/js/AttributePool'); const assert = require('assert').strict; -const cheerio = require('cheerio'); const contentcollector = require('../../../static/js/contentcollector'); +const jsdom = require('jsdom'); const tests = { nestedLi: { @@ -285,15 +285,13 @@ describe(__filename, function () { } it(testObj.description, async function () { - this.timeout(250); - const $ = cheerio.load(testObj.html); // Load HTML into Cheerio - const doc = $('body')[0]; // Creates a dom-like representation of HTML + const {window: {document}} = new jsdom.JSDOM(testObj.html); // Create an empty attribute pool const apool = new AttributePool(); // Convert a dom tree into a list of lines and attribute liens // using the content collector object const cc = contentcollector.makeContentCollector(true, null, apool); - cc.collectContent(doc); + cc.collectContent(document.body); const result = cc.finish(); const gotAttributes = result.lineAttribs; const wantAttributes = testObj.wantLineAttribs; diff --git a/src/tests/backend/specs/hooks.js b/src/tests/backend/specs/hooks.js index e601c934499..3120911aea2 100644 --- a/src/tests/backend/specs/hooks.js +++ b/src/tests/backend/specs/hooks.js @@ -93,13 +93,11 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; callHookFnSync(hook); }); it('passes context', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; callHookFnSync(hook, val); @@ -107,7 +105,6 @@ describe(__filename, function () { }); it('returns the value provided to the callback', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; assert.equal(callHookFnSync(hook, val), val); @@ -115,7 +112,6 @@ describe(__filename, function () { }); it('returns the value returned by the hook function', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will error. hook.hook_fn = (hn, ctx) => ctx; @@ -124,19 +120,16 @@ describe(__filename, function () { }); it('does not catch exceptions', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; assert.throws(() => callHookFnSync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; callHookFnSync(hook); }); it('checks for deprecation', async function () { - this.timeout(30); sinon.stub(console, 'warn'); hooks.deprecationNotices[hookName] = 'test deprecation'; callHookFnSync(hook); @@ -149,7 +142,6 @@ describe(__filename, function () { describe('supported hook function styles', function () { for (const tc of supportedSyncHookFunctions) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'warn'); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; @@ -194,7 +186,6 @@ describe(__filename, function () { for (const tc of testCases) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; assert.equal(callHookFnSync(hook), tc.wantVal); @@ -246,7 +237,6 @@ describe(__filename, function () { if (step1.async && step2.async) continue; it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); @@ -310,7 +300,6 @@ describe(__filename, function () { if (step1.rejects !== step2.rejects) continue; it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - this.timeout(30); const err = new Error('val'); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, err, 'val'); @@ -336,32 +325,27 @@ describe(__filename, function () { describe('hooks.callAll', function () { describe('basic behavior', function () { it('calls all in order', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]); }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hooks.callAll(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hooks.callAll(hookName, wantContext); @@ -370,40 +354,34 @@ describe(__filename, function () { describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(hooks.callAll(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(hooks.callAll(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook()); assert.deepEqual(hooks.callAll(hookName), []); @@ -413,44 +391,37 @@ describe(__filename, function () { describe('hooks.callFirst', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(hooks.callFirst(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(hooks.callFirst(hookName), []); }); it('passes hook name => {}', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hooks.callFirst(hookName); }); it('undefined context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName); }); it('null context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hooks.callFirst(hookName, wantContext); }); it('predicate never satisfied -> calls all in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -463,35 +434,30 @@ describe(__filename, function () { }); it('stops when predicate is satisfied', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('skips values that do not satisfy predicate (undefined)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('skips values that do not satisfy predicate (empty list)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook([]), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('null satisifes the predicate', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), [null]); }); it('non-empty arrays are returned unmodified', async function () { - this.timeout(30); const want = ['val1']; testHooks.length = 0; testHooks.push(makeHook(want), makeHook(['val2'])); @@ -499,7 +465,6 @@ describe(__filename, function () { }); it('value can be passed via callback', async function () { - this.timeout(30); const want = {}; hook.hook_fn = (hn, ctx, cb) => { cb(want); }; const got = hooks.callFirst(hookName); @@ -513,13 +478,11 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; await callHookFnAsync(hook); }); it('passes context', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; await callHookFnAsync(hook, val); @@ -527,7 +490,6 @@ describe(__filename, function () { }); it('returns the value provided to the callback', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; assert.equal(await callHookFnAsync(hook, val), val); @@ -536,7 +498,6 @@ describe(__filename, function () { }); it('returns the value returned by the hook function', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will never resolve. hook.hook_fn = (hn, ctx) => ctx; @@ -546,31 +507,26 @@ describe(__filename, function () { }); it('rejects if it throws an exception', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise passed to callback', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise returned', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; await callHookFnAsync(hook); }); it('checks for deprecation', async function () { - this.timeout(30); sinon.stub(console, 'warn'); hooks.deprecationNotices[hookName] = 'test deprecation'; await callHookFnAsync(hook); @@ -663,7 +619,6 @@ describe(__filename, function () { for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'warn'); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; @@ -811,7 +766,6 @@ describe(__filename, function () { if (step1.name.startsWith('return ') || step1.name === 'throw') continue; for (const step2 of behaviors) { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); @@ -865,7 +819,6 @@ describe(__filename, function () { if (step1.rejects !== step2.rejects) continue; it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - this.timeout(30); const err = new Error('val'); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, err, 'val'); @@ -891,7 +844,6 @@ describe(__filename, function () { describe('hooks.aCallAll', function () { describe('basic behavior', function () { it('calls all asynchronously, returns values in order', async function () { - this.timeout(30); testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. let nextIndex = 0; const hookPromises = []; @@ -926,25 +878,21 @@ describe(__filename, function () { }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; await hooks.aCallAll(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.aCallAll(hookName, wantContext); @@ -953,13 +901,11 @@ describe(__filename, function () { describe('aCallAll callback', function () { it('exception in callback rejects', async function () { - this.timeout(30); const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); await assert.rejects(p, {message: 'test exception'}); }); it('propagates error on exception', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; await hooks.aCallAll(hookName, {}, (err) => { assert(err instanceof Error); @@ -968,14 +914,12 @@ describe(__filename, function () { }); it('propagages null error on success', async function () { - this.timeout(30); await hooks.aCallAll(hookName, {}, (err) => { assert(err == null, `got non-null error: ${err}`); }); }); it('propagages results on success', async function () { - this.timeout(30); hook.hook_fn = () => 'val'; await hooks.aCallAll(hookName, {}, (err, results) => { assert.deepEqual(results, ['val']); @@ -983,47 +927,40 @@ describe(__filename, function () { }); it('returns callback return value', async function () { - this.timeout(30); assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); }); }); describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks[hookName]; assert.deepEqual(await hooks.aCallAll(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.aCallAll(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook(Promise.resolve())); assert.deepEqual(await hooks.aCallAll(hookName), []); @@ -1034,7 +971,6 @@ describe(__filename, function () { describe('hooks.callAllSerial', function () { describe('basic behavior', function () { it('calls all asynchronously, serially, in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1057,25 +993,21 @@ describe(__filename, function () { }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; await hooks.callAllSerial(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.callAllSerial(hookName, wantContext); @@ -1084,40 +1016,34 @@ describe(__filename, function () { describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks[hookName]; assert.deepEqual(await hooks.callAllSerial(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.callAllSerial(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook(Promise.resolve())); assert.deepEqual(await hooks.callAllSerial(hookName), []); @@ -1127,44 +1053,37 @@ describe(__filename, function () { describe('hooks.aCallFirst', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(await hooks.aCallFirst(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.aCallFirst(hookName), []); }); it('passes hook name => {}', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; await hooks.aCallFirst(hookName); }); it('undefined context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName); }); it('null context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.aCallFirst(hookName, wantContext); }); it('default predicate: predicate never satisfied -> calls all in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1177,7 +1096,6 @@ describe(__filename, function () { }); it('calls hook functions serially', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1200,35 +1118,30 @@ describe(__filename, function () { }); it('default predicate: stops when satisfied', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: skips values that do not satisfy (undefined)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: skips values that do not satisfy (empty list)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook([]), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: null satisifes', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), [null]); }); it('custom predicate: called for each hook function', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(0), makeHook(1), makeHook(2)); let got = 0; @@ -1237,7 +1150,6 @@ describe(__filename, function () { }); it('custom predicate: boolean false/true continues/stops iteration', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; @@ -1250,7 +1162,6 @@ describe(__filename, function () { }); it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; @@ -1263,7 +1174,6 @@ describe(__filename, function () { }); it('custom predicate: array value passed unmodified to predicate', async function () { - this.timeout(30); const want = [0]; hook.hook_fn = () => want; const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! @@ -1271,20 +1181,17 @@ describe(__filename, function () { }); it('custom predicate: normalized value passed to predicate (undefined)', async function () { - this.timeout(30); const predicate = (got) => { assert.deepEqual(got, []); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (null)', async function () { - this.timeout(30); hook.hook_fn = () => null; const predicate = (got) => { assert.deepEqual(got, [null]); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('non-empty arrays are returned unmodified', async function () { - this.timeout(30); const want = ['val1']; testHooks.length = 0; testHooks.push(makeHook(want), makeHook(['val2'])); @@ -1292,7 +1199,6 @@ describe(__filename, function () { }); it('value can be passed via callback', async function () { - this.timeout(30); const want = {}; hook.hook_fn = (hn, ctx, cb) => { cb(want); }; const got = await hooks.aCallFirst(hookName); diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.js index 221193c3b87..388b8346ab4 100644 --- a/src/tests/backend/specs/regression-db.js +++ b/src/tests/backend/specs/regression-db.js @@ -24,7 +24,6 @@ describe(__filename, function () { }); it('regression test for missing await in createAuthor (#5000)', async function () { - this.timeout(700); const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes. assert(await AuthorManager.doesAuthorExist(authorID)); }); diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 9b9e2101b16..15f56177499 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -2,96 +2,11 @@ const assert = require('assert').strict; const common = require('../common'); -const io = require('socket.io-client'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); -const setCookieParser = require('set-cookie-parser'); const settings = require('../../../node/utils/Settings'); - -const logger = common.logger; - -// Waits for and returns the next named socket.io event. Rejects if there is any error while waiting -// (unless waiting for that error event). -const getSocketEvent = async (socket, event) => { - const errorEvents = [ - 'error', - 'connect_error', - 'connect_timeout', - 'reconnect_error', - 'reconnect_failed', - ]; - const handlers = {}; - let timeoutId; - return new Promise((resolve, reject) => { - timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000); - for (const event of errorEvents) { - handlers[event] = (errorString) => { - logger.debug(`socket.io ${event} event: ${errorString}`); - reject(new Error(errorString)); - }; - } - // This will overwrite one of the above handlers if the user is waiting for an error event. - handlers[event] = (...args) => { - logger.debug(`socket.io ${event} event`); - if (args.length > 1) return resolve(args); - resolve(args[0]); - }; - Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler)); - }).finally(() => { - clearTimeout(timeoutId); - Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler)); - }); -}; - -// Establishes a new socket.io connection. Passes the cookies from the `set-cookie` header(s) in -// `res` (which may be nullish) to the server. Returns a socket.io Socket object. -const connect = async (res) => { - // Convert the `set-cookie` header(s) into a `cookie` header. - const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); - const reqCookieHdr = Object.entries(resCookies).map( - ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); - - logger.debug('socket.io connecting...'); - let padId = null; - if (res) { - padId = res.req.path.split('/p/')[1]; - } - const socket = io(`${common.baseUrl}/`, { - forceNew: true, // Different tests will have different query parameters. - path: '/socket.io', - // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the - // express_sid cookie must be passed as a query parameter. - query: {cookie: reqCookieHdr, padId}, - }); - try { - await getSocketEvent(socket, 'connect'); - } catch (e) { - socket.close(); - throw e; - } - logger.debug('socket.io connected'); - - return socket; -}; - -// Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad. -// Returns the CLIENT_VARS message from the server. -const handshake = async (socket, padID) => { - logger.debug('sending CLIENT_READY...'); - socket.send({ - component: 'pad', - type: 'CLIENT_READY', - padId: padID, - sessionID: null, - token: 't.12345', - protocolVersion: 2, - }); - logger.debug('waiting for CLIENT_VARS response...'); - const msg = await getSocketEvent(socket, 'message'); - logger.debug('received CLIENT_VARS message'); - return msg; -}; +const socketIoRouter = require('../../../node/handler/SocketIORouter'); describe(__filename, function () { this.timeout(30000); @@ -142,38 +57,33 @@ describe(__filename, function () { describe('Normal accesses', function () { it('!authn anonymous cookie /p/pad -> 200, ok', async function () { - this.timeout(600); const res = await agent.get('/p/pad').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn !cookie -> ok', async function () { - this.timeout(400); - socket = await connect(null); - const clientVars = await handshake(socket, 'pad'); + 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 () { - this.timeout(400); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + 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 () { - this.timeout(400); settings.requireAuthentication = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); for (const authn of [false, true]) { const desc = authn ? 'authn user' : '!authn anonymous'; it(`${desc} read-only /p/pad -> 200, ok`, async function () { - this.timeout(400); const get = (ep) => { let res = agent.get(ep); if (authn) res = res.auth('user', 'user-password'); @@ -181,32 +91,30 @@ describe(__filename, function () { }; settings.requireAuthentication = authn; let res = await get('/p/pad'); - socket = await connect(res); - let clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + let clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); const readOnlyId = clientVars.data.readOnlyId; assert(readOnlyManager.isReadOnlyId(readOnlyId)); socket.close(); res = await get(`/p/${readOnlyId}`); - socket = await connect(res); - clientVars = await handshake(socket, readOnlyId); + socket = await common.connect(res); + clientVars = await common.handshake(socket, readOnlyId); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); } it('authz user /p/pad -> 200, ok', async function () { - this.timeout(400); settings.requireAuthentication = true; settings.requireAuthorization = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('supports pad names with characters that must be percent-encoded', async function () { - this.timeout(400); settings.requireAuthentication = true; // requireAuthorization is set to true here to guarantee that the user's padAuthorizations // object is populated. Technically this isn't necessary because the user's padAuthorizations @@ -215,58 +123,54 @@ describe(__filename, function () { settings.requireAuthorization = true; const encodedPadId = encodeURIComponent('päd'); const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'päd'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'päd'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); }); describe('Abnormal access attempts', function () { it('authn anonymous /p/pad -> 401, error', async function () { - this.timeout(400); settings.requireAuthentication = true; const res = await agent.get('/p/pad').expect(401); // Despite the 401, try to create the pad via a socket.io connection anyway. - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('authn anonymous read-only /p/pad -> 401, error', async function () { - this.timeout(400); settings.requireAuthentication = true; let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); const readOnlyId = clientVars.data.readOnlyId; assert(readOnlyManager.isReadOnlyId(readOnlyId)); socket.close(); res = await agent.get(`/p/${readOnlyId}`).expect(401); // Despite the 401, try to read the pad via a socket.io connection anyway. - socket = await connect(res); - const message = await handshake(socket, readOnlyId); + socket = await common.connect(res); + const message = await common.handshake(socket, readOnlyId); assert.equal(message.accessStatus, 'deny'); }); it('authn !cookie -> error', async function () { - this.timeout(400); settings.requireAuthentication = true; - socket = await connect(null); - const message = await handshake(socket, 'pad'); + socket = await common.connect(null); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('authorization bypass attempt -> error', async function () { - this.timeout(400); // 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 connect(res); + socket = await common.connect(res); // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. - const message = await handshake(socket, 'other-pad'); + const message = await common.handshake(socket, 'other-pad'); assert.equal(message.accessStatus, 'deny'); }); }); @@ -278,66 +182,59 @@ describe(__filename, function () { }); it("level='create' -> can create", async function () { - this.timeout(400); authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it('level=true -> can create', async function () { - this.timeout(400); authorize = () => true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it("level='modify' -> can modify", async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. authorize = () => 'modify'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it("level='create' settings.editOnly=true -> unable to create", async function () { - this.timeout(400); authorize = () => 'create'; settings.editOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='modify' settings.editOnly=false -> unable to create", async function () { - this.timeout(400); authorize = () => 'modify'; settings.editOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to create", async function () { - this.timeout(400); authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to modify", async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); @@ -349,56 +246,50 @@ describe(__filename, function () { }); it('user.canCreate = true -> can create and modify', async function () { - this.timeout(400); settings.users.user.canCreate = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it('user.canCreate = false -> unable to create', async function () { - this.timeout(400); settings.users.user.canCreate = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to create', async function () { - this.timeout(400); settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to modify', async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); it('user.readOnly = false -> can create and modify', async function () { - this.timeout(400); settings.users.user.readOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it('user.readOnly = true, user.canCreate = true -> unable to create', async function () { - this.timeout(400); settings.users.user.canCreate = true; settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); }); @@ -410,23 +301,126 @@ describe(__filename, function () { }); it('authorize hook does not elevate level from user settings', async function () { - this.timeout(400); settings.users.user.readOnly = true; authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user settings does not elevate level from authorize hook', async function () { - this.timeout(400); 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 connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); }); + + 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 8b372c9593d..93c8b3bc4ac 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.js @@ -1,3 +1,5 @@ +'use strict'; + const common = require('../common'); const settings = require('../../../node/utils/Settings'); @@ -20,7 +22,6 @@ describe(__filename, function () { describe('/javascript', function () { it('/javascript -> 200', async function () { - this.timeout(200); await agent.get('/javascript').expect(200); }); }); diff --git a/src/tests/backend/specs/webaccess.js b/src/tests/backend/specs/webaccess.js index fe8c4c5c99c..7594b57e325 100644 --- a/src/tests/backend/specs/webaccess.js +++ b/src/tests/backend/specs/webaccess.js @@ -43,67 +43,56 @@ describe(__filename, function () { describe('webaccess: without plugins', function () { it('!authn !authz anonymous / -> 200', async function () { - this.timeout(150); settings.requireAuthentication = false; settings.requireAuthorization = false; await agent.get('/').expect(200); }); it('!authn !authz anonymous /admin/ -> 401', async function () { - this.timeout(100); settings.requireAuthentication = false; settings.requireAuthorization = false; await agent.get('/admin/').expect(401); }); it('authn !authz anonymous / -> 401', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').expect(401); }); it('authn !authz user / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); }); it('authn !authz user /admin/ -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); it('authn !authz admin / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('admin', 'admin-password').expect(200); }); it('authn !authz admin /admin/ -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); }); it('authn authz user / -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('user', 'user-password').expect(403); }); it('authn authz user /admin/ -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); it('authn authz admin / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('admin', 'admin-password').expect(200); }); it('authn authz admin /admin/ -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); @@ -117,7 +106,6 @@ describe(__filename, function () { // parsing, resulting in successful comparisons against a null or undefined password. for (const creds of ['admin', 'admin:']) { it(`admin password: ${adminPassword} credentials: ${creds}`, async function () { - this.timeout(100); settings.users.admin.password = adminPassword; const encCreds = Buffer.from(creds).toString('base64'); await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); @@ -173,13 +161,11 @@ describe(__filename, function () { }); it('defers if it returns []', async function () { - this.timeout(100); await agent.get('/').expect(200); // Note: The preAuthorize hook always runs even if requireAuthorization is false. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('bypasses authenticate and authorize hooks when true is returned', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; handlers.preAuthorize[0].innerHandle = () => [true]; @@ -187,7 +173,6 @@ describe(__filename, function () { assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('bypasses authenticate and authorize hooks when false is returned', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; handlers.preAuthorize[0].innerHandle = () => [false]; @@ -195,14 +180,12 @@ describe(__filename, function () { assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('bypasses authenticate and authorize hooks for static content, defers', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/static/robots.txt').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('cannot grant access to /admin', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [true]; await agent.get('/admin/').expect(401); // Notes: @@ -216,13 +199,11 @@ describe(__filename, function () { 'authenticate_1']); }); it('can deny access to /admin', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [false]; await agent.get('/admin/').auth('admin', 'admin-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('runs preAuthzFailure hook when access is denied', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [false]; let called = false; plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { @@ -238,7 +219,6 @@ describe(__filename, function () { assert(called); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); }); @@ -251,13 +231,11 @@ describe(__filename, function () { }); it('is not called if !requireAuthentication and not /admin/*', async function () { - this.timeout(100); settings.requireAuthentication = false; await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('is called if !requireAuthentication and /admin/*', async function () { - this.timeout(100); settings.requireAuthentication = false; await agent.get('/admin/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -266,7 +244,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('defers if empty list returned', async function () { - this.timeout(100); await agent.get('/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -274,21 +251,18 @@ describe(__filename, function () { 'authenticate_1']); }); it('does not defer if return [true], 200', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = (req) => { req.session.user = {}; return [true]; }; await agent.get('/').expect(200); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('does not defer if return [false], 401', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = (req) => [false]; await agent.get('/').expect(401); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('falls back to HTTP basic auth', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -296,7 +270,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('passes settings.users in context', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({users}) => { assert.equal(users, settings.users); }; @@ -307,7 +280,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('passes user, password in context if provided', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({username, password}) => { assert.equal(username, 'user'); assert.equal(password, 'user-password'); @@ -319,7 +291,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('does not pass user, password in context if not provided', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({username, password}) => { assert(username == null); assert(password == null); @@ -331,13 +302,11 @@ describe(__filename, function () { 'authenticate_1']); }); it('errors if req.session.user is not created', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = () => [true]; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); @@ -351,7 +320,6 @@ describe(__filename, function () { }); it('is not called if !requireAuthorization (non-/admin)', async function () { - this.timeout(100); settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -360,7 +328,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('is not called if !requireAuthorization (/admin)', async function () { - this.timeout(100); settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -369,7 +336,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('defers if empty list returned', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -379,7 +345,6 @@ describe(__filename, function () { 'authorize_1']); }); it('does not defer if return [true], 200', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = () => [true]; await agent.get('/').auth('user', 'user-password').expect(200); // Note: authorize_1 was not called because authorize_0 handled it. @@ -390,7 +355,6 @@ describe(__filename, function () { 'authorize_0']); }); it('does not defer if return [false], 403', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = (req) => [false]; await agent.get('/').auth('user', 'user-password').expect(403); // Note: authorize_1 was not called because authorize_0 handled it. @@ -401,7 +365,6 @@ describe(__filename, function () { 'authorize_0']); }); it('passes req.path in context', async function () { - this.timeout(100); handlers.authorize[0].checkContext = ({resource}) => { assert.equal(resource, '/'); }; @@ -414,7 +377,6 @@ describe(__filename, function () { 'authorize_1']); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').auth('user', 'user-password').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -461,14 +423,12 @@ describe(__filename, function () { // authn failure tests it('authn fail, no hooks handle -> 401', async function () { - this.timeout(100); await agent.get('/').expect(401); assert(handlers.authnFailure.called); assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); it('authn fail, authnFailure handles', async function () { - this.timeout(100); handlers.authnFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); assert(handlers.authnFailure.called); @@ -476,7 +436,6 @@ describe(__filename, function () { assert(!handlers.authFailure.called); }); it('authn fail, authFailure handles', async function () { - this.timeout(100); handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authFailure'); assert(handlers.authnFailure.called); @@ -484,7 +443,6 @@ describe(__filename, function () { assert(handlers.authFailure.called); }); it('authnFailure trumps authFailure', async function () { - this.timeout(100); handlers.authnFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); @@ -494,14 +452,12 @@ describe(__filename, function () { // authz failure tests it('authz fail, no hooks handle -> 403', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(403); assert(!handlers.authnFailure.called); assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); it('authz fail, authzFailure handles', async function () { - this.timeout(100); handlers.authzFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); assert(!handlers.authnFailure.called); @@ -509,7 +465,6 @@ describe(__filename, function () { assert(!handlers.authFailure.called); }); it('authz fail, authFailure handles', async function () { - this.timeout(100); handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); assert(!handlers.authnFailure.called); @@ -517,7 +472,6 @@ describe(__filename, function () { assert(handlers.authFailure.called); }); it('authzFailure trumps authFailure', async function () { - this.timeout(100); handlers.authzFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 9e63cc8f524..dbb5d4b2d23 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -4,19 +4,6 @@ const helper = {}; (() => { let $iframe; - const jsLibraries = {}; - - helper.init = async () => { - [ - jsLibraries.jquery, - jsLibraries.sendkeys, - ] = await Promise.all([ - $.get('../../static/js/vendors/jquery.js'), - $.get('lib/sendkeys.js'), - ]); - // make sure we don't override existing jquery - jsLibraries.jquery = `if (typeof $ === 'undefined') {\n${jsLibraries.jquery}\n}`; - }; helper.randomString = (len) => { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -28,20 +15,31 @@ const helper = {}; return randomstring; }; - const getFrameJQuery = ($iframe) => { - /* - I tried over 9001 ways to inject javascript into iframes. - This is the only way I found that worked in IE 7+8+9, FF and Chrome - */ + helper.getFrameJQuery = async ($iframe, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; - // IE 8+9 Hack to make eval appear - // https://stackoverflow.com/q/2720444 - win.execScript && win.execScript('null'); + const load = async (url) => { + const elem = doc.createElement('script'); + elem.setAttribute('src', url); + const p = new Promise((resolve, reject) => { + const handler = (evt) => { + elem.removeEventListener('load', handler); + elem.removeEventListener('error', handler); + if (evt.type === 'error') return reject(new Error(`failed to load ${url}`)); + resolve(); + }; + elem.addEventListener('load', handler); + elem.addEventListener('error', handler); + }); + doc.head.appendChild(elem); + await p; + }; - win.eval(jsLibraries.jquery); - win.eval(jsLibraries.sendkeys); + if (!win.$) await load('../../static/js/vendors/jquery.js'); + // sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In + // other words, do not load both jQuery and sendkeys inside a Promise.all() call.) + if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; win.$.document = doc; @@ -98,8 +96,19 @@ const helper = {}; _retry: 0, clearCookies: true, id: `FRONTEND_TEST_${helper.randomString(20)}`, + hookFns: {}, }, opts); + // Set up socket.io spying as early as possible. + /** chat messages received */ + helper.chatMessages = []; + /** changeset commits from the server */ + helper.commits = []; + /** userInfo messages from the server */ + helper.userInfos = []; + if (opts.hookFns._socketCreated == null) opts.hookFns._socketCreated = []; + opts.hookFns._socketCreated.unshift(() => helper.spyOnSocketIO()); + // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. let encodedParams; if (opts.params) { @@ -124,8 +133,27 @@ const helper = {}; $('#iframe-container iframe').remove(); // set new iframe $('#iframe-container').append($iframe); - await new Promise((resolve) => $iframe.one('load', resolve)); - helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); + await Promise.all([ + new Promise((resolve) => $iframe.one('load', resolve)), + // Install the hook functions as early as possible because some of them fire right away. + new Promise((resolve, reject) => { + if ($iframe[0].contentWindow._postPluginUpdateForTestingDone) { + return reject(new Error( + 'failed to set _postPluginUpdateForTesting before it would have been called')); + } + $iframe[0].contentWindow._postPluginUpdateForTesting = () => { + const {hooks} = + $iframe[0].contentWindow.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); + for (const [hookName, hookFns] of Object.entries(opts.hookFns)) { + if (hooks[hookName] == null) hooks[hookName] = []; + hooks[hookName].push( + ...hookFns.map((hookFn) => ({hook_name: hookName, hook_fn: hookFn}))); + } + resolve(); + }; + }), + ]); + helper.padChrome$ = await helper.getFrameJQuery($('#iframe-container iframe'), true); helper.padChrome$.padeditor = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; if (opts.clearCookies) { @@ -134,42 +162,25 @@ const helper = {}; if (opts.padPrefs) { helper.setPadPrefCookie(opts.padPrefs); } + const $loading = helper.padChrome$('#editorloadingbox'); + const $container = helper.padChrome$('#editorcontainer'); try { await helper.waitForPromise( - () => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000); + () => !$loading.is(':visible') && $container.hasClass('initialized'), 10000); } catch (err) { if (opts._retry++ >= 4) throw new Error('Pad never loaded'); return await helper.aNewPad(opts); } - helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); - helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); + helper.padOuter$ = + await helper.getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false); + helper.padInner$ = + await helper.getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true); // disable all animations, this makes tests faster and easier helper.padChrome$.fx.off = true; helper.padOuter$.fx.off = true; helper.padInner$.fx.off = true; - /* - * chat messages received - * @type {Array} - */ - helper.chatMessages = []; - - /* - * changeset commits from the server - * @type {Array} - */ - helper.commits = []; - - /* - * userInfo messages from the server - * @type {Array} - */ - helper.userInfos = []; - - // listen for server messages - helper.spyOnSocketIO(); - return opts.id; }; @@ -184,8 +195,8 @@ const helper = {}; $('#iframe-container iframe').remove(); // set new iframe $('#iframe-container').append($iframe); - $iframe.one('load', () => { - helper.admin$ = getFrameJQuery($('#iframe-container iframe')); + $iframe.one('load', async () => { + helper.admin$ = await helper.getFrameJQuery($('#iframe-container iframe'), false); }); }; diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js index 253bfbc0df6..b828cd601a7 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -12,7 +12,9 @@ helper.spyOnSocketIO = () => { } else if (msg.data.type === 'USER_NEWINFO') { helper.userInfos.push(msg); } else if (msg.data.type === 'CHAT_MESSAGE') { - helper.chatMessages.push(msg); + helper.chatMessages.push(msg.data.message); + } else if (msg.data.type === 'CHAT_MESSAGES') { + helper.chatMessages.push(...msg.data.messages); } }); }; diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index d34676a66b0..831bf403ea4 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -1,5 +1,21 @@ 'use strict'; +const getCookies = + () => helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_utils').Cookies; + +const setToken = (token) => getCookies().set('token', token); + +const getToken = () => getCookies().get('token'); + +const startActingLike = (user) => { + helper.padChrome$ = user.padChrome$; + helper.padOuter$ = user.padOuter$; + helper.padInner$ = user.padInner$; + if (helper.padChrome$) setToken(user.token); +}; + +const clearToken = () => getCookies().remove('token'); + helper.multipleUsers = { _user0: null, _user1: null, @@ -34,18 +50,11 @@ helper.multipleUsers = { }, async _loadJQueryForUser1Frame() { - const code = await $.get('/static/js/jquery.js'); - - // make sure we don't override existing jquery - const jQueryCode = `if(typeof $ === "undefined") {\n${code}\n}`; - const sendkeysCode = await $.get('/tests/frontend/lib/sendkeys.js'); - const codesToLoad = [jQueryCode, sendkeysCode]; - - this._user1.padChrome$ = getFrameJQuery(codesToLoad, this._user1.$frame); + this._user1.padChrome$ = await helper.getFrameJQuery(this._user1.$frame, true); this._user1.padOuter$ = - getFrameJQuery(codesToLoad, this._user1.padChrome$('iframe[name="ace_outer"]')); + await helper.getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), false); this._user1.padInner$ = - getFrameJQuery(codesToLoad, this._user1.padOuter$('iframe[name="ace_inner"]')); + await helper.getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true); // update helper vars now that they are available helper.padChrome$ = this._user1.padChrome$; @@ -54,14 +63,11 @@ helper.multipleUsers = { }, async _createUser1Frame() { - // create the iframe - const padUrl = this._user0.$frame.attr('src'); - this._user1.$frame = $('