diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..8fd968ed280 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: ether diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 00000000000..e50491d232e --- /dev/null +++ b/.github/workflows/backend-tests.yml @@ -0,0 +1,60 @@ +name: "Backend tests" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + withoutplugins: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: without plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install libreoffice + run: | + sudo add-apt-repository -y ppa:libreoffice/ppa + sudo apt update + sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + # configures some settings and runs npm run test + - name: Run the backend tests + run: tests/frontend/travis/runnerBackend.sh + + withplugins: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: with Plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install libreoffice + run: | + sudo add-apt-repository -y ppa:libreoffice/ppa + sudo apt update + sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: Install etherpad plugins + run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents + + # configures some settings and runs npm run test + - name: Run the backend tests + run: tests/frontend/travis/runnerBackend.sh diff --git a/.github/workflows/dockerfile.yml b/.github/workflows/dockerfile.yml new file mode 100644 index 00000000000..8f6d5c3b037 --- /dev/null +++ b/.github/workflows/dockerfile.yml @@ -0,0 +1,26 @@ +name: "Dockerfile" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + dockerfile: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: build image and run connectivity test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: docker build + run: | + docker build -t etherpad:test . + docker run -d -p 9001:9001 etherpad:test + ./bin/installDeps.sh + sleep 3 # delay for startup? + cd src && npm run test-container diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 00000000000..3b178622ea0 --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,83 @@ +name: "Frontend tests" + +on: [push] + +jobs: + withoutplugins: + name: without plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Run sauce-connect-action + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + run: tests/frontend/travis/sauce_tunnel.sh + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + + - name: Write custom settings.json with loglevel WARN + run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json" + + - name: Run the frontend tests + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + GIT_HASH: ${{ steps.environment.outputs.sha_short }} + run: | + tests/frontend/travis/runner.sh + + withplugins: + name: with plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Run sauce-connect-action + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + run: tests/frontend/travis/sauce_tunnel.sh + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: Install etherpad plugins + run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents ep_set_title_on_pad + + - name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + + - name: Write custom settings.json with loglevel WARN + run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json" + + # XXX we should probably run all tests, because plugins could effect their results + - name: Remove standard frontend test files, so only plugin tests are run + run: rm tests/frontend/specs/* + + - name: Run the frontend tests + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} + GIT_HASH: ${{ steps.environment.outputs.sha_short }} + run: | + tests/frontend/travis/runner.sh diff --git a/.github/workflows/lint-package-lock.yml b/.github/workflows/lint-package-lock.yml new file mode 100644 index 00000000000..beef64ffe33 --- /dev/null +++ b/.github/workflows/lint-package-lock.yml @@ -0,0 +1,24 @@ +name: "Lint" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + lint-package-lock: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: package-lock.json + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install lockfile-lint + run: npm install lockfile-lint + + - name: Run lockfile-lint on package-lock.json + run: npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml new file mode 100644 index 00000000000..095adc785b0 --- /dev/null +++ b/.github/workflows/load-test.yml @@ -0,0 +1,53 @@ +name: "Loadtest" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + withoutplugins: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: without plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: Install etherpad-load-test + run: sudo npm install -g etherpad-load-test + + - name: Run load test + run: tests/frontend/travis/runnerLoadTest.sh + + withplugins: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: with Plugins + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: Install etherpad-load-test + run: sudo npm install -g etherpad-load-test + + - name: Install etherpad plugins + run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents + + # configures some settings and runs npm run test + - name: Run load test + run: tests/frontend/travis/runnerLoadTest.sh diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml new file mode 100644 index 00000000000..4bdfc219419 --- /dev/null +++ b/.github/workflows/rate-limit.yml @@ -0,0 +1,39 @@ +name: "rate limit" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + ratelimit: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: docker network + run: docker network create --subnet=172.23.42.0/16 ep_net + + - name: build docker image + run: | + docker build -f Dockerfile -t epl-debian-slim . + docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest . + docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip . + - name: run docker images + run: | + docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim & + docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest + docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip + + - name: install dependencies and create symlink for ep_etherpad-lite + run: bin/installDeps.sh + + - name: run rate limit test + run: | + cd tests/ratelimit + ./testlimits.sh diff --git a/.gitignore b/.gitignore index 95aed121feb..c75e5a61f73 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ var/dirty.db bin/convertSettings.json *~ *.patch -src/static/js/jquery.js npm-debug.log *.DS_Store .ep_initialized diff --git a/.travis.yml b/.travis.yml index 7b0ed03aef7..3f8ad1cf105 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,59 +8,127 @@ services: cache: false -before_install: - - sudo add-apt-repository -y ppa:libreoffice/ppa - - sudo apt-get update - - sudo apt-get -y install libreoffice - - sudo apt-get -y install libreoffice-pdfimport - -install: - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - -script: - - "tests/frontend/travis/runner.sh" - env: global: - secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec=" - secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g=" +_set_loglevel_warn: &set_loglevel_warn | + sed -e 's/"loglevel":[^,]*/"loglevel": "WARN"/' \ + settings.json.template >settings.json.template.new && + mv settings.json.template.new settings.json.template + +_install_libreoffice: &install_libreoffice >- + sudo add-apt-repository -y ppa:libreoffice/ppa && + sudo apt-get update && + sudo apt-get -y install libreoffice libreoffice-pdfimport + +_install_plugins: &install_plugins >- + npm install + ep_align + ep_author_hover + ep_cursortrace + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_readonly_guest + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + ep_set_title_on_pad + jobs: include: # we can only frontend tests from the ether/ organization and not from forks. # To request tests to be run ask a maintainer to fork your repo to ether/ - if: fork = false - name: "Test the Frontend" + name: "Test the Frontend without Plugins" install: - #FIXME - - "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/g' settings.json.template > settings.json" + - *set_loglevel_warn - "tests/frontend/travis/sauce_tunnel.sh" - "bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" script: - - "tests/frontend/travis/runner.sh" - - name: "Run the Backend tests" + - "./tests/frontend/travis/runner.sh" + - name: "Run the Backend tests without Plugins" install: + - *install_libreoffice + - *set_loglevel_warn - "bin/installDeps.sh" - "cd src && npm install && cd -" script: - "tests/frontend/travis/runnerBackend.sh" -## Temporarily commented out the Dockerfile tests -# - name: "Test the Dockerfile" -# install: -# - "cd src && npm install && cd -" -# script: -# - "docker build -t etherpad:test ." -# - "docker run -d -p 9001:9001 etherpad:test && sleep 3" -# - "cd src && npm run test-container" - - name: "Load test Etherpad" + - name: "Test the Dockerfile" + install: + - "cd src && npm install && cd -" + script: + - "docker build -t etherpad:test ." + - "docker run -d -p 9001:9001 etherpad:test && sleep 3" + - "cd src && npm run test-container" + - name: "Load test Etherpad without Plugins" install: + - *set_loglevel_warn - "bin/installDeps.sh" - "cd src && npm install && cd -" - "npm install -g etherpad-load-test" script: - "tests/frontend/travis/runnerLoadTest.sh" + # we can only frontend tests from the ether/ organization and not from forks. + # To request tests to be run ask a maintainer to fork your repo to ether/ + - if: fork = false + name: "Test the Frontend Plugins only" + install: + - *set_loglevel_warn + - "tests/frontend/travis/sauce_tunnel.sh" + - "bin/installDeps.sh" + - "rm tests/frontend/specs/*" + - *install_plugins + - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + script: + - "./tests/frontend/travis/runner.sh" + - name: "Lint test package-lock.json" + install: + - "npm install lockfile-lint" + script: + - npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm + - name: "Run the Backend tests with Plugins" + install: + - *install_libreoffice + - *set_loglevel_warn + - "bin/installDeps.sh" + - *install_plugins + - "cd src && npm install && cd -" + script: + - "tests/frontend/travis/runnerBackend.sh" + - name: "Test the Dockerfile" + install: + - "cd src && npm install && cd -" + script: + - "docker build -t etherpad:test ." + - "docker run -d -p 9001:9001 etherpad:test && sleep 3" + - "cd src && npm run test-container" + - name: "Load test Etherpad with Plugins" + install: + - *set_loglevel_warn + - "bin/installDeps.sh" + - *install_plugins + - "cd src && npm install && cd -" + - "npm install -g etherpad-load-test" + script: + - "tests/frontend/travis/runnerLoadTest.sh" + - name: "Test rate limit" + install: + - "docker network create --subnet=172.23.42.0/16 ep_net" + - "docker build -f Dockerfile -t epl-debian-slim ." + - "docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest ." + - "docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip ." + - "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest" + - "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &" + - "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip" + - "./bin/installDeps.sh" + script: + - "cd tests/ratelimit && bash testlimits.sh" notifications: irc: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0931502848f..b63f571b9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ -# Develop -- TODO Change to 1.8.x. -* ... +# 1.8.7 +### Compatibility-breaking changes +* **IMPORTANT:** It is no longer possible to protect a group pad with a + password. All API calls to `setPassword` or `isPasswordProtected` will fail. + Existing group pads that were previously password protected will no longer be + password protected. If you need fine-grained access control, you can restrict + API session creation in your frontend service, or you can use plugins. +* All workarounds for Microsoft Internet Explorer have been removed. IE might + still work, but it is untested. +* Plugin hook functions are now subject to new sanity checks. Buggy hook + functions will cause an error message to be logged +* Authorization failures now return 403 by default instead of 401 +* The `authorize` hook is now only called after successful authentication. Use + the new `preAuthorize` hook if you need to bypass authentication +* The `authFailure` hook is deprecated; use the new `authnFailure` and + `authzFailure` hooks instead +* The `indexCustomInlineScripts` hook was removed +* The `client` context property for the `handleMessage` and + `handleMessageSecurity` hooks has been renamed to `socket` (the old name is + still usable but deprecated) +* The `aceAttribClasses` hook functions are now called synchronously +* The format of `ENTER`, `CREATE`, and `LEAVE` log messages has changed +* Strings passed to `$.gritter.add()` are now expected to be plain text, not + HTML. Use jQuery or DOM objects if you need formatting + +### Notable new features +* Users can now import without creating and editing the pad first +* Added a new `readOnly` user setting that makes it possible to create users in + `settings.json` that can read pads but not create or modify them +* Added a new `canCreate` user setting that makes it possible to create users in + `settings.json` that can modify pads but not create them +* The `authorize` hook now accepts `readOnly` to grant read-only access to a pad +* The `authorize` hook now accepts `modify` to grant modify-only (creation + prohibited) access to a pad +* All authentication successes and failures are now logged +* Added a new `cookie.sameSite` setting that makes it possible to enable + authentication when Etherpad is embedded in an iframe from another site +* New `exportHTMLAdditionalContent` hook to include additional HTML content +* New `exportEtherpadAdditionalContent` hook to include additional database + content in `.etherpad` exports +* New `expressCloseServer` hook to close Express when required +* The `padUpdate` hook context now includes `revs` and `changeset` +* `checkPlugins.js` has various improvements to help plugin developers +* The HTTP request object (and therefore the express-session state) is now + accessible from within most `eejsBlock_*` hooks +* Users without a `password` or `hash` property in `settings.json` are no longer + ignored, so they can now be used by authentication plugins +* New permission denied modal and block ``permissionDenied`` +* Plugins are now updated to the latest version instead of minor or patches + +### Notable fixes +* Fixed rate limit accounting when Etherpad is behind a reverse proxy +* Fixed typos that prevented access to pads via an HTTP API session +* Fixed authorization failures for pad URLs containing a percent-encoded + character +* Fixed exporting of read-only pads +* Passwords are no longer written to connection state database entries or logged + in debug logs +* When using the keyboard to navigate through the toolbar buttons the button + with the focus is now highlighted +* Fixed support for Node.js 10 by passing the `--experimental-worker` flag +* Fixed export of HTML attributes within a line +* Fixed occasional "Cannot read property 'offsetTop' of undefined" error in + timeslider when "follow pad contents" is checked +* socket.io errors are now displayed instead of silently ignored +* Pasting while the caret is in a link now works (except for middle-click paste + on X11 systems) +* Removal of Microsoft Internet Explorer specific code +* Import better handles line breaks and white space +* Fix issue with ``createDiffHTML`` incorrect call of ``getInternalRevisionAText`` +* Allow additional characters in URLs +* MySQL engine fix and various other UeberDB updates (See UeberDB changelog). +* Admin UI improvements on search results (to remove duplicate items) +* Removal of unused cruft from ``clientVars`` (``ip`` and ``userAgent``) + + +### Minor changes +* Temporary disconnections no longer force a full page refresh +* Toolbar layout for narrow screens is improved +* Fixed `SameSite` cookie attribute for the `language`, `token`, and `pref` + cookies +* Fixed superfluous database accesses when deleting a pad +* Expanded test coverage. +* `package-lock.json` is now lint checked on commit +* Various lint fixes/modernization of code # 1.8.6 * IMPORTANT: This fixes a severe problem with postgresql in 1.8.5 diff --git a/Dockerfile b/Dockerfile index 45601c8764f..aa6091a5943 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,4 @@ COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json RUN chmod -R g=u . EXPOSE 9001 -CMD ["node", "node_modules/ep_etherpad-lite/node/server.js"] +CMD ["node", "--experimental-worker", "node_modules/ep_etherpad-lite/node/server.js"] diff --git a/README.md b/README.md index cf319d32225..086441b19b1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # A real-time collaborative editor for the web Docker Pulls -[![Travis (.org)](https://api.travis-ci.org/ether/etherpad-lite.svg?branch=develop)](https://travis-ci.org/github/ether/etherpad-lite) +[![Travis (.com)](https://api.travis-ci.com/ether/etherpad-lite.svg?branch=develop)](https://travis-ci.com/github/ether/etherpad-lite) ![Demo Etherpad Animated Jif](doc/images/etherpad_demo.gif "Etherpad in action") # About -Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on _your_ server, under _your_ control. +Etherpad is a real-time collaborative editor [scalable to thousands of simultaneous real time users](http://scale.etherpad.org/). It provides [full data export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) capabilities, and runs on _your_ server, under _your_ control. **[Try it out](https://video.etherpad.com)** @@ -19,7 +19,7 @@ Etherpad is a real-time collaborative editor scalable to thousands of simultaneo ### Quick install on Debian/Ubuntu ``` -curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt install -y nodejs git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh ``` @@ -127,7 +127,8 @@ Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/ # Get in touch The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues). -For **responsible disclosure of vulnerabilities**, please write a mail to the maintainer (a.mux@inwind.it). +For **responsible disclosure of vulnerabilities**, please write a mail to the maintainers (a.mux@inwind.it and contact@etherpad.org). +Join the official [Etherpad Discord Channel](https://discord.com/invite/daEjfhw) # HTTP API Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) diff --git a/bin/checkAllPads.js b/bin/checkAllPads.js index 0d4e8bb8d7e..f90e57aef10 100644 --- a/bin/checkAllPads.js +++ b/bin/checkAllPads.js @@ -3,88 +3,85 @@ */ if (process.argv.length != 2) { - console.error("Use: node bin/checkAllPads.js"); + console.error('Use: node bin/checkAllPads.js'); process.exit(1); } // load and initialize NPM -let npm = require('../src/node_modules/npm'); -npm.load({}, async function() { - +const npm = require('../src/node_modules/npm'); +npm.load({}, async () => { try { // initialize the database - let settings = require('../src/node/utils/Settings'); - let db = require('../src/node/db/DB'); + const settings = require('../src/node/utils/Settings'); + const db = require('../src/node/db/DB'); await db.init(); // load modules - let Changeset = require('../src/static/js/Changeset'); - let padManager = require('../src/node/db/PadManager'); + const Changeset = require('../src/static/js/Changeset'); + const padManager = require('../src/node/db/PadManager'); // get all pads - let res = await padManager.listAllPads(); - - for (let padId of res.padIDs) { + const res = await padManager.listAllPads(); - let pad = await padManager.getPad(padId); + for (const padId of res.padIDs) { + const pad = await padManager.getPad(padId); // check if the pad has a pool if (pad.pool === undefined) { - console.error("[" + pad.id + "] Missing attribute pool"); + console.error(`[${pad.id}] Missing attribute pool`); continue; } // create an array with key kevisions // key revisions always save the full pad atext - let head = pad.getHeadRevisionNumber(); - let keyRevisions = []; + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; for (let rev = 0; rev < head; rev += 100) { keyRevisions.push(rev); } // run through all key revisions - for (let keyRev of keyRevisions) { - + for (const keyRev of keyRevisions) { // create an array of revisions we need till the next keyRevision or the End - var revisionsNeeded = []; - for (let rev = keyRev ; rev <= keyRev + 100 && rev <= head; rev++) { + const revisionsNeeded = []; + for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { revisionsNeeded.push(rev); } // this array will hold all revision changesets - var revisions = []; + const revisions = []; // run through all needed revisions and get them from the database - for (let revNum of revisionsNeeded) { - let revision = await db.get("pad:" + pad.id + ":revs:" + revNum); - revisions[revNum] = revision; + for (const revNum of revisionsNeeded) { + const revision = await db.get(`pad:${pad.id}:revs:${revNum}`); + revisions[revNum] = revision; } // check if the revision exists if (revisions[keyRev] == null) { - console.error("[" + pad.id + "] Missing revision " + keyRev); + console.error(`[${pad.id}] Missing revision ${keyRev}`); continue; } // check if there is a atext in the keyRevisions if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error("[" + pad.id + "] Missing atext in revision " + keyRev); + console.error(`[${pad.id}] Missing atext in revision ${keyRev}`); continue; } - let apool = pad.pool; + const apool = pad.pool; let atext = revisions[keyRev].meta.atext; for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { try { - let cs = revisions[rev].changeset; + const cs = revisions[rev].changeset; atext = Changeset.applyToAText(cs, atext, apool); } catch (e) { - console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message); + console.error(`[${pad.id}] Bad changeset at revision ${i} - ${e.message}`); } } } - console.log("finished"); + console.log('finished'); process.exit(0); } } catch (err) { diff --git a/bin/checkPad.js b/bin/checkPad.js index c6a3a19711b..323840e7261 100644 --- a/bin/checkPad.js +++ b/bin/checkPad.js @@ -3,7 +3,7 @@ */ if (process.argv.length != 3) { - console.error("Use: node bin/checkPad.js $PADID"); + console.error('Use: node bin/checkPad.js $PADID'); process.exit(1); } @@ -11,83 +11,80 @@ if (process.argv.length != 3) { const padId = process.argv[2]; // load and initialize NPM; -let npm = require('../src/node_modules/npm'); -npm.load({}, async function() { - +const npm = require('../src/node_modules/npm'); +npm.load({}, async () => { try { // initialize database - let settings = require('../src/node/utils/Settings'); - let db = require('../src/node/db/DB'); + const settings = require('../src/node/utils/Settings'); + const db = require('../src/node/db/DB'); await db.init(); // load modules - let Changeset = require('ep_etherpad-lite/static/js/Changeset'); - let padManager = require('../src/node/db/PadManager'); + const Changeset = require('ep_etherpad-lite/static/js/Changeset'); + const padManager = require('../src/node/db/PadManager'); - let exists = await padManager.doesPadExists(padId); + const exists = await padManager.doesPadExists(padId); if (!exists) { - console.error("Pad does not exist"); + console.error('Pad does not exist'); process.exit(1); } // get the pad - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // create an array with key revisions // key revisions always save the full pad atext - let head = pad.getHeadRevisionNumber(); - let keyRevisions = []; + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; for (let rev = 0; rev < head; rev += 100) { keyRevisions.push(rev); } // run through all key revisions - for (let keyRev of keyRevisions) { - + for (const keyRev of keyRevisions) { // create an array of revisions we need till the next keyRevision or the End - let revisionsNeeded = []; + const revisionsNeeded = []; for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) { revisionsNeeded.push(rev); } // this array will hold all revision changesets - var revisions = []; + const revisions = []; // run through all needed revisions and get them from the database - for (let revNum of revisionsNeeded) { - let revision = await db.get("pad:" + padId + ":revs:" + revNum); + for (const revNum of revisionsNeeded) { + const revision = await db.get(`pad:${padId}:revs:${revNum}`); revisions[revNum] = revision; } // check if the pad has a pool - if (pad.pool === undefined ) { - console.error("Attribute pool is missing"); + if (pad.pool === undefined) { + console.error('Attribute pool is missing'); process.exit(1); } // check if there is an atext in the keyRevisions if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) { - console.error("No atext in key revision " + keyRev); + console.error(`No atext in key revision ${keyRev}`); continue; } - let apool = pad.pool; + const apool = pad.pool; let atext = revisions[keyRev].meta.atext; for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) { try { // console.log("check revision " + rev); - let cs = revisions[rev].changeset; + const cs = revisions[rev].changeset; atext = Changeset.applyToAText(cs, atext, apool); - } catch(e) { - console.error("Bad changeset at revision " + rev + " - " + e.message); + } catch (e) { + console.error(`Bad changeset at revision ${rev} - ${e.message}`); continue; } } - console.log("finished"); + console.log('finished'); process.exit(0); } - } catch (e) { console.trace(e); process.exit(1); diff --git a/bin/checkPadDeltas.js b/bin/checkPadDeltas.js index f1bd3ffe515..1e45f7148bd 100644 --- a/bin/checkPadDeltas.js +++ b/bin/checkPadDeltas.js @@ -1,120 +1,111 @@ -/* - * This is a debug tool. It checks all revisions for data corruption - */ - -if (process.argv.length != 3) { - console.error("Use: node bin/checkPadDeltas.js $PADID"); - process.exit(1); -} - -// get the padID -const padId = process.argv[2]; - -// load and initialize NPM; -var expect = require('expect.js') -var diff = require('diff') -var async = require('async') - -let npm = require('../src/node_modules/npm'); -var async = require("ep_etherpad-lite/node_modules/async"); -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); - -npm.load({}, async function() { - - try { - // initialize database - let settings = require('../src/node/utils/Settings'); - let db = require('../src/node/db/DB'); - await db.init(); - - // load modules - let Changeset = require('ep_etherpad-lite/static/js/Changeset'); - let padManager = require('../src/node/db/PadManager'); - - let exists = await padManager.doesPadExists(padId); - if (!exists) { - console.error("Pad does not exist"); - process.exit(1); - } - - // get the pad - let pad = await padManager.getPad(padId); - - //create an array with key revisions - //key revisions always save the full pad atext - var head = pad.getHeadRevisionNumber(); - var keyRevisions = []; - for(var i=0;i { + try { + // initialize database + const settings = require('../src/node/utils/Settings'); + const db = require('../src/node/db/DB'); + await db.init(); + + // load modules + const Changeset = require('ep_etherpad-lite/static/js/Changeset'); + const padManager = require('../src/node/db/PadManager'); + + const exists = await padManager.doesPadExists(padId); + if (!exists) { + console.error('Pad does not exist'); + process.exit(1); + } + + // get the pad + const pad = await padManager.getPad(padId); + + // create an array with key revisions + // key revisions always save the full pad atext + const head = pad.getHeadRevisionNumber(); + const keyRevisions = []; + for (var i = 0; i < head; i += 100) { + keyRevisions.push(i); + } + + // create an array with all revisions + const revisions = []; + for (var i = 0; i <= head; i++) { + revisions.push(i); + } + + let atext = Changeset.makeAText('\n'); + + // run trough all revisions + async.forEachSeries(revisions, (revNum, callback) => { + // console.log('Fetching', revNum) + db.db.get(`pad:${padId}:revs:${revNum}`, (err, revision) => { + if (err) return callback(err); + + // check if there is a atext in the keyRevisions + if (~keyRevisions.indexOf(revNum) && (revision === undefined || revision.meta === undefined || revision.meta.atext === undefined)) { + console.error(`No atext in key revision ${revNum}`); + callback(); + return; + } + + try { + // console.log("check revision ", revNum); + const cs = revision.changeset; + atext = Changeset.applyToAText(cs, atext, pad.pool); + } catch (e) { + console.error(`Bad changeset at revision ${revNum} - ${e.message}`); + callback(); + return; + } + + if (~keyRevisions.indexOf(revNum)) { + try { + expect(revision.meta.atext.text).to.eql(atext.text); + expect(revision.meta.atext.attribs).to.eql(atext.attribs); + } catch (e) { + console.error(`Atext in key revision ${revNum} doesn't match computed one.`); + console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; })); + // console.error(e) + // console.log('KeyRev. :', revision.meta.atext) + // console.log('Computed:', atext) + callback(); + return; + } + } + + setImmediate(callback); + }); + }, (er) => { + if (pad.atext.text == atext.text) { console.log('ok'); } else { + console.error('Pad AText doesn\'t match computed one! (Computed ', atext.text.length, ', db', pad.atext.text.length, ')'); + console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; })); + } + callback(er); + }); + + process.exit(0); + } catch (e) { + console.trace(e); + process.exit(1); + } +}); diff --git a/bin/cleanRun.sh b/bin/cleanRun.sh index 379b770a5a2..57de27e5cd6 100755 --- a/bin/cleanRun.sh +++ b/bin/cleanRun.sh @@ -1,7 +1,10 @@ #!/bin/sh -#Move to the folder where ep-lite is installed -cd $(dirname $0) +# Move to the folder where ep-lite is installed +cd "$(dirname "$0")"/.. + +# Source constants and usefull functions +. bin/functions.sh #Was this script started in the bin folder? if yes move out if [ -d "../bin" ]; then @@ -38,4 +41,4 @@ bin/installDeps.sh "$@" || exit 1 echo "Started Etherpad..." SCRIPTPATH=$(pwd -P) -node "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@" +node $(compute_node_args) "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/convert.js b/bin/convert.js index 82e0f757919..47f8b2d275a 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,128 +1,116 @@ -var startTime = Date.now(); -var fs = require("fs"); -var ueberDB = require("../src/node_modules/ueberdb2"); -var mysql = require("../src/node_modules/ueberdb2/node_modules/mysql"); -var async = require("../src/node_modules/async"); -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); - -var settingsFile = process.argv[2]; -var sqlOutputFile = process.argv[3]; - -//stop if the settings file is not set -if(!settingsFile || !sqlOutputFile) -{ - console.error("Use: node convert.js $SETTINGSFILE $SQLOUTPUT"); +const startTime = Date.now(); +const fs = require('fs'); +const ueberDB = require('../src/node_modules/ueberdb2'); +const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql'); +const async = require('../src/node_modules/async'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); + +const settingsFile = process.argv[2]; +const sqlOutputFile = process.argv[3]; + +// stop if the settings file is not set +if (!settingsFile || !sqlOutputFile) { + console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT'); process.exit(1); } -log("read settings file..."); -//read the settings file and parse the json -var settings = JSON.parse(fs.readFileSync(settingsFile, "utf8")); -log("done"); - -log("open output file..."); -var sqlOutput = fs.openSync(sqlOutputFile, "w"); -var sql = "SET CHARACTER SET UTF8;\n" + - "CREATE TABLE IF NOT EXISTS `store` ( \n" + - "`key` VARCHAR( 100 ) NOT NULL , \n" + - "`value` LONGTEXT NOT NULL , \n" + - "PRIMARY KEY ( `key` ) \n" + - ") ENGINE = INNODB;\n" + - "START TRANSACTION;\n\n"; +log('read settings file...'); +// read the settings file and parse the json +const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); +log('done'); + +log('open output file...'); +const sqlOutput = fs.openSync(sqlOutputFile, 'w'); +const sql = 'SET CHARACTER SET UTF8;\n' + + 'CREATE TABLE IF NOT EXISTS `store` ( \n' + + '`key` VARCHAR( 100 ) NOT NULL , \n' + + '`value` LONGTEXT NOT NULL , \n' + + 'PRIMARY KEY ( `key` ) \n' + + ') ENGINE = INNODB;\n' + + 'START TRANSACTION;\n\n'; fs.writeSync(sqlOutput, sql); -log("done"); - -var etherpadDB = mysql.createConnection({ - host : settings.etherpadDB.host, - user : settings.etherpadDB.user, - password : settings.etherpadDB.password, - database : settings.etherpadDB.database, - port : settings.etherpadDB.port +log('done'); + +const etherpadDB = mysql.createConnection({ + host: settings.etherpadDB.host, + user: settings.etherpadDB.user, + password: settings.etherpadDB.password, + database: settings.etherpadDB.database, + port: settings.etherpadDB.port, }); -//get the timestamp once -var timestamp = Date.now(); +// get the timestamp once +const timestamp = Date.now(); -var padIDs; +let padIDs; async.series([ - //get all padids out of the database... - function(callback) - { - log("get all padIds out of the database..."); + // get all padids out of the database... + function (callback) { + log('get all padIds out of the database...'); - etherpadDB.query("SELECT ID FROM PAD_META", [], function(err, _padIDs) - { + etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => { padIDs = _padIDs; callback(err); }); }, - function(callback) - { - log("done"); - - //create a queue with a concurrency 100 - var queue = async.queue(function (padId, callback) - { - convertPad(padId, function(err) - { + function (callback) { + log('done'); + + // create a queue with a concurrency 100 + const queue = async.queue((padId, callback) => { + convertPad(padId, (err) => { incrementPadStats(); callback(err); }); }, 100); - //set the step callback as the queue callback + // set the step callback as the queue callback queue.drain = callback; - //add the padids to the worker queue - for(var i=0,length=padIDs.length;i { + if (err) throw err; + + // write the groups + let sql = ''; + for (const proID in proID2groupID) { + const groupID = proID2groupID[proID]; + const subdomain = proID2subdomain[proID]; + + sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`; + sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`; } - //close transaction - sql+="COMMIT;"; + // close transaction + sql += 'COMMIT;'; - //end the sql file - fs.writeSync(sqlOutput, sql, undefined, "utf-8"); + // end the sql file + fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); fs.closeSync(sqlOutput); - log("finished."); + log('finished.'); process.exit(0); }); -function log(str) -{ - console.log((Date.now() - startTime)/1000 + "\t" + str); +function log(str) { + console.log(`${(Date.now() - startTime) / 1000}\t${str}`); } -var padsDone = 0; +let padsDone = 0; -function incrementPadStats() -{ +function incrementPadStats() { padsDone++; - if(padsDone%100 == 0) - { - var averageTime = Math.round(padsDone/((Date.now() - startTime)/1000)); - log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s") + if (padsDone % 100 == 0) { + const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000)); + log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`); } } @@ -130,293 +118,246 @@ var proID2groupID = {}; var proID2subdomain = {}; var groups = {}; -function convertPad(padId, callback) -{ - var changesets = []; - var changesetsMeta = []; - var chatMessages = []; - var authors = []; - var apool; - var subdomain; - var padmeta; +function convertPad(padId, callback) { + const changesets = []; + const changesetsMeta = []; + const chatMessages = []; + const authors = []; + let apool; + let subdomain; + let padmeta; async.series([ - //get all needed db values - function(callback) - { + // get all needed db values + function (callback) { async.parallel([ - //get the pad revisions - function(callback) - { - var sql = "SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { - //parse the pages - for(var i=0,length=results.length;i { + if (!err) { + try { + // parse the pages + for (let i = 0, length = results.length; i < length; i++) { parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); } - }catch(e) {err = e} + } catch (e) { err = e; } } callback(err); }); }, - //get the chat entries - function(callback) - { - var sql = "SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { - //parse the pages - for(var i=0,length=results.length;i { + if (!err) { + try { + // parse the pages + for (let i = 0, length = results.length; i < length; i++) { parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false); } - }catch(e) {err = e} + } catch (e) { err = e; } } callback(err); }); }, - //get the pad revisions meta data - function(callback) - { - var sql = "SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { - //parse the pages - for(var i=0,length=results.length;i { + if (!err) { + try { + // parse the pages + for (let i = 0, length = results.length; i < length; i++) { parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); } - }catch(e) {err = e} + } catch (e) { err = e; } } callback(err); }); }, - //get the attribute pool of this pad - function(callback) - { - var sql = "SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { - apool=JSON.parse(results[0].JSON).x; - }catch(e) {err = e} + // get the attribute pool of this pad + function (callback) { + const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?'; + + etherpadDB.query(sql, [padId], (err, results) => { + if (!err) { + try { + apool = JSON.parse(results[0].JSON).x; + } catch (e) { err = e; } } callback(err); }); }, - //get the authors informations - function(callback) - { - var sql = "SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { - //parse the pages - for(var i=0, length=results.length;i { + if (!err) { + try { + // parse the pages + for (let i = 0, length = results.length; i < length; i++) { parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); } - }catch(e) {err = e} + } catch (e) { err = e; } } callback(err); }); }, - //get the pad information - function(callback) - { - var sql = "SELECT JSON FROM `PAD_META` WHERE ID=?"; - - etherpadDB.query(sql, [padId], function(err, results) - { - if(!err) - { - try - { + // get the pad information + function (callback) { + const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?'; + + etherpadDB.query(sql, [padId], (err, results) => { + if (!err) { + try { padmeta = JSON.parse(results[0].JSON).x; - }catch(e) {err = e} + } catch (e) { err = e; } } callback(err); }); }, - //get the subdomain - function(callback) - { - //skip if this is no proPad - if(padId.indexOf("$") == -1) - { + // get the subdomain + function (callback) { + // skip if this is no proPad + if (padId.indexOf('$') == -1) { callback(); return; } - //get the proID out of this padID - var proID = padId.split("$")[0]; + // get the proID out of this padID + const proID = padId.split('$')[0]; - var sql = "SELECT subDomain FROM pro_domains WHERE ID = ?"; + const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?'; - etherpadDB.query(sql, [proID], function(err, results) - { - if(!err) - { + etherpadDB.query(sql, [proID], (err, results) => { + if (!err) { subdomain = results[0].subDomain; } callback(err); }); - } + }, ], callback); }, - function(callback) - { - //saves all values that should be written to the database - var values = {}; - - //this is a pro pad, let's convert it to a group pad - if(padId.indexOf("$") != -1) - { - var padIdParts = padId.split("$"); - var proID = padIdParts[0]; - var padName = padIdParts[1]; - - var groupID - - //this proID is not converted so far, do it - if(proID2groupID[proID] == null) - { - groupID = "g." + randomString(16); - - //create the mappers for this new group + function (callback) { + // saves all values that should be written to the database + const values = {}; + + // this is a pro pad, let's convert it to a group pad + if (padId.indexOf('$') != -1) { + const padIdParts = padId.split('$'); + const proID = padIdParts[0]; + const padName = padIdParts[1]; + + let groupID; + + // this proID is not converted so far, do it + if (proID2groupID[proID] == null) { + groupID = `g.${randomString(16)}`; + + // create the mappers for this new group proID2groupID[proID] = groupID; proID2subdomain[proID] = subdomain; groups[groupID] = {pads: {}}; } - //use the generated groupID; + // use the generated groupID; groupID = proID2groupID[proID]; - //rename the pad - padId = groupID + "$" + padName; + // rename the pad + padId = `${groupID}$${padName}`; - //set the value for this pad in the group + // set the value for this pad in the group groups[groupID].pads[padId] = 1; } - try - { - var newAuthorIDs = {}; - var oldName2newName = {}; + try { + const newAuthorIDs = {}; + const oldName2newName = {}; - //replace the authors with generated authors + // replace the authors with generated authors // we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global - for(var i in apool.numToAttrib) - { + for (var i in apool.numToAttrib) { var key = apool.numToAttrib[i][0]; - var value = apool.numToAttrib[i][1]; + const value = apool.numToAttrib[i][1]; - //skip non authors and anonymous authors - if(key != "author" || value == "") - continue; + // skip non authors and anonymous authors + if (key != 'author' || value == '') continue; - //generate new author values - var authorID = "a." + randomString(16); - var authorColorID = authors[i].colorId || Math.floor(Math.random()*(exports.getColorPalette().length)); - var authorName = authors[i].name || null; + // generate new author values + const authorID = `a.${randomString(16)}`; + const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length)); + const authorName = authors[i].name || null; - //overwrite the authorID of the attribute pool + // overwrite the authorID of the attribute pool apool.numToAttrib[i][1] = authorID; - //write the author to the database - values["globalAuthor:" + authorID] = {"colorId" : authorColorID, "name": authorName, "timestamp": timestamp}; + // write the author to the database + values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp}; - //save in mappers + // save in mappers newAuthorIDs[i] = authorID; oldName2newName[value] = authorID; } - //save all revisions - for(var i=0;i __dirname + '/../' + f; +const m = (f) => `${__dirname}/../${f}`; const fs = require('fs'); const path = require('path'); @@ -12,10 +12,10 @@ const settings = require(m('src/node/utils/Settings')); const supertest = require(m('src/node_modules/supertest')); (async () => { - const api = supertest('http://'+settings.ip+':'+settings.port); + const api = supertest(`http://${settings.ip}:${settings.port}`); const filePath = path.join(__dirname, '../APIKEY.txt'); - const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); + const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); let res; @@ -43,5 +43,5 @@ const supertest = require(m('src/node_modules/supertest')); res = await api.post(uri('createSession', {apikey, groupID, authorID, validUntil})); if (res.body.code === 1) throw new Error(`Error creating session: ${res.body}`); console.log('Session made: ====> create a cookie named sessionID and set the value to', - res.body.data.sessionID); + res.body.data.sessionID); })(); diff --git a/bin/debugRun.sh b/bin/debugRun.sh index d9b18aaa24d..9b2fff9bd41 100755 --- a/bin/debugRun.sh +++ b/bin/debugRun.sh @@ -3,6 +3,9 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + # Prepare the environment bin/installDeps.sh || exit 1 @@ -12,4 +15,4 @@ echo "Open 'chrome://inspect' on Chrome to start debugging." # Use 0.0.0.0 to allow external connections to the debugger # (ex: running Etherpad on a docker container). Use default port # (9229) -node --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@" +node $(compute_node_args) --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@" diff --git a/bin/deleteAllGroupSessions.js b/bin/deleteAllGroupSessions.js index cda4a3a59a9..ee0058ffa2a 100644 --- a/bin/deleteAllGroupSessions.js +++ b/bin/deleteAllGroupSessions.js @@ -4,48 +4,48 @@ */ const request = require('../src/node_modules/request'); -const settings = require(__dirname+'/../tests/container/loadSettings').loadSettings(); -const supertest = require(__dirname+'/../src/node_modules/supertest'); -const api = supertest('http://'+settings.ip+":"+settings.port); +const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings(); +const supertest = require(`${__dirname}/../src/node_modules/supertest`); +const api = supertest(`http://${settings.ip}:${settings.port}`); const path = require('path'); const fs = require('fs'); // get the API Key -var filePath = path.join(__dirname, '../APIKEY.txt'); -var apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); +const filePath = path.join(__dirname, '../APIKEY.txt'); +const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); // Set apiVersion to base value, we change this later. -var apiVersion = 1; -var guids; +let apiVersion = 1; +let guids; // Update the apiVersion api.get('/api/') -.expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; -}) -.then(function(){ - let guri = '/api/'+apiVersion+'/listAllGroups?apikey='+apikey; - api.get(guri) - .then(function(res){ - guids = res.body.data.groupIDs; - guids.forEach(function(groupID){ - let luri = '/api/'+apiVersion+'/listSessionsOfGroup?apikey='+apikey + "&groupID="+groupID; - api.get(luri) - .then(function(res){ - if(res.body.data){ - Object.keys(res.body.data).forEach(function(sessionID){ - if(sessionID){ - console.log("Deleting", sessionID); - let duri = '/api/'+apiVersion+'/deleteSession?apikey='+apikey + "&sessionID="+sessionID; - api.post(duri); // deletes - } - }) - }else{ - // no session in this group. - } - }) + .expect((res) => { + apiVersion = res.body.currentVersion; + if (!res.body.currentVersion) throw new Error('No version set in API'); + return; }) - }) -}) + .then(() => { + const guri = `/api/${apiVersion}/listAllGroups?apikey=${apikey}`; + api.get(guri) + .then((res) => { + guids = res.body.data.groupIDs; + guids.forEach((groupID) => { + const luri = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`; + api.get(luri) + .then((res) => { + if (res.body.data) { + Object.keys(res.body.data).forEach((sessionID) => { + if (sessionID) { + console.log('Deleting', sessionID); + const duri = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`; + api.post(duri); // deletes + } + }); + } else { + // no session in this group. + } + }); + }); + }); + }); diff --git a/bin/deletePad.js b/bin/deletePad.js index 2ce82f8a428..e145d63a05d 100644 --- a/bin/deletePad.js +++ b/bin/deletePad.js @@ -4,47 +4,45 @@ */ const request = require('../src/node_modules/request'); -const settings = require(__dirname+'/../tests/container/loadSettings').loadSettings(); -const supertest = require(__dirname+'/../src/node_modules/supertest'); -const api = supertest('http://'+settings.ip+":"+settings.port); +const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings(); +const supertest = require(`${__dirname}/../src/node_modules/supertest`); +const api = supertest(`http://${settings.ip}:${settings.port}`); const path = require('path'); const fs = require('fs'); if (process.argv.length != 3) { - console.error("Use: node deletePad.js $PADID"); + console.error('Use: node deletePad.js $PADID'); process.exit(1); } // get the padID -let padId = process.argv[2]; +const padId = process.argv[2]; // get the API Key -var filePath = path.join(__dirname, '../APIKEY.txt'); -var apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); +const filePath = path.join(__dirname, '../APIKEY.txt'); +const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); // Set apiVersion to base value, we change this later. -var apiVersion = 1; +let apiVersion = 1; // Update the apiVersion api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .end(function(err, res){ - + .expect((res) => { + apiVersion = res.body.currentVersion; + if (!res.body.currentVersion) throw new Error('No version set in API'); + return; + }) + .end((err, res) => { // Now we know the latest API version, let's delete pad - var uri = '/api/'+apiVersion+'/deletePad?apikey='+apikey+'&padID='+padId; - api.post(uri) - .expect(function(res){ - if (res.body.code === 1){ - console.error("Error deleting pad", res.body); - }else{ - console.log("Deleted pad", res.body); - } - return; - }) - .end(function(){}) - }); + const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`; + api.post(uri) + .expect((res) => { + if (res.body.code === 1) { + console.error('Error deleting pad', res.body); + } else { + console.log('Deleted pad', res.body); + } + return; + }) + .end(() => {}); + }); // end - diff --git a/bin/doc/generate.js b/bin/doc/generate.js index b3a2c2aceea..803f5017e12 100644 --- a/bin/doc/generate.js +++ b/bin/doc/generate.js @@ -20,19 +20,19 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var marked = require('marked'); -var fs = require('fs'); -var path = require('path'); +const marked = require('marked'); +const fs = require('fs'); +const path = require('path'); // parse the args. // Don't use nopt or whatever for this. It's simple enough. -var args = process.argv.slice(2); -var format = 'json'; -var template = null; -var inputFile = null; +const args = process.argv.slice(2); +let format = 'json'; +let template = null; +let inputFile = null; -args.forEach(function (arg) { +args.forEach((arg) => { if (!arg.match(/^\-\-/)) { inputFile = arg; } else if (arg.match(/^\-\-format=/)) { @@ -40,7 +40,7 @@ args.forEach(function (arg) { } else if (arg.match(/^\-\-template=/)) { template = arg.replace(/^\-\-template=/, ''); } -}) +}); if (!inputFile) { @@ -49,25 +49,25 @@ if (!inputFile) { console.error('Input file = %s', inputFile); -fs.readFile(inputFile, 'utf8', function(er, input) { +fs.readFile(inputFile, 'utf8', (er, input) => { if (er) throw er; // process the input for @include lines processIncludes(inputFile, input, next); }); -var includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; -var includeData = {}; +const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; +const includeData = {}; function processIncludes(inputFile, input, cb) { - var includes = input.match(includeExpr); + const includes = input.match(includeExpr); if (includes === null) return cb(null, input); - var errState = null; + let errState = null; console.error(includes); - var incCount = includes.length; + let incCount = includes.length; if (incCount === 0) cb(null, input); - includes.forEach(function(include) { - var fname = include.replace(/^@include\s+/, ''); + includes.forEach((include) => { + let fname = include.replace(/^@include\s+/, ''); if (!fname.match(/\.md$/)) fname += '.md'; if (includeData.hasOwnProperty(fname)) { @@ -78,11 +78,11 @@ function processIncludes(inputFile, input, cb) { } } - var fullFname = path.resolve(path.dirname(inputFile), fname); - fs.readFile(fullFname, 'utf8', function(er, inc) { + const fullFname = path.resolve(path.dirname(inputFile), fname); + fs.readFile(fullFname, 'utf8', (er, inc) => { if (errState) return; if (er) return cb(errState = er); - processIncludes(fullFname, inc, function(er, inc) { + processIncludes(fullFname, inc, (er, inc) => { if (errState) return; if (er) return cb(errState = er); incCount--; @@ -101,20 +101,20 @@ function next(er, input) { if (er) throw er; switch (format) { case 'json': - require('./json.js')(input, inputFile, function(er, obj) { + require('./json.js')(input, inputFile, (er, obj) => { console.log(JSON.stringify(obj, null, 2)); if (er) throw er; }); break; case 'html': - require('./html.js')(input, inputFile, template, function(er, html) { + require('./html.js')(input, inputFile, template, (er, html) => { if (er) throw er; console.log(html); }); break; default: - throw new Error('Invalid format: ' + format); + throw new Error(`Invalid format: ${format}`); } } diff --git a/bin/doc/html.js b/bin/doc/html.js index 700ab18ccb6..26cf3f18557 100644 --- a/bin/doc/html.js +++ b/bin/doc/html.js @@ -19,15 +19,15 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var fs = require('fs'); -var marked = require('marked'); -var path = require('path'); +const fs = require('fs'); +const marked = require('marked'); +const path = require('path'); module.exports = toHTML; function toHTML(input, filename, template, cb) { - var lexed = marked.lexer(input); - fs.readFile(template, 'utf8', function(er, template) { + const lexed = marked.lexer(input); + fs.readFile(template, 'utf8', (er, template) => { if (er) return cb(er); render(lexed, filename, template, cb); }); @@ -35,7 +35,7 @@ function toHTML(input, filename, template, cb) { function render(lexed, filename, template, cb) { // get the section - var section = getSection(lexed); + const section = getSection(lexed); filename = path.basename(filename, '.md'); @@ -43,7 +43,7 @@ function render(lexed, filename, template, cb) { // generate the table of contents. // this mutates the lexed contents in-place. - buildToc(lexed, filename, function(er, toc) { + buildToc(lexed, filename, (er, toc) => { if (er) return cb(er); template = template.replace(/__FILENAME__/g, filename); @@ -63,11 +63,11 @@ function render(lexed, filename, template, cb) { // just update the list item text in-place. // lists that come right after a heading are what we're after. function parseLists(input) { - var state = null; - var depth = 0; - var output = []; + let state = null; + let depth = 0; + const output = []; output.links = input.links; - input.forEach(function(tok) { + input.forEach((tok) => { if (state === null) { if (tok.type === 'heading') { state = 'AFTERHEADING'; @@ -79,7 +79,7 @@ function parseLists(input) { if (tok.type === 'list_start') { state = 'LIST'; if (depth === 0) { - output.push({ type:'html', text: '
' }); + output.push({type: 'html', text: '
'}); } depth++; output.push(tok); @@ -99,7 +99,7 @@ function parseLists(input) { depth--; if (depth === 0) { state = null; - output.push({ type:'html', text: '
' }); + output.push({type: 'html', text: '
'}); } output.push(tok); return; @@ -117,16 +117,16 @@ function parseLists(input) { function parseListItem(text) { text = text.replace(/\{([^\}]+)\}/, '$1'); - //XXX maybe put more stuff here? + // XXX maybe put more stuff here? return text; } // section is just the first heading function getSection(lexed) { - var section = ''; - for (var i = 0, l = lexed.length; i < l; i++) { - var tok = lexed[i]; + const section = ''; + for (let i = 0, l = lexed.length; i < l; i++) { + const tok = lexed[i]; if (tok.type === 'heading') return tok.text; } return ''; @@ -134,40 +134,39 @@ function getSection(lexed) { function buildToc(lexed, filename, cb) { - var indent = 0; - var toc = []; - var depth = 0; - lexed.forEach(function(tok) { + const indent = 0; + let toc = []; + let depth = 0; + lexed.forEach((tok) => { if (tok.type !== 'heading') return; if (tok.depth - depth > 1) { - return cb(new Error('Inappropriate heading level\n' + - JSON.stringify(tok))); + return cb(new Error(`Inappropriate heading level\n${ + JSON.stringify(tok)}`)); } depth = tok.depth; - var id = getId(filename + '_' + tok.text.trim()); - toc.push(new Array((depth - 1) * 2 + 1).join(' ') + - '* ' + - tok.text + ''); - tok.text += '#'; + const id = getId(`${filename}_${tok.text.trim()}`); + toc.push(`${new Array((depth - 1) * 2 + 1).join(' ') + }* ${ + tok.text}`); + tok.text += `#`; }); toc = marked.parse(toc.join('\n')); cb(null, toc); } -var idCounters = {}; +const idCounters = {}; function getId(text) { text = text.toLowerCase(); text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/^_+|_+$/, ''); text = text.replace(/^([^a-z])/, '_$1'); if (idCounters.hasOwnProperty(text)) { - text += '_' + (++idCounters[text]); + text += `_${++idCounters[text]}`; } else { idCounters[text] = 0; } return text; } - diff --git a/bin/doc/json.js b/bin/doc/json.js index a404675b585..3ce62a30136 100644 --- a/bin/doc/json.js +++ b/bin/doc/json.js @@ -24,24 +24,24 @@ module.exports = doJSON; // Take the lexed input, and return a JSON-encoded object // A module looks like this: https://gist.github.com/1777387 -var marked = require('marked'); +const marked = require('marked'); function doJSON(input, filename, cb) { - var root = {source: filename}; - var stack = [root]; - var depth = 0; - var current = root; - var state = null; - var lexed = marked.lexer(input); - lexed.forEach(function (tok) { - var type = tok.type; - var text = tok.text; + const root = {source: filename}; + const stack = [root]; + let depth = 0; + let current = root; + let state = null; + const lexed = marked.lexer(input); + lexed.forEach((tok) => { + const type = tok.type; + let text = tok.text; // // This is for cases where the markdown semantic structure is lacking. if (type === 'paragraph' || type === 'html') { - var metaExpr = /\n*/g; - text = text.replace(metaExpr, function(_0, k, v) { + const metaExpr = /\n*/g; + text = text.replace(metaExpr, (_0, k, v) => { current[k.trim()] = v.trim(); return ''; }); @@ -52,8 +52,8 @@ function doJSON(input, filename, cb) { if (type === 'heading' && !text.trim().match(/^example/i)) { if (tok.depth - depth > 1) { - return cb(new Error('Inappropriate heading level\n'+ - JSON.stringify(tok))); + return cb(new Error(`Inappropriate heading level\n${ + JSON.stringify(tok)}`)); } // Sometimes we have two headings with a single @@ -61,7 +61,7 @@ function doJSON(input, filename, cb) { if (current && state === 'AFTERHEADING' && depth === tok.depth) { - var clone = current; + const clone = current; current = newSection(tok); current.clone = clone; // don't keep it around on the stack. @@ -75,7 +75,7 @@ function doJSON(input, filename, cb) { // root is always considered the level=0 section, // and the lowest heading is 1, so this should always // result in having a valid parent node. - var d = tok.depth; + let d = tok.depth; while (d <= depth) { finishSection(stack.pop(), stack[stack.length - 1]); d++; @@ -98,7 +98,7 @@ function doJSON(input, filename, cb) { // // If one of these isn't found, then anything that comes between // here and the next heading should be parsed as the desc. - var stability + let stability; if (state === 'AFTERHEADING') { if (type === 'code' && (stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) { @@ -138,7 +138,6 @@ function doJSON(input, filename, cb) { current.desc = current.desc || []; current.desc.push(tok); - }); // finish any sections left open @@ -146,7 +145,7 @@ function doJSON(input, filename, cb) { finishSection(current, stack[stack.length - 1]); } - return cb(null, root) + return cb(null, root); } @@ -193,14 +192,14 @@ function doJSON(input, filename, cb) { // default: 'false' } ] } ] function processList(section) { - var list = section.list; - var values = []; - var current; - var stack = []; + const list = section.list; + const values = []; + let current; + const stack = []; // for now, *just* build the hierarchical list - list.forEach(function(tok) { - var type = tok.type; + list.forEach((tok) => { + const type = tok.type; if (type === 'space') return; if (type === 'list_item_start') { if (!current) { @@ -217,26 +216,26 @@ function processList(section) { return; } else if (type === 'list_item_end') { if (!current) { - throw new Error('invalid list - end without current item\n' + - JSON.stringify(tok) + '\n' + - JSON.stringify(list)); + throw new Error(`invalid list - end without current item\n${ + JSON.stringify(tok)}\n${ + JSON.stringify(list)}`); } current = stack.pop(); } else if (type === 'text') { if (!current) { - throw new Error('invalid list - text without current item\n' + - JSON.stringify(tok) + '\n' + - JSON.stringify(list)); + throw new Error(`invalid list - text without current item\n${ + JSON.stringify(tok)}\n${ + JSON.stringify(list)}`); } current.textRaw = current.textRaw || ''; - current.textRaw += tok.text + ' '; + current.textRaw += `${tok.text} `; } }); // shove the name in there for properties, since they are always // just going to be the value etc. if (section.type === 'property' && values[0]) { - values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw; + values[0].textRaw = `\`${section.name}\` ${values[0].textRaw}`; } // now pull the actual values out of the text bits. @@ -252,9 +251,9 @@ function processList(section) { // each item is an argument, unless the name is 'return', // in which case it's the return value. section.signatures = section.signatures || []; - var sig = {} + var sig = {}; section.signatures.push(sig); - sig.params = values.filter(function(v) { + sig.params = values.filter((v) => { if (v.name === 'return') { sig.return = v; return false; @@ -271,7 +270,7 @@ function processList(section) { delete value.name; section.typeof = value.type; delete value.type; - Object.keys(value).forEach(function(k) { + Object.keys(value).forEach((k) => { section[k] = value[k]; }); break; @@ -289,36 +288,36 @@ function processList(section) { // textRaw = "someobject.someMethod(a, [b=100], [c])" function parseSignature(text, sig) { - var params = text.match(paramExpr); + let params = text.match(paramExpr); if (!params) return; params = params[1]; // the ] is irrelevant. [ indicates optionalness. params = params.replace(/\]/g, ''); - params = params.split(/,/) - params.forEach(function(p, i, _) { + params = params.split(/,/); + params.forEach((p, i, _) => { p = p.trim(); if (!p) return; - var param = sig.params[i]; - var optional = false; - var def; + let param = sig.params[i]; + let optional = false; + let def; // [foo] -> optional if (p.charAt(0) === '[') { optional = true; p = p.substr(1); } - var eq = p.indexOf('='); + const eq = p.indexOf('='); if (eq !== -1) { def = p.substr(eq + 1); p = p.substr(0, eq); } if (!param) { - param = sig.params[i] = { name: p }; + param = sig.params[i] = {name: p}; } // at this point, the name should match. if (p !== param.name) { console.error('Warning: invalid param "%s"', p); - console.error(' > ' + JSON.stringify(param)); - console.error(' > ' + text); + console.error(` > ${JSON.stringify(param)}`); + console.error(` > ${text}`); } if (optional) param.optional = true; if (def !== undefined) param.default = def; @@ -332,18 +331,18 @@ function parseListItem(item) { // the goal here is to find the name, type, default, and optional. // anything left over is 'desc' - var text = item.textRaw.trim(); + let text = item.textRaw.trim(); // text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, ''); text = text.replace(/^, /, '').trim(); - var retExpr = /^returns?\s*:?\s*/i; - var ret = text.match(retExpr); + const retExpr = /^returns?\s*:?\s*/i; + const ret = text.match(retExpr); if (ret) { item.name = 'return'; text = text.replace(retExpr, ''); } else { - var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; - var name = text.match(nameExpr); + const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; + const name = text.match(nameExpr); if (name) { item.name = name[1]; text = text.replace(nameExpr, ''); @@ -351,24 +350,24 @@ function parseListItem(item) { } text = text.trim(); - var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i; - var def = text.match(defaultExpr); + const defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i; + const def = text.match(defaultExpr); if (def) { item.default = def[1]; text = text.replace(defaultExpr, ''); } text = text.trim(); - var typeExpr = /^\{([^\}]+)\}/; - var type = text.match(typeExpr); + const typeExpr = /^\{([^\}]+)\}/; + const type = text.match(typeExpr); if (type) { item.type = type[1]; text = text.replace(typeExpr, ''); } text = text.trim(); - var optExpr = /^Optional\.|(?:, )?Optional$/; - var optional = text.match(optExpr); + const optExpr = /^Optional\.|(?:, )?Optional$/; + const optional = text.match(optExpr); if (optional) { item.optional = true; text = text.replace(optExpr, ''); @@ -382,9 +381,9 @@ function parseListItem(item) { function finishSection(section, parent) { if (!section || !parent) { - throw new Error('Invalid finishSection call\n'+ - JSON.stringify(section) + '\n' + - JSON.stringify(parent)); + throw new Error(`Invalid finishSection call\n${ + JSON.stringify(section)}\n${ + JSON.stringify(parent)}`); } if (!section.type) { @@ -394,7 +393,7 @@ function finishSection(section, parent) { } section.displayName = section.name; section.name = section.name.toLowerCase() - .trim().replace(/\s+/g, '_'); + .trim().replace(/\s+/g, '_'); } if (section.desc && Array.isArray(section.desc)) { @@ -411,10 +410,10 @@ function finishSection(section, parent) { // Merge them into the parent. if (section.type === 'class' && section.ctors) { section.signatures = section.signatures || []; - var sigs = section.signatures; - section.ctors.forEach(function(ctor) { + const sigs = section.signatures; + section.ctors.forEach((ctor) => { ctor.signatures = ctor.signatures || [{}]; - ctor.signatures.forEach(function(sig) { + ctor.signatures.forEach((sig) => { sig.desc = ctor.desc; }); sigs.push.apply(sigs, ctor.signatures); @@ -425,7 +424,7 @@ function finishSection(section, parent) { // properties are a bit special. // their "type" is the type of object, not "property" if (section.properties) { - section.properties.forEach(function (p) { + section.properties.forEach((p) => { if (p.typeof) p.type = p.typeof; else delete p.type; delete p.typeof; @@ -434,27 +433,27 @@ function finishSection(section, parent) { // handle clones if (section.clone) { - var clone = section.clone; + const clone = section.clone; delete section.clone; delete clone.clone; deepCopy(section, clone); finishSection(clone, parent); } - var plur; + let plur; if (section.type.slice(-1) === 's') { - plur = section.type + 'es'; + plur = `${section.type}es`; } else if (section.type.slice(-1) === 'y') { plur = section.type.replace(/y$/, 'ies'); } else { - plur = section.type + 's'; + plur = `${section.type}s`; } // if the parent's type is 'misc', then it's just a random // collection of stuff, like the "globals" section. // Make the children top-level items. if (section.type === 'misc') { - Object.keys(section).forEach(function(k) { + Object.keys(section).forEach((k) => { switch (k) { case 'textRaw': case 'name': @@ -486,9 +485,7 @@ function finishSection(section, parent) { // Not a general purpose deep copy. // But sufficient for these basic things. function deepCopy(src, dest) { - Object.keys(src).filter(function(k) { - return !dest.hasOwnProperty(k); - }).forEach(function(k) { + Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { dest[k] = deepCopy_(src[k]); }); } @@ -497,14 +494,14 @@ function deepCopy_(src) { if (!src) return src; if (Array.isArray(src)) { var c = new Array(src.length); - src.forEach(function(v, i) { + src.forEach((v, i) => { c[i] = deepCopy_(v); }); return c; } if (typeof src === 'object') { var c = {}; - Object.keys(src).forEach(function(k) { + Object.keys(src).forEach((k) => { c[k] = deepCopy_(src[k]); }); return c; @@ -514,21 +511,21 @@ function deepCopy_(src) { // these parse out the contents of an H# tag -var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; -var classExpr = /^Class:\s*([^ ]+).*?$/i; -var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; -var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; -var classMethExpr = +const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; +const classExpr = /^Class:\s*([^ ]+).*?$/i; +const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; +const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; +const classMethExpr = /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; -var methExpr = +const methExpr = /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; -var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; +const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; var paramExpr = /\((.*)\);?$/; function newSection(tok) { - var section = {}; + const section = {}; // infer the type from the text. - var text = section.textRaw = tok.text; + const text = section.textRaw = tok.text; if (text.match(eventExpr)) { section.type = 'event'; section.name = text.replace(eventExpr, '$1'); diff --git a/bin/extractPadData.js b/bin/extractPadData.js index cce297f7113..a811076efff 100644 --- a/bin/extractPadData.js +++ b/bin/extractPadData.js @@ -5,60 +5,60 @@ */ if (process.argv.length != 3) { - console.error("Use: node extractPadData.js $PADID"); + console.error('Use: node extractPadData.js $PADID'); process.exit(1); } // get the padID -let padId = process.argv[2]; +const padId = process.argv[2]; -let npm = require('../src/node_modules/npm'); +const npm = require('../src/node_modules/npm'); -npm.load({}, async function(er) { +npm.load({}, async (er) => { if (er) { - console.error("Could not load NPM: " + er) + console.error(`Could not load NPM: ${er}`); process.exit(1); } try { // initialize database - let settings = require('../src/node/utils/Settings'); - let db = require('../src/node/db/DB'); + const settings = require('../src/node/utils/Settings'); + const db = require('../src/node/db/DB'); await db.init(); // load extra modules - let dirtyDB = require('../src/node_modules/dirty'); - let padManager = require('../src/node/db/PadManager'); - let util = require('util'); + const dirtyDB = require('../src/node_modules/dirty'); + const padManager = require('../src/node/db/PadManager'); + const util = require('util'); // initialize output database - let dirty = dirtyDB(padId + '.db'); + const dirty = dirtyDB(`${padId}.db`); // Promise wrapped get and set function - let wrapped = db.db.db.wrappedDB; - let get = util.promisify(wrapped.get.bind(wrapped)); - let set = util.promisify(dirty.set.bind(dirty)); + const wrapped = db.db.db.wrappedDB; + const get = util.promisify(wrapped.get.bind(wrapped)); + const set = util.promisify(dirty.set.bind(dirty)); // array in which required key values will be accumulated - let neededDBValues = ['pad:' + padId]; + const neededDBValues = [`pad:${padId}`]; // get the actual pad object - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // add all authors - neededDBValues.push(...pad.getAllAuthors().map(author => 'globalAuthor:' + author)); + neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`)); // add all revisions for (let rev = 0; rev <= pad.head; ++rev) { - neededDBValues.push('pad:' + padId + ':revs:' + rev); + neededDBValues.push(`pad:${padId}:revs:${rev}`); } // add all chat values for (let chat = 0; chat <= pad.chatHead; ++chat) { - neededDBValues.push('pad:' + padId + ':chat:' + chat); + neededDBValues.push(`pad:${padId}:chat:${chat}`); } - for (let dbkey of neededDBValues) { + for (const dbkey of neededDBValues) { let dbvalue = await get(dbkey); if (dbvalue && typeof dbvalue !== 'object') { dbvalue = JSON.parse(dbvalue); diff --git a/bin/fastRun.sh b/bin/fastRun.sh index e00bb8c72c4..90d83dc8e2d 100755 --- a/bin/fastRun.sh +++ b/bin/fastRun.sh @@ -12,6 +12,9 @@ set -eu # source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +# Source constants and usefull functions +. ${DIR}/../bin/functions.sh + echo "Running directly, without checking/installing dependencies" # move to the base Etherpad directory. This will be necessary until Etherpad @@ -19,4 +22,4 @@ echo "Running directly, without checking/installing dependencies" cd "${DIR}/.." # run Etherpad main class -node "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "${@}" +node $(compute_node_args) "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/bin/functions.sh b/bin/functions.sh new file mode 100644 index 00000000000..c7f3c85561f --- /dev/null +++ b/bin/functions.sh @@ -0,0 +1,74 @@ +# minimum required node version +REQUIRED_NODE_MAJOR=10 +REQUIRED_NODE_MINOR=13 + +# minimum required npm version +REQUIRED_NPM_MAJOR=5 +REQUIRED_NPM_MINOR=5 + +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +is_cmd() { command -v "$@" >/dev/null 2>&1; } + + +get_program_version() { + PROGRAM="$1" + KIND="${2:-full}" + PROGRAM_VERSION_STRING=$($PROGRAM --version) + PROGRAM_VERSION_STRING=${PROGRAM_VERSION_STRING#"v"} + + DETECTED_MAJOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 1) + [ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM major version from version string \"$PROGRAM_VERSION_STRING\"" + case "$DETECTED_MAJOR" in + ''|*[!0-9]*) + fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\"" + ;; + esac + + DETECTED_MINOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 2) + [ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM minor version from version string \"$PROGRAM_VERSION_STRING\"" + case "$DETECTED_MINOR" in + ''|*[!0-9]*) + fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\"" + esac + + case $KIND in + major) + echo $DETECTED_MAJOR + exit;; + minor) + echo $DETECTED_MINOR + exit;; + *) + echo $DETECTED_MAJOR.$DETECTED_MINOR + exit;; + esac + + echo $VERSION +} + + +compute_node_args() { + ARGS="" + + NODE_MAJOR=$(get_program_version "node" "major") + [ "$NODE_MAJOR" -eq "10" ] && ARGS="$ARGS --experimental-worker" + + echo $ARGS +} + + +require_minimal_version() { + PROGRAM_LABEL="$1" + VERSION="$2" + REQUIRED_MAJOR="$3" + REQUIRED_MINOR="$4" + + VERSION_MAJOR=$(pecho "$VERSION" | cut -s -d "." -f 1) + VERSION_MINOR=$(pecho "$VERSION" | cut -s -d "." -f 2) + + [ "$VERSION_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$VERSION_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$VERSION_MINOR" -ge "$REQUIRED_MINOR" ]) \ + || fatal "Your $PROGRAM_LABEL version \"$VERSION_MAJOR.$VERSION_MINOR\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required." +} diff --git a/bin/importSqlFile.js b/bin/importSqlFile.js index 8bc78323a17..a67cb8bf0a5 100644 --- a/bin/importSqlFile.js +++ b/bin/importSqlFile.js @@ -1,94 +1,87 @@ -var startTime = Date.now(); +const startTime = Date.now(); -require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { +require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => { + const fs = require('fs'); - var fs = require("fs"); + const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2'); + const settings = require('ep_etherpad-lite/node/utils/Settings'); + const log4js = require('ep_etherpad-lite/node_modules/log4js'); - var ueberDB = require("ep_etherpad-lite/node_modules/ueberdb2"); - var settings = require("ep_etherpad-lite/node/utils/Settings"); - var log4js = require('ep_etherpad-lite/node_modules/log4js'); - - var dbWrapperSettings = { + const dbWrapperSettings = { cache: 0, writeInterval: 100, - json: false // data is already json encoded + json: false, // data is already json encoded }; - var db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger("ueberDB")); + const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); - var sqlFile = process.argv[2]; + const sqlFile = process.argv[2]; - //stop if the settings file is not set - if(!sqlFile) - { - console.error("Use: node importSqlFile.js $SQLFILE"); + // stop if the settings file is not set + if (!sqlFile) { + console.error('Use: node importSqlFile.js $SQLFILE'); process.exit(1); } - log("initializing db"); - db.init(function(err) - { - //there was an error while initializing the database, output it and stop - if(err) - { - console.error("ERROR: Problem while initializing the database"); + log('initializing db'); + db.init((err) => { + // there was an error while initializing the database, output it and stop + if (err) { + console.error('ERROR: Problem while initializing the database'); console.error(err.stack ? err.stack : err); process.exit(1); - } - else - { - log("done"); + } else { + log('done'); - log("open output file..."); - var lines = fs.readFileSync(sqlFile, 'utf8').split("\n"); + log('open output file...'); + const lines = fs.readFileSync(sqlFile, 'utf8').split('\n'); - var count = lines.length; - var keyNo = 0; + const count = lines.length; + let keyNo = 0; - process.stdout.write("Start importing " + count + " keys...\n"); - lines.forEach(function(l) { - if (l.substr(0, 27) == "REPLACE INTO store VALUES (") { - var pos = l.indexOf("', '"); - var key = l.substr(28, pos - 28); - var value = l.substr(pos + 3); + process.stdout.write(`Start importing ${count} keys...\n`); + lines.forEach((l) => { + if (l.substr(0, 27) == 'REPLACE INTO store VALUES (') { + const pos = l.indexOf("', '"); + const key = l.substr(28, pos - 28); + let value = l.substr(pos + 3); value = value.substr(0, value.length - 2); - console.log("key: " + key + " val: " + value); - console.log("unval: " + unescape(value)); + console.log(`key: ${key} val: ${value}`); + console.log(`unval: ${unescape(value)}`); db.set(key, unescape(value), null); keyNo++; if (keyNo % 1000 == 0) { - process.stdout.write(" " + keyNo + "/" + count + "\n"); + process.stdout.write(` ${keyNo}/${count}\n`); } } }); - process.stdout.write("\n"); - process.stdout.write("done. waiting for db to finish transaction. depended on dbms this may take some time...\n"); + process.stdout.write('\n'); + process.stdout.write('done. waiting for db to finish transaction. depended on dbms this may take some time...\n'); - db.doShutdown(function() { - log("finished, imported " + keyNo + " keys."); + db.doShutdown(() => { + log(`finished, imported ${keyNo} keys.`); process.exit(0); }); } }); }); -function log(str) -{ - console.log((Date.now() - startTime)/1000 + "\t" + str); +function log(str) { + console.log(`${(Date.now() - startTime) / 1000}\t${str}`); } -unescape = function(val) { +unescape = function (val) { // value is a string if (val.substr(0, 1) == "'") { val = val.substr(0, val.length - 1).substr(1); - return val.replace(/\\[0nrbtZ\\'"]/g, function(s) { - switch(s) { - case "\\0": return "\0"; - case "\\n": return "\n"; - case "\\r": return "\r"; - case "\\b": return "\b"; - case "\\t": return "\t"; - case "\\Z": return "\x1a"; + return val.replace(/\\[0nrbtZ\\'"]/g, (s) => { + switch (s) { + case '\\0': return '\0'; + case '\\n': return '\n'; + case '\\r': return '\r'; + case '\\b': return '\b'; + case '\\t': return '\t'; + case '\\Z': return '\x1a'; default: return s.substr(1); } }); diff --git a/bin/installDeps.sh b/bin/installDeps.sh index 5e0bbb931eb..bdce38fc75a 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -1,52 +1,11 @@ #!/bin/sh -# minimum required node version -REQUIRED_NODE_MAJOR=10 -REQUIRED_NODE_MINOR=13 - -# minimum required npm version -REQUIRED_NPM_MAJOR=5 -REQUIRED_NPM_MINOR=5 - -pecho() { printf %s\\n "$*"; } -log() { pecho "$@"; } -error() { log "ERROR: $@" >&2; } -fatal() { error "$@"; exit 1; } -is_cmd() { command -v "$@" >/dev/null 2>&1; } - -require_minimal_version() { - PROGRAM_LABEL="$1" - VERSION_STRING="$2" - REQUIRED_MAJOR="$3" - REQUIRED_MINOR="$4" - - # Flag -s (--only-delimited on GNU cut) ensures no string is returned - # when there is no match - DETECTED_MAJOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 1) - DETECTED_MINOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 2) - - [ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM_LABEL major version from version string \"$VERSION_STRING\"" - - [ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM_LABEL minor version from version string \"$VERSION_STRING\"" - - case "$DETECTED_MAJOR" in - ''|*[!0-9]*) - fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\"" - ;; - esac - - case "$DETECTED_MINOR" in - ''|*[!0-9]*) - fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\"" - esac - - [ "$DETECTED_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$DETECTED_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$DETECTED_MINOR" -ge "$REQUIRED_MINOR" ]) \ - || fatal "Your $PROGRAM_LABEL version \"$VERSION_STRING\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required." -} - # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + # Is node installed? # Not checking io.js, default installation creates a symbolic link to node is_cmd node || fatal "Please install node.js ( https://nodejs.org )" @@ -55,15 +14,10 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )" is_cmd npm || fatal "Please install npm ( https://npmjs.org )" # Check npm version -NPM_VERSION_STRING=$(npm --version) - -require_minimal_version "npm" "$NPM_VERSION_STRING" "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" +require_minimal_version "npm" $(get_program_version "npm") "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" # Check node version -NODE_VERSION_STRING=$(node --version) -NODE_VERSION_STRING=${NODE_VERSION_STRING#"v"} - -require_minimal_version "nodejs" "$NODE_VERSION_STRING" "$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" diff --git a/bin/migrateDirtyDBtoRealDB.js b/bin/migrateDirtyDBtoRealDB.js index ba329aa342a..63425cab7be 100644 --- a/bin/migrateDirtyDBtoRealDB.js +++ b/bin/migrateDirtyDBtoRealDB.js @@ -1,6 +1,5 @@ -require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { - - process.chdir(npm.root+'/..') +require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => { + process.chdir(`${npm.root}/..`); // This script requires that you have modified your settings.json file // to work with a real database. Please make a backup of your dirty.db @@ -10,40 +9,40 @@ require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) { // `node --max-old-space-size=4096 bin/migrateDirtyDBtoRealDB.js` - var settings = require("ep_etherpad-lite/node/utils/Settings"); - var dirty = require("../src/node_modules/dirty"); - var ueberDB = require("../src/node_modules/ueberdb2"); - var log4js = require("../src/node_modules/log4js"); - var dbWrapperSettings = { - "cache": "0", // The cache slows things down when you're mostly writing. - "writeInterval": 0 // Write directly to the database, don't buffer + const settings = require('ep_etherpad-lite/node/utils/Settings'); + let dirty = require('../src/node_modules/dirty'); + const ueberDB = require('../src/node_modules/ueberdb2'); + const log4js = require('../src/node_modules/log4js'); + const dbWrapperSettings = { + cache: '0', // The cache slows things down when you're mostly writing. + writeInterval: 0, // Write directly to the database, don't buffer }; - var db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger("ueberDB")); - var i = 0; - var length = 0; - - db.init(function() { - console.log("Waiting for dirtyDB to parse its file."); - dirty = dirty('var/dirty.db').on("load", function() { - dirty.forEach(function(){ + const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); + let i = 0; + let length = 0; + + db.init(() => { + console.log('Waiting for dirtyDB to parse its file.'); + dirty = dirty('var/dirty.db').on('load', () => { + dirty.forEach(() => { length++; }); console.log(`Found ${length} records, processing now.`); - dirty.forEach(async function(key, value) { - let error = await db.set(key, value); + dirty.forEach(async (key, value) => { + const error = await db.set(key, value); console.log(`Wrote record ${i}`); i++; if (i === length) { - console.log("finished, just clearing up for a bit..."); - setTimeout(function() { + console.log('finished, just clearing up for a bit...'); + setTimeout(() => { process.exit(0); }, 5000); } }); - console.log("Please wait for all records to flush to database, then kill this process."); + console.log('Please wait for all records to flush to database, then kill this process.'); }); - console.log("done?") + console.log('done?'); }); }); diff --git a/bin/plugins/README.md b/bin/plugins/README.md index dc929798c29..81d5a42988f 100755 --- a/bin/plugins/README.md +++ b/bin/plugins/README.md @@ -1,46 +1,52 @@ -The files in this folder are for Plugin developers. - -# Get suggestions to improve your Plugin - -This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project. - -``` -node bin/plugins/checkPlugin.js $PLUGIN_NAME$ -``` - -# Basic Example: -``` -node bin/plugins/checkPlugin.js ep_webrtc -``` - -## Autofixing - will autofix any issues it can -``` -node bin/plugins/checkPlugins.js ep_whatever autofix -``` - -## Autocommitting, push, npm minor patch and npm publish (highly dangerous) -``` -node bin/plugins/checkPlugins.js ep_whatever autofix autocommit -``` - -# All the plugins -Replace johnmclear with your github username - -``` -# Clones -cd node_modules -GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone -cd .. - -# autofixes and autocommits /pushes & npm publishes -for dir in `ls node_modules`; -do -# echo $0 -if [[ $dir == *"ep_"* ]]; then -if [[ $dir != "ep_etherpad-lite" ]]; then -node bin/plugins/checkPlugin.js $dir autofix autocommit -fi -fi -# echo $dir -done -``` +The files in this folder are for Plugin developers. + +# Get suggestions to improve your Plugin + +This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project. + +``` +node bin/plugins/checkPlugin.js $PLUGIN_NAME$ +``` + +# Basic Example: +``` +node bin/plugins/checkPlugin.js ep_webrtc +``` + +## Autofixing - will autofix any issues it can +``` +node bin/plugins/checkPlugins.js ep_whatever autofix +``` + +## Autocommitting, push, npm minor patch and npm publish (highly dangerous) +``` +node bin/plugins/checkPlugins.js ep_whatever autofix autocommit +``` + +# All the plugins +Replace johnmclear with your github username + +``` +# Clones +cd node_modules +GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone +cd .. + +# autofixes and autocommits /pushes & npm publishes +for dir in `ls node_modules`; +do +# echo $0 +if [[ $dir == *"ep_"* ]]; then +if [[ $dir != "ep_etherpad-lite" ]]; then +node bin/plugins/checkPlugin.js $dir autofix autocommit +fi +fi +# echo $dir +done +``` + +# Automating update of ether organization plugins +``` +getCorePlugins.sh +updateCorePlugins.sh +``` diff --git a/bin/plugins/checkPlugin.js b/bin/plugins/checkPlugin.js index 0fccb4f1203..fd31c148e9e 100755 --- a/bin/plugins/checkPlugin.js +++ b/bin/plugins/checkPlugin.js @@ -1,246 +1,469 @@ -// pro usage for all your plugins, replace johnmclear with your github username -/* -cd node_modules -GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone -cd .. - -for dir in `ls node_modules`; -do -# echo $0 -if [[ $dir == *"ep_"* ]]; then -if [[ $dir != "ep_etherpad-lite" ]]; then -node bin/plugins/checkPlugin.js $dir autofix autocommit -fi -fi -# echo $dir -done -*/ - -/* -* -* Usage -* -* Normal usage: node bin/plugins/checkPlugins.js ep_whatever -* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix -* Auto commit, push and publish(to npm) * highly dangerous: -node bin/plugins/checkPlugins.js ep_whatever autofix autocommit - -*/ - -const fs = require("fs"); -const { exec } = require("child_process"); - -// get plugin name & path from user input -const pluginName = process.argv[2]; -const pluginPath = "node_modules/"+pluginName; - -console.log("Checking the plugin: "+ pluginName) - -// Should we autofix? -if (process.argv[3] && process.argv[3] === "autofix") var autoFix = true; - -// Should we update files where possible? -if (process.argv[5] && process.argv[5] === "autoupdate") var autoUpdate = true; - -// Should we automcommit and npm publish?! -if (process.argv[4] && process.argv[4] === "autocommit") var autoCommit = true; - - -if(autoCommit){ - console.warn("Auto commit is enabled, I hope you know what you are doing...") -} - -fs.readdir(pluginPath, function (err, rootFiles) { - //handling error - if (err) { - return console.log('Unable to scan directory: ' + err); - } - - // rewriting files to lower case - var files = []; - - // some files we need to know the actual file name. Not compulsory but might help in the future. - var readMeFileName; - var repository; - var hasAutofixed = false; - - for (var i = 0; i < rootFiles.length; i++) { - if(rootFiles[i].toLowerCase().indexOf("readme") !== -1) readMeFileName = rootFiles[i]; - files.push(rootFiles[i].toLowerCase()); - } - - if(files.indexOf("package.json") === -1){ - console.warn("no package.json, please create"); - } - - if(files.indexOf("package.json") !== -1){ - let packageJSON = fs.readFileSync(pluginPath+"/package.json", {encoding:'utf8', flag:'r'}); - - if(packageJSON.toLowerCase().indexOf("repository") === -1){ - console.warn("No repository in package.json"); - if(autoFix){ - console.warn("Repository not detected in package.json. Please add repository section manually.") - } - }else{ - // useful for creating README later. - repository = JSON.parse(packageJSON).repository.url; - } - - } - if(files.indexOf("readme") === -1 && files.indexOf("readme.md") === -1){ - console.warn("README.md file not found, please create"); - if(autoFix){ - console.log("Autofixing missing README.md file, please edit the README.md file further to include plugin specific details."); - let readme = fs.readFileSync("bin/plugins/lib/README.md", {encoding:'utf8', flag:'r'}) - readme = readme.replace(/\[plugin_name\]/g, pluginName); - if(repository){ - let org = repository.split("/")[3]; - let name = repository.split("/")[4]; - readme = readme.replace(/\[org_name\]/g, org); - readme = readme.replace(/\[repo_url\]/g, name); - fs.writeFileSync(pluginPath+"/README.md", readme); - }else{ - console.warn("Unable to find repository in package.json, aborting.") - } - } - } - - if(files.indexOf("readme") !== -1 && files.indexOf("readme.md") !== -1){ - let readme = fs.readFileSync(pluginPath+"/"+readMeFileName, {encoding:'utf8', flag:'r'}); - if(readme.toLowerCase().indexOf("license") === -1){ - console.warn("No license section in README"); - if(autoFix){ - console.warn("Please add License section to README manually.") - } - } - } - - if(files.indexOf("license") === -1 && files.indexOf("license.md") === -1){ - console.warn("LICENSE.md file not found, please create"); - if(autoFix){ - hasAutofixed = true; - console.log("Autofixing missing LICENSE.md file, including Apache 2 license."); - exec("git config user.name", (error, name, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - let license = fs.readFileSync("bin/plugins/lib/LICENSE.md", {encoding:'utf8', flag:'r'}); - license = license.replace("[yyyy]", new Date().getFullYear()); - license = license.replace("[name of copyright owner]", name) - fs.writeFileSync(pluginPath+"/LICENSE.md", license); - }); - } - } - - var travisConfig = fs.readFileSync("bin/plugins/lib/travis.yml", {encoding:'utf8', flag:'r'}); - travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName); - - if(files.indexOf(".travis.yml") === -1){ - console.warn(".travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.") - // TODO: Make it check version of the .travis file to see if it needs an update. - if(autoFix){ - hasAutofixed = true; - console.log("Autofixing missing .travis.yml file"); - fs.writeFileSync(pluginPath+"/.travis.yml", travisConfig); - console.log("Travis file created, please sign into travis and enable this repository") - } - } - if(autoFix && autoUpdate){ - // checks the file versioning of .travis and updates it to the latest. - let existingConfig = fs.readFileSync(pluginPath + "/.travis.yml", {encoding:'utf8', flag:'r'}); - let existingConfigLocation = existingConfig.indexOf("##ETHERPAD_TRAVIS_V="); - let existingValue = existingConfig.substr(existingConfigLocation+20, existingConfig.length); - - let newConfigLocation = travisConfig.indexOf("##ETHERPAD_TRAVIS_V="); - let newValue = travisConfig.substr(newConfigLocation+20, travisConfig.length); - - if(existingConfigLocation === -1){ - console.warn("no previous .travis.yml version found so writing new.") - // we will write the newTravisConfig to the location. - fs.writeFileSync(pluginPath + "/.travis.yml", travisConfig); - }else{ - if(newValue > existingValue){ - console.log("updating .travis.yml"); - fs.writeFileSync(pluginPath + "/.travis.yml", travisConfig); - hasAutofixed = true; - } - } - } - - if(files.indexOf(".gitignore") === -1){ - console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository.") - if(autoFix){ - hasAutofixed = true; - console.log("Autofixing missing .gitignore file"); - let gitignore = fs.readFileSync("bin/plugins/lib/gitignore", {encoding:'utf8', flag:'r'}); - fs.writeFileSync(pluginPath+"/.gitignore", gitignore); - } - } - - if(files.indexOf("locales") === -1){ - console.warn("Translations not found, please create. Translation files help with Etherpad accessibility."); - } - - - if(files.indexOf(".ep_initialized") !== -1){ - console.warn(".ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.") - if(autoFix){ - hasAutofixed = true; - console.log("Autofixing incorrectly existing .ep_initialized file"); - fs.unlinkSync(pluginPath+"/.ep_initialized"); - } - } - - if(files.indexOf("npm-debug.log") !== -1){ - console.warn("npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.") - if(autoFix){ - hasAutofixed = true; - console.log("Autofixing incorrectly existing npm-debug.log file"); - fs.unlinkSync(pluginPath+"/npm-debug.log"); - } - } - - if(files.indexOf("static") !== -1){ - fs.readdir(pluginPath+"/static", function (errRead, staticFiles) { - if(staticFiles.indexOf("tests") === -1){ - console.warn("Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin") - } - }) - }else{ - console.warn("Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin") - } - - if(hasAutofixed){ - console.log("Fixes applied, please check git diff then run the following command:\n\n") - // bump npm Version - if(autoCommit){ - // holy shit you brave. - console.log("Attempting autocommit and auto publish to npm") - exec("cd node_modules/"+ pluginName + " && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..", (error, name, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log("I think she's got it! By George she's got it!") - process.exit(0) - }); - }else{ - console.log("cd node_modules/"+ pluginName + " && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..") - } - } - - //listing all files using forEach - files.forEach(function (file) { - // Do whatever you want to do with the file - // console.log(file.toLowerCase()); - }); -}); +/* +* +* Usage -- see README.md +* +* Normal usage: node bin/plugins/checkPlugins.js ep_whatever +* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix +* Auto commit, push and publish(to npm) * highly dangerous: +node bin/plugins/checkPlugins.js ep_whatever autofix autocommit + +*/ + +const fs = require('fs'); +const {exec} = require('child_process'); + +// get plugin name & path from user input +const pluginName = process.argv[2]; + +if (!pluginName) { + console.error('no plugin name specified'); + process.exit(1); +} + +const pluginPath = `node_modules/${pluginName}`; + +console.log(`Checking the plugin: ${pluginName}`); + +// Should we autofix? +if (process.argv[3] && process.argv[3] === 'autofix') var autoFix = true; + +// Should we update files where possible? +if (process.argv[5] && process.argv[5] === 'autoupdate') var autoUpdate = true; + +// Should we automcommit and npm publish?! +if (process.argv[4] && process.argv[4] === 'autocommit') var autoCommit = true; + + +if (autoCommit) { + console.warn('Auto commit is enabled, I hope you know what you are doing...'); +} + +fs.readdir(pluginPath, (err, rootFiles) => { + // handling error + if (err) { + return console.log(`Unable to scan directory: ${err}`); + } + + // rewriting files to lower case + const files = []; + + // some files we need to know the actual file name. Not compulsory but might help in the future. + let readMeFileName; + let repository; + let hasAutoFixed = false; + + for (let i = 0; i < rootFiles.length; i++) { + if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i]; + files.push(rootFiles[i].toLowerCase()); + } + + if (files.indexOf('.git') === -1) { + console.error('No .git folder, aborting'); + process.exit(1); + } + + // do a git pull... + var child_process = require('child_process'); + try { + child_process.execSync('git pull ', {cwd: `${pluginPath}/`}); + } catch (e) { + console.error('Error git pull', e); + } + + try { + const path = `${pluginPath}/.github/workflows/npmpublish.yml`; + if (!fs.existsSync(path)) { + console.log('no .github/workflows/npmpublish.yml, create one and set npm secret to auto publish to npm on commit'); + if (autoFix) { + const npmpublish = + fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, npmpublish); + hasAutoFixed = true; + console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo"); + } else { + console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo'); + } + } else { + // autopublish exists, we should check the version.. + // checkVersion takes two file paths and checks for a version string in them. + const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); + const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); + const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); + + const reqVersionFile = fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); + const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); + + if (!existingValue || (reqValue > existingValue)) { + const npmpublish = + fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, npmpublish); + hasAutoFixed = true; + } + } + } catch (err) { + console.error(err); + } + + + try { + const path = `${pluginPath}/.github/workflows/backend-tests.yml`; + if (!fs.existsSync(path)) { + console.log('no .github/workflows/backend-tests.yml, create one and set npm secret to auto publish to npm on commit'); + if (autoFix) { + const backendTests = + fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, backendTests); + hasAutoFixed = true; + } + } else { + // autopublish exists, we should check the version.. + // checkVersion takes two file paths and checks for a version string in them. + const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'}); + const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V='); + const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length)); + + const reqVersionFile = fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V='); + const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length)); + + if (!existingValue || (reqValue > existingValue)) { + const backendTests = + fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); + fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); + fs.writeFileSync(path, backendTests); + hasAutoFixed = true; + } + } + } catch (err) { + console.error(err); + } + + if (files.indexOf('package.json') === -1) { + console.warn('no package.json, please create'); + } + + if (files.indexOf('package.json') !== -1) { + const packageJSON = fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'}); + const parsedPackageJSON = JSON.parse(packageJSON); + if (autoFix) { + let updatedPackageJSON = false; + if (!parsedPackageJSON.funding) { + updatedPackageJSON = true; + parsedPackageJSON.funding = { + type: 'individual', + url: 'https://etherpad.org/', + }; + } + if (updatedPackageJSON) { + hasAutoFixed = true; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + } + } + + if (packageJSON.toLowerCase().indexOf('repository') === -1) { + console.warn('No repository in package.json'); + if (autoFix) { + console.warn('Repository not detected in package.json. Please add repository section manually.'); + } + } else { + // useful for creating README later. + repository = parsedPackageJSON.repository.url; + } + + // include lint config + if (packageJSON.toLowerCase().indexOf('devdependencies') === -1 || !parsedPackageJSON.devDependencies.eslint) { + console.warn('Missing eslint reference in devDependencies'); + if (autoFix) { + const devDependencies = { + 'eslint': '^7.14.0', + 'eslint-config-etherpad': '^1.0.13', + 'eslint-plugin-mocha': '^8.0.0', + 'eslint-plugin-node': '^11.1.0', + 'eslint-plugin-prefer-arrow': '^1.2.2', + 'eslint-plugin-promise': '^4.2.1', + }; + hasAutoFixed = true; + parsedPackageJSON.devDependencies = devDependencies; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + + const child_process = require('child_process'); + try { + child_process.execSync('npm install', {cwd: `${pluginPath}/`}); + hasAutoFixed = true; + } catch (e) { + console.error('Failed to create package-lock.json'); + } + } + } + + // include peer deps config + if (packageJSON.toLowerCase().indexOf('peerdependencies') === -1 || !parsedPackageJSON.peerDependencies) { + console.warn('Missing peer deps reference in package.json'); + if (autoFix) { + const peerDependencies = { + 'ep_etherpad-lite': '>=1.8.6', + }; + hasAutoFixed = true; + parsedPackageJSON.peerDependencies = peerDependencies; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + const child_process = require('child_process'); + try { + child_process.execSync('npm install --no-save ep_etherpad-lite@file:../../src', {cwd: `${pluginPath}/`}); + hasAutoFixed = true; + } catch (e) { + console.error('Failed to create package-lock.json'); + } + } + } + + if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) { + console.warn('No esLintConfig in package.json'); + if (autoFix) { + const eslintConfig = { + root: true, + extends: 'etherpad/plugin', + }; + hasAutoFixed = true; + parsedPackageJSON.eslintConfig = eslintConfig; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + } + } + + if (packageJSON.toLowerCase().indexOf('scripts') === -1) { + console.warn('No scripts in package.json'); + if (autoFix) { + const scripts = { + 'lint': 'eslint .', + 'lint:fix': 'eslint --fix .', + }; + hasAutoFixed = true; + parsedPackageJSON.scripts = scripts; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + } + } + + if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) { + console.warn('No engines or node engine in package.json'); + if (autoFix) { + const engines = { + node: '>=10.13.0', + }; + hasAutoFixed = true; + parsedPackageJSON.engines = engines; + fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2)); + } + } + } + + if (files.indexOf('package-lock.json') === -1) { + console.warn('package-lock.json file not found. Please run npm install in the plugin folder and commit the package-lock.json file.'); + if (autoFix) { + var child_process = require('child_process'); + try { + child_process.execSync('npm install', {cwd: `${pluginPath}/`}); + console.log('Making package-lock.json'); + hasAutoFixed = true; + } catch (e) { + console.error('Failed to create package-lock.json'); + } + } + } + + if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) { + console.warn('README.md file not found, please create'); + if (autoFix) { + console.log('Autofixing missing README.md file, please edit the README.md file further to include plugin specific details.'); + let readme = fs.readFileSync('bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'}); + readme = readme.replace(/\[plugin_name\]/g, pluginName); + if (repository) { + const org = repository.split('/')[3]; + const name = repository.split('/')[4]; + readme = readme.replace(/\[org_name\]/g, org); + readme = readme.replace(/\[repo_url\]/g, name); + fs.writeFileSync(`${pluginPath}/README.md`, readme); + } else { + console.warn('Unable to find repository in package.json, aborting.'); + } + } + } + + if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) { + console.warn('CONTRIBUTING.md file not found, please create'); + if (autoFix) { + console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md file further to include plugin specific details.'); + let contributing = fs.readFileSync('bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'}); + contributing = contributing.replace(/\[plugin_name\]/g, pluginName); + fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing); + } + } + + + if (files.indexOf('readme') !== -1 && files.indexOf('readme.md') !== -1) { + const readme = fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'}); + if (readme.toLowerCase().indexOf('license') === -1) { + console.warn('No license section in README'); + if (autoFix) { + console.warn('Please add License section to README manually.'); + } + } + } + + if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) { + console.warn('LICENSE.md file not found, please create'); + if (autoFix) { + hasAutoFixed = true; + console.log('Autofixing missing LICENSE.md file, including Apache 2 license.'); + exec('git config user.name', (error, name, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + return; + } + let license = fs.readFileSync('bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'}); + license = license.replace('[yyyy]', new Date().getFullYear()); + license = license.replace('[name of copyright owner]', name); + fs.writeFileSync(`${pluginPath}/LICENSE.md`, license); + }); + } + } + + let travisConfig = fs.readFileSync('bin/plugins/lib/travis.yml', {encoding: 'utf8', flag: 'r'}); + travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName); + + if (files.indexOf('.travis.yml') === -1) { + console.warn('.travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.'); + // TODO: Make it check version of the .travis file to see if it needs an update. + if (autoFix) { + hasAutoFixed = true; + console.log('Autofixing missing .travis.yml file'); + fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); + console.log('Travis file created, please sign into travis and enable this repository'); + } + } + if (autoFix && autoUpdate) { + // checks the file versioning of .travis and updates it to the latest. + const existingConfig = fs.readFileSync(`${pluginPath}/.travis.yml`, {encoding: 'utf8', flag: 'r'}); + const existingConfigLocation = existingConfig.indexOf('##ETHERPAD_TRAVIS_V='); + const existingValue = parseInt(existingConfig.substr(existingConfigLocation + 20, existingConfig.length)); + + const newConfigLocation = travisConfig.indexOf('##ETHERPAD_TRAVIS_V='); + const newValue = parseInt(travisConfig.substr(newConfigLocation + 20, travisConfig.length)); + if (existingConfigLocation === -1) { + console.warn('no previous .travis.yml version found so writing new.'); + // we will write the newTravisConfig to the location. + fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); + } else if (newValue > existingValue) { + console.log('updating .travis.yml'); + fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig); + hasAutoFixed = true; + }// + } + + if (files.indexOf('.gitignore') === -1) { + console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository."); + if (autoFix) { + hasAutoFixed = true; + console.log('Autofixing missing .gitignore file'); + const gitignore = fs.readFileSync('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'}); + fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + } + } else { + let gitignore = + fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'}); + if (gitignore.indexOf('node_modules/') === -1) { + console.warn('node_modules/ missing from .gitignore'); + if (autoFix) { + gitignore += 'node_modules/'; + fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + hasAutoFixed = true; + } + } + } + + // if we include templates but don't have translations... + if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) { + console.warn('Translations not found, please create. Translation files help with Etherpad accessibility.'); + } + + + if (files.indexOf('.ep_initialized') !== -1) { + console.warn('.ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.'); + if (autoFix) { + hasAutoFixed = true; + console.log('Autofixing incorrectly existing .ep_initialized file'); + fs.unlinkSync(`${pluginPath}/.ep_initialized`); + } + } + + if (files.indexOf('npm-debug.log') !== -1) { + console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.'); + if (autoFix) { + hasAutoFixed = true; + console.log('Autofixing incorrectly existing npm-debug.log file'); + fs.unlinkSync(`${pluginPath}/npm-debug.log`); + } + } + + if (files.indexOf('static') !== -1) { + fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => { + if (staticFiles.indexOf('tests') === -1) { + console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); + } + }); + } else { + console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin'); + } + + // linting begins + if (autoFix) { + var lintCmd = 'npm run lint:fix'; + } else { + var lintCmd = 'npm run lint'; + } + + try { + child_process.execSync(lintCmd, {cwd: `${pluginPath}/`}); + console.log('Linting...'); + if (autoFix) { + // todo: if npm run lint doesn't do anything no need for... + hasAutoFixed = true; + } + } catch (e) { + // it is gonna throw an error anyway + console.log('Manual linting probably required, check with: npm run lint'); + } + // linting ends. + + if (hasAutoFixed) { + console.log('Fixes applied, please check git diff then run the following command:\n\n'); + // bump npm Version + if (autoCommit) { + // holy shit you brave. + console.log('Attempting autocommit and auto publish to npm'); + // github should push to npm for us :) + exec(`cd node_modules/${pluginName} && git rm -rf node_modules --ignore-unmatch && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && git push && cd ../..`, (error, name, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + return; + } + console.log("I think she's got it! By George she's got it!"); + process.exit(0); + }); + } else { + console.log(`cd node_modules/${pluginName} && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..`); + } + } + + console.log('Finished'); +}); diff --git a/bin/plugins/getCorePlugins.sh b/bin/plugins/getCorePlugins.sh new file mode 100755 index 00000000000..e8ce68b21f5 --- /dev/null +++ b/bin/plugins/getCorePlugins.sh @@ -0,0 +1,4 @@ +cd node_modules/ +GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone +GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100&page=2&" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone +GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100&page=3&" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone diff --git a/bin/plugins/lib/CONTRIBUTING.md b/bin/plugins/lib/CONTRIBUTING.md new file mode 100644 index 00000000000..724e02ac021 --- /dev/null +++ b/bin/plugins/lib/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributor Guidelines +(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch)) + +## Pull requests + +* the commit series in the PR should be _linear_ (it **should not contain merge commits**). This is necessary because we want to be able to [bisect](https://en.wikipedia.org/wiki/Bisection_(software_engineering)) bugs easily. Rewrite history/perform a rebase if necessary +* PRs should be issued against the **develop** branch: we never pull directly into **master** +* PRs **should not have conflicts** with develop. If there are, please resolve them rebasing and force-pushing +* when preparing your PR, please make sure that you have included the relevant **changes to the documentation** (preferably with usage examples) +* contain meaningful and detailed **commit messages** in the form: + ``` + submodule: description + + longer description of the change you have made, eventually mentioning the + 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 +* 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 + * output a `WARN` in the log informing that the feature is going to be removed + * remove the feature in the next version +* if you want to add a new feature, put it under a **feature flag**: + * once the new feature has reached a minimal level of stability, do a PR for it, so it can be integrated early + * expose a mechanism for enabling/disabling the feature + * the new feature should be **disabled** by default. With the feature disabled, the code path should be exactly the same as before your contribution. This is a __necessary condition__ for early integration +* think of the PR not as something that __you wrote__, but as something that __someone else is going to read__. The commit series in the PR should tell a novice developer the story of your thoughts when developing it + +## How to write a bug report + +* Please be polite, we all are humans and problems can occur. +* Please add as much information as possible, for example + * client os(s) and version(s) + * browser(s) and version(s), is the problem reproducible on different clients + * special environments like firewalls or antivirus + * host os and version + * npm and nodejs version + * Logfiles if available + * steps to reproduce + * what you expected to happen + * what actually happened +* Please format logfiles and code examples with markdown see github Markdown help below the issue textarea for more information. + +If you send logfiles, please set the loglevel switch DEBUG in your settings.json file: + +``` +/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ + "loglevel": "DEBUG", +``` + +The logfile location is defined in startup script or the log is directly shown in the commandline after you have started etherpad. + +## General goals of Etherpad +To make sure everybody is going in the same direction: +* easy to install for admins and easy to use for people +* easy to integrate into other apps, but also usable as standalone +* lightweight and scalable +* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core. +Also, keep it maintainable. We don't wanna end up as the monster Etherpad was! + +## How to work with git? +* Don't work in your master branch. +* Make a new branch for every feature you're working on. (This ensures that you can work you can do lots of small, independent pull requests instead of one big one with complete different features) +* Don't use the online edit function of github (this only creates ugly and not working commits!) +* Try to make clean commits that are easy readable (including descriptive commit messages!) +* Test before you push. Sounds easy, it isn't! +* Don't check in stuff that gets generated during build or runtime +* Make small pull requests that are easy to review but make sure they do add value by themselves / individually + +## Coding style +* Do write comments. (You don't have to comment every line, but if you come up with something that's a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless!) +* Never ever use tabs +* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces +* Don't overengineer. Don't try to solve any possible problem in one step, but try to solve problems as easy as possible and improve the solution over time! +* Do generalize sooner or later! (if an old solution, quickly hacked together, poses more problems than it solves today, refactor it!) +* Keep it compatible. Do not introduce changes to the public API, db schema or configurations too lightly. Don't make incompatible changes without good reasons! +* If you do make changes, document them! (see below) +* Use protocol independent urls "//" + +## Branching model / git workflow +see git flow http://nvie.com/posts/a-successful-git-branching-model/ + +### `master` branch +* the stable +* This is the branch everyone should use for production stuff + +### `develop`branch +* everything that is READY to go into master at some point in time +* This stuff is tested and ready to go out + +### release branches +* stuff that should go into master very soon +* only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why) +* we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle. + +### hotfix branches +* fixes for bugs in master + +### feature branches (in your own repos) +* these are the branches where you develop your features in +* If it's ready to go out, it will be merged into develop + +Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop + +## Documentation +The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision. + +Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request. + +You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet. + +## Testing +Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `/tests/frontend`. + +Back-end tests can be run from the `src` directory, via `npm test`. + +## Things you can help with +Etherpad is much more than software. So if you aren't a developer then worry not, there is still a LOT you can do! A big part of what we do is community engagement. You can help in the following ways + * Triage bugs (applying labels) and confirming their existence + * Testing fixes (simply applying them and seeing if it fixes your issue or not) - Some git experience required + * Notifying large site admins of new releases + * Writing Changelogs for releases + * Creating Windows packages + * Creating releases + * Bumping dependencies periodically and checking they don't break anything + * Write proposals for grants + * Co-Author and Publish CVEs + * Work with SFC to maintain legal side of project + * Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS + diff --git a/bin/plugins/lib/LICENSE.md b/bin/plugins/lib/LICENSE.md index 8cb6bc0c609..004c62e1b1e 100755 --- a/bin/plugins/lib/LICENSE.md +++ b/bin/plugins/lib/LICENSE.md @@ -1,13 +1,13 @@ -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/bin/plugins/lib/README.md b/bin/plugins/lib/README.md index c3a3b1fbf37..3a1e2619330 100755 --- a/bin/plugins/lib/README.md +++ b/bin/plugins/lib/README.md @@ -1,28 +1,29 @@ -[![Travis (.org)](https://api.travis-ci.org/[org_name]/[repo_url].svg?branch=develop)](https://travis-ci.org/github/[org_name]/[repo_url]) - -# My awesome plugin README example -Explain what your plugin does and who it's useful for. - -## Example animated gif of usage if appropriate - -## Installing -npm install [plugin_name] - -or Use the Etherpad ``/admin`` interface. - -## Settings -Document settings if any - -## Testing -Document how to run backend / frontend tests. - -### Frontend - -Visit http://whatever/tests/frontend/ to run the frontend tests. - -### backend - -Type ``cd src && npm run test`` to run the backend tests. - -## LICENSE -Apache 2.0 +[![Travis (.com)](https://api.travis-ci.com/[org_name]/[repo_url].svg?branch=develop)](https://travis-ci.com/github/[org_name]/[repo_url]) + +# My awesome plugin README example +Explain what your plugin does and who it's useful for. + +## Example animated gif of usage if appropriate +![screenshot](https://user-images.githubusercontent.com/220864/99979953-97841d80-2d9f-11eb-9782-5f65817c58f4.PNG) + +## Installing +npm install [plugin_name] + +or Use the Etherpad ``/admin`` interface. + +## Settings +Document settings if any + +## Testing +Document how to run backend / frontend tests. + +### Frontend + +Visit http://whatever/tests/frontend/ to run the frontend tests. + +### backend + +Type ``cd src && npm run test`` to run the backend tests. + +## LICENSE +Apache 2.0 diff --git a/bin/plugins/lib/backend-tests.yml b/bin/plugins/lib/backend-tests.yml new file mode 100644 index 00000000000..324cc4baf0a --- /dev/null +++ b/bin/plugins/lib/backend-tests.yml @@ -0,0 +1,51 @@ +# You need to change lines 38 and 46 in case the plugin's name on npmjs.com is different +# from the repository name + +name: "Backend tests" + +# any branch is useful for testing before a PR is submitted +on: [push, pull_request] + +jobs: + withplugins: + # run on pushes to any branch + # run on PRs from external forks + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: with Plugins + runs-on: ubuntu-latest + + steps: + - name: Install libreoffice + run: | + sudo add-apt-repository -y ppa:libreoffice/ppa + sudo apt update + sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + + # clone etherpad-lite + - name: Install etherpad core + uses: actions/checkout@v2 + with: + repository: ether/etherpad-lite + + - name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + + # clone this repository into node_modules/ep_plugin-name + - name: Checkout plugin repository + uses: actions/checkout@v2 + with: + path: ./node_modules/${{github.event.repository.name}} + + - name: Install plugin dependencies + run: | + cd node_modules/${{github.event.repository.name}} + npm ci + + # configures some settings and runs npm run test + - name: Run the backend tests + run: tests/frontend/travis/runnerBackend.sh + +##ETHERPAD_NPM_V=1 +## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh diff --git a/bin/plugins/lib/gitignore b/bin/plugins/lib/gitignore index f6d13a09674..0719a85c1bd 100755 --- a/bin/plugins/lib/gitignore +++ b/bin/plugins/lib/gitignore @@ -1,5 +1,5 @@ -.ep_initialized -.DS_Store -node_modules/ -node_modules -npm-debug.log +.ep_initialized +.DS_Store +node_modules/ +node_modules +npm-debug.log diff --git a/bin/plugins/lib/npmpublish.yml b/bin/plugins/lib/npmpublish.yml new file mode 100644 index 00000000000..8d94ce88ae0 --- /dev/null +++ b/bin/plugins/lib/npmpublish.yml @@ -0,0 +1,73 @@ +# This workflow will run tests using node and then publish a package to the npm registry when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js Package + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + # Clone ether/etherpad-lite to ../etherpad-lite so that ep_etherpad-lite + # can be "installed" in this plugin's node_modules. The checkout v2 action + # doesn't support cloning outside of $GITHUB_WORKSPACE (see + # https://github.com/actions/checkout/issues/197), so the repo is first + # cloned to etherpad-lite then moved to ../etherpad-lite. To avoid + # conflicts with this plugin's clone, etherpad-lite must be cloned and + # moved out before this plugin's repo is cloned to $GITHUB_WORKSPACE. + - uses: actions/checkout@v2 + with: + repository: ether/etherpad-lite + path: etherpad-lite + - run: mv etherpad-lite .. + # etherpad-lite has been moved outside of $GITHUB_WORKSPACE, so it is now + # safe to clone this plugin's repo to $GITHUB_WORKSPACE. + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + # All of ep_etherpad-lite's devDependencies are installed because the + # plugin might do `require('ep_etherpad-lite/node_modules/${devDep}')`. + # Eventually it would be nice to create an ESLint plugin that prohibits + # Etherpad plugins from piggybacking off of ep_etherpad-lite's + # devDependencies. If we had that, we could change this line to only + # install production dependencies. + - run: cd ../etherpad-lite/src && npm ci + - run: npm ci + # This runs some sanity checks and creates a symlink at + # node_modules/ep_etherpad-lite that points to ../../etherpad-lite/src. + # This step must be done after `npm ci` installs the plugin's dependencies + # because npm "helpfully" cleans up such symlinks. :( Installing + # ep_etherpad-lite in the plugin's node_modules prevents lint errors and + # unit test failures if the plugin does `require('ep_etherpad-lite/foo')`. + - run: npm install --no-save ep_etherpad-lite@file:../etherpad-lite/src + - run: npm test + - run: npm run lint + + publish-npm: + if: github.event_name == 'push' + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: git config user.name 'github-actions[bot]' + - run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + - run: npm ci + - run: npm version patch + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - run: git push --follow-tags + +##ETHERPAD_NPM_V=1 +## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh diff --git a/bin/plugins/lib/travis.yml b/bin/plugins/lib/travis.yml old mode 100755 new mode 100644 index 81e7d336e1d..099d7e4459b --- a/bin/plugins/lib/travis.yml +++ b/bin/plugins/lib/travis.yml @@ -1,68 +1,70 @@ -language: node_js - -node_js: - - "lts/*" - -cache: false - -before_install: - - sudo add-apt-repository -y ppa:libreoffice/ppa - - sudo apt-get update - - sudo apt-get -y install libreoffice - - sudo apt-get -y install libreoffice-pdfimport - -services: - - docker - -install: - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - -before_script: - - "tests/frontend/travis/sauce_tunnel.sh" - -script: - - "tests/frontend/travis/runner.sh" - -env: - global: - - secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec=" - - secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g=" - -jobs: - include: - - name: "Run the Backend tests" - install: - - "npm install" - - "mkdir [plugin_name]" - - "mv !([plugin_name]) [plugin_name]" - - "git clone https://github.com/ether/etherpad-lite.git etherpad" - - "cd etherpad" - - "mkdir node_modules" - - "mv ../[plugin_name] node_modules" - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - - "cd src && npm install && cd -" - script: - - "tests/frontend/travis/runnerBackend.sh" - - name: "Test the Frontend" - install: - - "npm install" - - "mkdir [plugin_name]" - - "mv !([plugin_name]) [plugin_name]" - - "git clone https://github.com/ether/etherpad-lite.git etherpad" - - "cd etherpad" - - "mkdir node_modules" - - "mv ../[plugin_name] node_modules" - - "bin/installDeps.sh" - - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - script: - - "tests/frontend/travis/runner.sh" - -notifications: - irc: - channels: - - "irc.freenode.org#etherpad-lite-dev" - -##ETHERPAD_TRAVIS_V=3 -## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh +language: node_js + +node_js: + - "lts/*" + +cache: false + +services: + - docker + +install: + - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + +#script: +# - "tests/frontend/travis/runner.sh" + +env: + global: + - secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec=" + - secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g=" + +jobs: + include: + - name: "Lint test package-lock" + install: + - "npm install lockfile-lint" + script: + - npx lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm + - name: "Run the Backend tests" + before_install: + - sudo add-apt-repository -y ppa:libreoffice/ppa + - sudo apt-get update + - sudo apt-get -y install libreoffice + - sudo apt-get -y install libreoffice-pdfimport + install: + - "npm install" + - "mkdir [plugin_name]" + - "mv !([plugin_name]) [plugin_name]" + - "git clone https://github.com/ether/etherpad-lite.git etherpad" + - "cd etherpad" + - "mkdir -p node_modules" + - "mv ../[plugin_name] node_modules" + - "bin/installDeps.sh" + - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + - "cd src && npm install && cd -" + script: + - "tests/frontend/travis/runnerBackend.sh" + - name: "Test the Frontend" + before_script: + - "tests/frontend/travis/sauce_tunnel.sh" + install: + - "npm install" + - "mkdir [plugin_name]" + - "mv !([plugin_name]) [plugin_name]" + - "git clone https://github.com/ether/etherpad-lite.git etherpad" + - "cd etherpad" + - "mkdir -p node_modules" + - "mv ../[plugin_name] node_modules" + - "bin/installDeps.sh" + - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" + script: + - "tests/frontend/travis/runner.sh" + +notifications: + irc: + channels: + - "irc.freenode.org#etherpad-lite-dev" + +##ETHERPAD_TRAVIS_V=9 +## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh diff --git a/bin/plugins/updateCorePlugins.sh b/bin/plugins/updateCorePlugins.sh new file mode 100755 index 00000000000..bf4e6b6d652 --- /dev/null +++ b/bin/plugins/updateCorePlugins.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +for dir in node_modules/ep_*; do + dir=${dir#node_modules/} + [ "$dir" != ep_etherpad-lite ] || continue + node bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate +done diff --git a/bin/rebuildPad.js b/bin/rebuildPad.js index 0013718a9c2..12ff218479c 100644 --- a/bin/rebuildPad.js +++ b/bin/rebuildPad.js @@ -3,121 +3,124 @@ known "good" revision. */ -if(process.argv.length != 4 && process.argv.length != 5) { - console.error("Use: node bin/repairPad.js $PADID $REV [$NEWPADID]"); +if (process.argv.length != 4 && process.argv.length != 5) { + console.error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]'); process.exit(1); } -var npm = require("../src/node_modules/npm"); -var async = require("../src/node_modules/async"); -var ueberDB = require("../src/node_modules/ueberdb2"); +const npm = require('../src/node_modules/npm'); +const async = require('../src/node_modules/async'); +const ueberDB = require('../src/node_modules/ueberdb2'); -var padId = process.argv[2]; -var newRevHead = process.argv[3]; -var newPadId = process.argv[4] || padId + "-rebuilt"; +const padId = process.argv[2]; +const newRevHead = process.argv[3]; +const newPadId = process.argv[4] || `${padId}-rebuilt`; -var db, oldPad, newPad, settings; -var AuthorManager, ChangeSet, Pad, PadManager; +let db, oldPad, newPad, settings; +let AuthorManager, ChangeSet, Pad, PadManager; async.series([ - function(callback) { - npm.load({}, function(err) { - if(err) { - console.error("Could not load NPM: " + err) + function (callback) { + npm.load({}, (err) => { + if (err) { + console.error(`Could not load NPM: ${err}`); process.exit(1); } else { callback(); } - }) + }); }, - function(callback) { + function (callback) { // Get a handle into the database db = require('../src/node/db/DB'); db.init(callback); - }, function(callback) { - PadManager = require('../src/node/db/PadManager'); - Pad = require('../src/node/db/Pad').Pad; - // Get references to the original pad and to a newly created pad - // HACK: This is a standalone script, so we want to write everything - // out to the database immediately. The only problem with this is - // that a driver (like the mysql driver) can hardcode these values. - db.db.db.settings = {cache: 0, writeInterval: 0, json: true}; - // Validate the newPadId if specified and that a pad with that ID does - // not already exist to avoid overwriting it. - if (!PadManager.isValidPadId(newPadId)) { - console.error("Cannot create a pad with that id as it is invalid"); - process.exit(1); - } - PadManager.doesPadExists(newPadId, function(err, exists) { - if (exists) { - console.error("Cannot create a pad with that id as it already exists"); - process.exit(1); - } - }); - PadManager.getPad(padId, function(err, pad) { - oldPad = pad; - newPad = new Pad(newPadId); - callback(); - }); - }, function(callback) { + }, + function (callback) { + PadManager = require('../src/node/db/PadManager'); + Pad = require('../src/node/db/Pad').Pad; + // Get references to the original pad and to a newly created pad + // HACK: This is a standalone script, so we want to write everything + // out to the database immediately. The only problem with this is + // that a driver (like the mysql driver) can hardcode these values. + db.db.db.settings = {cache: 0, writeInterval: 0, json: true}; + // Validate the newPadId if specified and that a pad with that ID does + // not already exist to avoid overwriting it. + if (!PadManager.isValidPadId(newPadId)) { + console.error('Cannot create a pad with that id as it is invalid'); + process.exit(1); + } + PadManager.doesPadExists(newPadId, (err, exists) => { + if (exists) { + console.error('Cannot create a pad with that id as it already exists'); + process.exit(1); + } + }); + PadManager.getPad(padId, (err, pad) => { + oldPad = pad; + newPad = new Pad(newPadId); + callback(); + }); + }, + function (callback) { // Clone all Chat revisions - var chatHead = oldPad.chatHead; - for(var i = 0, curHeadNum = 0; i <= chatHead; i++) { - db.db.get("pad:" + padId + ":chat:" + i, function (err, chat) { - db.db.set("pad:" + newPadId + ":chat:" + curHeadNum++, chat); - console.log("Created: Chat Revision: pad:" + newPadId + ":chat:" + curHeadNum); + const chatHead = oldPad.chatHead; + for (var i = 0, curHeadNum = 0; i <= chatHead; i++) { + db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => { + db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat); + console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`); }); } callback(); - }, function(callback) { + }, + function (callback) { // Rebuild Pad from revisions up to and including the new revision head - AuthorManager = require("../src/node/db/AuthorManager"); - Changeset = require("ep_etherpad-lite/static/js/Changeset"); + AuthorManager = require('../src/node/db/AuthorManager'); + Changeset = require('ep_etherpad-lite/static/js/Changeset'); // Author attributes are derived from changesets, but there can also be // non-author attributes with specific mappings that changesets depend on // and, AFAICT, cannot be recreated any other way newPad.pool.numToAttrib = oldPad.pool.numToAttrib; - for(var curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { - db.db.get("pad:" + padId + ":revs:" + curRevNum, function(err, rev) { + for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { + db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => { if (rev.meta) { - throw "The specified revision number could not be found."; + throw 'The specified revision number could not be found.'; } - var newRevNum = ++newPad.head; - var newRevId = "pad:" + newPad.id + ":revs:" + newRevNum; + const newRevNum = ++newPad.head; + const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; db.db.set(newRevId, rev); AuthorManager.addPad(rev.meta.author, newPad.id); newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); - console.log("Created: Revision: pad:" + newPad.id + ":revs:" + newRevNum); + console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); if (newRevNum == newRevHead) { callback(); } }); } - }, function(callback) { + }, + function (callback) { // Add saved revisions up to the new revision head console.log(newPad.head); - var newSavedRevisions = []; - for(var i in oldPad.savedRevisions) { - savedRev = oldPad.savedRevisions[i] + const newSavedRevisions = []; + for (const i in oldPad.savedRevisions) { + savedRev = oldPad.savedRevisions[i]; if (savedRev.revNum <= newRevHead) { newSavedRevisions.push(savedRev); - console.log("Added: Saved Revision: " + savedRev.revNum); + console.log(`Added: Saved Revision: ${savedRev.revNum}`); } } newPad.savedRevisions = newSavedRevisions; callback(); - }, function(callback) { + }, + function (callback) { // Save the source pad - db.db.set("pad:"+newPadId, newPad, function(err) { - console.log("Created: Source Pad: pad:" + newPadId); - newPad.saveToDatabase(); - callback(); + db.db.set(`pad:${newPadId}`, newPad, (err) => { + console.log(`Created: Source Pad: pad:${newPadId}`); + newPad.saveToDatabase().then(() => callback(), callback); }); - } -], function (err) { - if(err) throw err; - else { - console.info("finished"); + }, +], (err) => { + if (err) { throw err; } else { + console.info('finished'); process.exit(0); } }); diff --git a/bin/release.js b/bin/release.js new file mode 100644 index 00000000000..b2c7c1a354a --- /dev/null +++ b/bin/release.js @@ -0,0 +1,65 @@ +'use strict'; +const fs = require('fs'); +const child_process = require('child_process'); +const semver = require('../src/node_modules/semver'); + +/* + +Usage + +node bin/release.js patch + +*/ +const usage = 'node bin/release.js [patch/minor/major] -- example: "node bin/release.js patch"'; + +const release = process.argv[2]; + +if(!release) { + console.log(usage); + throw new Error('No release type included'); +} + +const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'}); +let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'}); +packageJson = JSON.parse(packageJson); +const currentVersion = packageJson.version; + +const newVersion = semver.inc(currentVersion, release); +if(!newVersion) { + console.log(usage); + throw new Error('Unable to generate new version from input'); +} + +const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1; + +if(!changelogIncludesVersion) { + throw new Error('No changelog record for ', newVersion, ' - please create changelog record'); +} + +console.log('Okay looks good, lets create the package.json and package-lock.json'); + +packageJson.version = newVersion; + +fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2)); + +// run npm version `release` where release is patch, minor or major +child_process.execSync('npm install --package-lock-only', {cwd: `src/`}); +// run npm install --package-lock-only <-- required??? + +child_process.execSync(`git checkout -b release/${newVersion}`); +child_process.execSync(`git add src/package.json`); +child_process.execSync(`git add src/package-lock.json`); +child_process.execSync(`git commit -m 'bump version'`); +child_process.execSync(`git push origin release/${newVersion}`); + + +child_process.execSync(`make docs`); +child_process.execSync(`git clone git@github.com:ether/ether.github.com.git`); +child_process.execSync(`cp -R out/doc/ ether.github.com/doc/${newVersion}`); + +console.log('Once merged into master please run the following commands'); +console.log(`git tag -a ${newVersion} && git push origin master`); +console.log(`cd ether.github.com && git add . && git commit -m ${newVersion} docs`); + +console.log('Once the new docs are uploaded then modify the download link on etherpad.org and then pull master onto develop'); +console.log('Finally go public with an announcement via our comms channels :)'); diff --git a/bin/repairPad.js b/bin/repairPad.js index d495baef51b..8408e4b72fa 100644 --- a/bin/repairPad.js +++ b/bin/repairPad.js @@ -2,47 +2,47 @@ * This is a repair tool. It extracts all datas of a pad, removes and inserts them again. */ -console.warn("WARNING: This script must not be used while etherpad is running!"); +console.warn('WARNING: This script must not be used while etherpad is running!'); if (process.argv.length != 3) { - console.error("Use: node bin/repairPad.js $PADID"); + console.error('Use: node bin/repairPad.js $PADID'); process.exit(1); } // get the padID -var padId = process.argv[2]; +const padId = process.argv[2]; -let npm = require("../src/node_modules/npm"); -npm.load({}, async function(er) { +const npm = require('../src/node_modules/npm'); +npm.load({}, async (er) => { if (er) { - console.error("Could not load NPM: " + er) + console.error(`Could not load NPM: ${er}`); process.exit(1); } try { // intialize database - let settings = require('../src/node/utils/Settings'); - let db = require('../src/node/db/DB'); + const settings = require('../src/node/utils/Settings'); + const db = require('../src/node/db/DB'); await db.init(); // get the pad - let padManager = require('../src/node/db/PadManager'); - let pad = await padManager.getPad(padId); + const padManager = require('../src/node/db/PadManager'); + const pad = await padManager.getPad(padId); // accumulate the required keys - let neededDBValues = ["pad:" + padId]; + const neededDBValues = [`pad:${padId}`]; // add all authors - neededDBValues.push(...pad.getAllAuthors().map(author => "globalAuthor:")); + neededDBValues.push(...pad.getAllAuthors().map((author) => 'globalAuthor:')); // add all revisions for (let rev = 0; rev <= pad.head; ++rev) { - neededDBValues.push("pad:" + padId + ":revs:" + rev); + neededDBValues.push(`pad:${padId}:revs:${rev}`); } // add all chat values for (let chat = 0; chat <= pad.chatHead; ++chat) { - neededDBValues.push("pad:" + padId + ":chat:" + chat); + neededDBValues.push(`pad:${padId}:chat:${chat}`); } // @@ -55,21 +55,20 @@ npm.load({}, async function(er) { // // See gitlab issue #3545 // - console.info("aborting [gitlab #3545]"); + console.info('aborting [gitlab #3545]'); process.exit(1); // now fetch and reinsert every key - neededDBValues.forEach(function(key, value) { - console.log("Key: " + key+ ", value: " + value); + neededDBValues.forEach((key, value) => { + console.log(`Key: ${key}, value: ${value}`); db.remove(key); db.set(key, value); }); - console.info("finished"); + console.info('finished'); process.exit(0); - } catch (er) { - if (er.name === "apierror") { + if (er.name === 'apierror') { console.error(er); } else { console.trace(er); diff --git a/bin/run.sh b/bin/run.sh index ff6b3de093c..50bce4bdd55 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -1,13 +1,11 @@ #!/bin/sh -pecho() { printf %s\\n "$*"; } -log() { pecho "$@"; } -error() { log "ERROR: $@" >&2; } -fatal() { error "$@"; exit 1; } - # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. +# Source constants and usefull functions +. bin/functions.sh + ignoreRoot=0 for ARG in "$@"; do if [ "$ARG" = "--root" ]; then @@ -34,4 +32,4 @@ bin/installDeps.sh "$@" || exit 1 log "Starting Etherpad..." SCRIPTPATH=$(pwd -P) -exec node "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@" +exec node $(compute_node_args) "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@" diff --git a/doc/api/hooks_overview.md b/doc/api/hooks_overview.md index 1de547c9009..35a88dbe1b2 100644 --- a/doc/api/hooks_overview.md +++ b/doc/api/hooks_overview.md @@ -4,48 +4,114 @@ A hook function is registered with a hook via the plugin's `ep.json` file. See the Plugins section for details. A hook may have many registered functions from different plugins. -When a hook is invoked, its registered functions are called with three -arguments: +Some hooks call their registered functions one at a time until one of them +returns a value. Others always call all of their registered functions and +combine the results (if applicable). -1. hookName - The name of the hook being invoked. -2. context - An object with some relevant information about the context of the +## Registered hook functions + +Note: The documentation in this section applies to every hook unless the +hook-specific documentation says otherwise. + +### Arguments + +Hook functions are called with three arguments: + +1. `hookName` - The name of the hook being invoked. +2. `context` - An object with some relevant information about the context of the call. See the hook-specific documentation for details. -3. callback - Function to call when done. This callback takes a single argument, - the meaning of which depends on the hook. See the "Return values" section for - general information that applies to most hooks. The value returned by this - callback must be returned by the hook function unless otherwise specified. +3. `cb` - For asynchronous operations this callback can be called to signal + completion and optionally provide a return value. The callback takes a single + argument, the meaning of which depends on the hook (see the "Return values" + section for general information that applies to most hooks). This callback + always returns `undefined`. -## Return values +### Expected behavior -Note: This section applies to every hook unless the hook-specific documentation -says otherwise. +The presence of a callback parameter suggests that every hook function can run +asynchronously. While that is the eventual goal, there are some legacy hooks +that expect their hook functions to provide a value synchronously. For such +hooks, the hook functions must do one of the following: -Hook functions return zero or more values to Etherpad by passing an array to the -provided callback. Hook functions typically provide a single value (array of -length one). If the function does not want to or need to provide a value, it may -pass an empty array or `undefined` (which is treated the same as an empty -array). Hook functions may also provide more than one value (array of length two -or more). +* Call the callback with a non-Promise value (`undefined` is acceptable) and + return `undefined`, in that order. +* Return a non-Promise value other than `undefined` (`null` is acceptable) and + never call the callback. Note that `async` functions *always* return a + Promise, so they must never be used for synchronous hooks. +* Only have two parameters (`hookName` and `context`) and return any non-Promise + value (`undefined` is acceptable). -Some hooks concatenate the arrays provided by its registered functions. For -example, if a hook's registered functions pass `[1, 2]`, `undefined`, `[3, 4]`, -`[]`, and `[5]` to the provided callback, then the hook's return value is `[1, -2, 3, 4, 5]`. +For hooks that permit asynchronous behavior, the hook functions must do one or +more of the following: -Other hooks only use the first non-empty array provided by a registered -function. In this case, each of the hook's registered functions is called one at -a time until one provides a non-empty array. The remaining functions are -skipped. If none of the functions provide a non-empty array, or there are no -registered functions, the hook's return value is `[]`. +* Return `undefined` and call the callback, in either order. +* Return something other than `undefined` (`null` is acceptable) and never call + the callback. Note that `async` functions *always* return a Promise, so they + must never call the callback. +* Only have two parameters (`hookName` and `context`). -Example: +Note that the acceptable behaviors for asynchronous hook functions is a superset +of the acceptable behaviors for synchronous hook functions. -``` -exports.abstractHook = (hookName, context, callback) => { - if (notApplicableToThisPlugin(context)) { - return callback(); - } - const value = doSomeProcessing(context); - return callback([value]); +WARNING: The number of parameters is determined by examining +[Function.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length), +which does not count [default +parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) +or ["rest" +parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters). +To avoid problems, do not use default or rest parameters when defining hook +functions. + +### Return values + +A hook function can provide a value to Etherpad in one of the following ways: + +* Pass the desired value as the first argument to the callback. +* Return the desired value directly. The value must not be `undefined` unless + the hook function only has two parameters. (Hook functions with three + parameters that want to provide `undefined` should instead use the callback.) +* For hooks that permit asynchronous behavior, return a Promise that resolves to + the desired value. +* For hooks that permit asynchronous behavior, pass a Promise that resolves to + the desired value as the first argument to the callback. + +Examples: + +```javascript +exports.exampleOne = (hookName, context, callback) => { + return 'valueOne'; +}; + +exports.exampleTwo = (hookName, context, callback) => { + callback('valueTwo'); + return; +}; + +// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR +exports.exampleThree = (hookName, context, callback) => { + return new Promise('valueThree'); +}; + +// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR +exports.exampleFour = (hookName, context, callback) => { + callback(new Promise('valueFour')); + return; +}; + +// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR +exports.exampleFive = async (hookName, context) => { + // Note that this function is async, so it actually returns a Promise that + // is resolved to 'valueFive'. + return 'valueFive'; }; ``` + +Etherpad collects the values provided by the hook functions into an array, +filters out all `undefined` values, then flattens the array one level. +Flattening one level makes it possible for a hook function to behave as if it +were multiple separate hook functions. + +For example: Suppose a hook has eight registered functions that return the +following values: `1`, `[2]`, `['3a', '3b']` `[[4]]`, `undefined`, +`[undefined]`, `[]`, and `null`. The value returned to the caller of the hook is +`[1, 2, '3a', '3b', [4], undefined, null]`. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index b4ef1e5250a..e13adfa97f5 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -10,6 +10,28 @@ Things in context: Use this hook to receive the global settings in your plugin. +## shutdown +Called from: src/node/server.js + +Things in context: None + +This hook runs before shutdown. Use it to stop timers, close sockets and files, +flush buffers, etc. The database is not available while this hook is running. +The shutdown function must not block for long because there is a short timeout +before the process is forcibly terminated. + +The shutdown function must return a Promise, which must resolve to `undefined`. +Returning `callback(value)` will return a Promise that is resolved to `value`. + +Example: + +``` +// using an async function +exports.shutdown = async (hookName, context) => { + await flushBuffers(); +}; +``` + ## pluginUninstall Called from: src/static/js/pluginfw/installer.js @@ -54,6 +76,25 @@ Things in context: This hook gets called after the application object has been created, but before it starts listening. This is similar to the expressConfigure hook, but it's not guaranteed that the application object will have all relevant configuration variables. +## expressCloseServer + +Called from: src/node/hooks/express.js + +Things in context: Nothing + +This hook is called when the HTTP server is closing, which happens during +shutdown (see the shutdown hook) and when the server restarts (e.g., when a +plugin is installed via the `/admin/plugins` page). The HTTP server may or may +not already be closed when this hook executes. + +Example: + +``` +exports.expressCloseServer = async () => { + await doSomeCleanup(); +}; +``` + ## eejsBlock_`` Called from: src/node/eejs/index.js @@ -96,7 +137,6 @@ Available blocks in `pad.html` are: * `indexCustomStyles` - contains the `index.css` `` tag, allows you to add your own or to customize the one provided by the active skin * `indexWrapper` - contains the form for creating new pads * `indexCustomScripts` - contains the `index.js` `"); + res.send(``); }); -} +}; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6f3cab8a51d..279b08dfaba 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -18,31 +18,32 @@ * limitations under the License. */ - -var padManager = require("../db/PadManager"); -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); -var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager"); -var authorManager = require("../db/AuthorManager"); -var readOnlyManager = require("../db/ReadOnlyManager"); -var settings = require('../utils/Settings'); -var securityManager = require("../db/SecurityManager"); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs.js"); -var log4js = require('log4js'); -var messageLogger = log4js.getLogger("message"); -var accessLogger = log4js.getLogger("access"); -var _ = require('underscore'); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); -var channels = require("channels"); -var stats = require('../stats'); -var remoteAddress = require("../utils/RemoteAddress").remoteAddress; +/* global exports, process, require */ + +const padManager = require('../db/PadManager'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); +const AttributeManager = require('ep_etherpad-lite/static/js/AttributeManager'); +const authorManager = require('../db/AuthorManager'); +const readOnlyManager = require('../db/ReadOnlyManager'); +const settings = require('../utils/Settings'); +const securityManager = require('../db/SecurityManager'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js'); +const log4js = require('log4js'); +const messageLogger = log4js.getLogger('message'); +const accessLogger = log4js.getLogger('access'); +const _ = require('underscore'); +const hooks = require('ep_etherpad-lite/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 nodeify = require('nodeify'); +const {RateLimiterMemory} = require('rate-limiter-flexible'); +const webaccess = require('../hooks/express/webaccess'); const rateLimiter = new RateLimiterMemory({ points: settings.commitRateLimiting.points, - duration: settings.commitRateLimiting.duration + duration: settings.commitRateLimiting.duration, }); /** @@ -53,22 +54,18 @@ const rateLimiter = new RateLimiterMemory({ * readonlyPadId = The readonly pad id of the pad * readonly = Wether the client has only read access (true) or read/write access (false) * rev = That last revision that was send to this client - * author = the author name of this session + * author = the author ID used for this session */ -var sessioninfos = {}; +const sessioninfos = {}; exports.sessioninfos = sessioninfos; // Measure total amount of users -stats.gauge('totalUsers', function() { - return Object.keys(socketio.sockets.sockets).length; -}); +stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); /** * A changeset queue per pad that is processed by handleUserChanges() */ -var padChannels = new channels.channels(function(data, callback) { - return nodeify(handleUserChanges(data), callback); -}); +const padChannels = new channels.channels(({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback)); /** * Saves the Socket class we need to send and receive data from the client @@ -79,107 +76,96 @@ let socketio; * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket */ -exports.setSocketIO = function(socket_io) -{ - socketio=socket_io; -} +exports.setSocketIO = function (socket_io) { + socketio = socket_io; +}; /** * Handles the connection of a new user - * @param client the new client + * @param socket the socket.io Socket object for the new connection from the client */ -exports.handleConnect = function(client) -{ +exports.handleConnect = (socket) => { stats.meter('connects').mark(); // Initalize sessioninfos for this new session - sessioninfos[client.id]={}; -} + sessioninfos[socket.id] = {}; +}; /** * Kicks all sessions from a pad - * @param client the new client */ -exports.kickSessionsFromPad = function(padID) -{ - if(typeof socketio.sockets['clients'] !== 'function') - return; +exports.kickSessionsFromPad = function (padID) { + if (typeof socketio.sockets.clients !== 'function') return; // skip if there is nobody on this pad - if(_getRoomClients(padID).length === 0) - return; + if (_getRoomSockets(padID).length === 0) return; // disconnect everyone from this pad - socketio.sockets.in(padID).json.send({disconnect:"deleted"}); -} + socketio.sockets.in(padID).json.send({disconnect: 'deleted'}); +}; /** * Handles the disconnection of a user - * @param client the client that leaves + * @param socket the socket.io Socket object for the client */ -exports.handleDisconnect = async function(client) -{ +exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); // save the padname of this session - let session = sessioninfos[client.id]; + 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) { - // Get the IP address from our persistant object - let ip = remoteAddress[client.id]; - - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } - - accessLogger.info('[LEAVE] Pad "' + session.padId + '": Author "' + session.author + '" on client ' + client.id + ' with IP "' + ip + '" left the pad'); + 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 - let color = await authorManager.getAuthorColorId(session.author); + const color = await authorManager.getAuthorColorId(session.author); // prepare the notification for the other users on the pad, that this user left - let messageToTheOtherUsers = { - "type": "COLLABROOM", - "data": { - type: "USER_LEAVE", + const messageToTheOtherUsers = { + type: 'COLLABROOM', + data: { + type: 'USER_LEAVE', userInfo: { - "ip": "127.0.0.1", - "colorId": color, - "userAgent": "Anonymous", - "userId": session.author - } - } + colorId: color, + userId: session.author, + }, + }, }; // Go through all user that are still on the pad, and send them the USER_LEAVE message - client.broadcast.to(session.padId).json.send(messageToTheOtherUsers); + socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); // Allow plugins to hook into users leaving the pad - hooks.callAll("userLeave", session); + hooks.callAll('userLeave', session); } // Delete the sessioninfos entrys of this session - delete sessioninfos[client.id]; -} + delete sessioninfos[socket.id]; +}; /** * Handles a message from a user - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -exports.handleMessage = async function(client, message) -{ - var env = process.env.NODE_ENV || 'development'; +exports.handleMessage = async (socket, message) => { + const env = process.env.NODE_ENV || 'development'; if (env === 'production') { try { - await rateLimiter.consume(client.handshake.address); // consume 1 point per event from IP - }catch(e){ - console.warn("Rate limited: ", client.handshake.address, " to reduce the amount of rate limiting that happens edit the rateLimit values in settings.json"); + 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'); stats.meter('rateLimited').mark(); - client.json.send({disconnect:"rateLimited"}); + socket.json.send({disconnect: 'rateLimited'}); return; } } @@ -192,109 +178,115 @@ exports.handleMessage = async function(client, message) return; } - let thisSession = sessioninfos[client.id]; + const thisSession = sessioninfos[socket.id]; if (!thisSession) { - messageLogger.warn("Dropped message from an unknown connection.") + messageLogger.warn('Dropped message from an unknown connection.'); return; } - // Allow plugins to bypass the readonly message blocker - if ((await hooks.aCallAll('handleMessageSecurity', {client, message})).some((w) => w === true)) { - thisSession.readonly = false; - } - - // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for - // all messages handleMessage will be called, even if the client is not authorized - if ((await hooks.aCallAll('handleMessage', {client, message})).some((m) => m === null)) { - return; - } - - if (message.type === "CLIENT_READY") { + if (message.type === 'CLIENT_READY') { // client tried to auth for the first time (first msg from the client) - createSessionInfoAuth(client, message); + createSessionInfoAuth(thisSession, message); } - // the session may have been dropped during earlier processing - if (!sessioninfos[client.id]) { - messageLogger.warn("Dropping message from a connection that has gone away.") + const auth = thisSession.auth; + if (!auth) { + console.error('Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.'); return; } - // Simulate using the load testing tool - if (!sessioninfos[client.id].auth) { - console.error("Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.") - return; - } - - let auth = sessioninfos[client.id].auth; - // check if pad is requested via readOnly let padId = auth.padID; - if (padId.indexOf("r.") === 0) { + if (padId.indexOf('r.') === 0) { // Pad is readOnly, first get the real Pad ID padId = await readOnlyManager.getPadId(padId); } - const {session: {user} = {}} = client.client.request; - const {accessStatus} = - await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user); + const {session: {user} = {}} = socket.client.request; + const {accessStatus, authorID} = + await securityManager.checkAccess(padId, auth.sessionID, auth.token, user); + if (accessStatus !== 'grant') { + // Access denied. Send the reason to the user. + socket.json.send({accessStatus}); + return; + } + if (thisSession.author != null && thisSession.author !== authorID) { + messageLogger.warn( + `${'Rejecting message from client because the author ID changed mid-session.' + + ' Bad or missing token or sessionID?' + + ` socket:${socket.id}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + + ` originalAuthorID:${thisSession.author}` + + ` newAuthorID:${authorID}`}${ + (user && user.username) ? ` username:${user.username}` : '' + } message:${message}`); + socket.json.send({disconnect: 'rejected'}); + return; + } + thisSession.author = authorID; - if (accessStatus !== "grant") { - // no access, send the client a message that tells him why - client.json.send({ accessStatus }); + // Allow plugins to bypass the readonly message blocker + const context = {message, socket, client: socket}; // `client` for backwards compatibility. + if ((await hooks.aCallAll('handleMessageSecurity', context)).some((w) => w === true)) { + thisSession.readonly = false; + } + + // Call handleMessage hook. If a plugin returns null, the message will be dropped. + if ((await hooks.aCallAll('handleMessage', context)).some((m) => m === null)) { return; } - // access was granted + // Drop the message if the client disconnected during the above processing. + if (sessioninfos[socket.id] !== thisSession) { + messageLogger.warn('Dropping message from a connection that has gone away.'); + return; + } // Check what type of message we get and delegate to the other methods - if (message.type === "CLIENT_READY") { - handleClientReady(client, message); - } else if (message.type === "CHANGESET_REQ") { - handleChangesetRequest(client, message); - } else if(message.type === "COLLABROOM") { + if (message.type === 'CLIENT_READY') { + await handleClientReady(socket, message, authorID); + } else if (message.type === 'CHANGESET_REQ') { + await handleChangesetRequest(socket, message); + } else if (message.type === 'COLLABROOM') { if (thisSession.readonly) { - messageLogger.warn("Dropped message, COLLABROOM for readonly pad"); - } else if (message.data.type === "USER_CHANGES") { - stats.counter('pendingEdits').inc() - padChannels.emit(message.padId, {client: client, message: message}); // add to pad queue - } else if (message.data.type === "USERINFO_UPDATE") { - handleUserInfoUpdate(client, message); - } else if (message.data.type === "CHAT_MESSAGE") { - handleChatMessage(client, message); - } else if (message.data.type === "GET_CHAT_MESSAGES") { - handleGetChatMessages(client, message); - } else if (message.data.type === "SAVE_REVISION") { - handleSaveRevisionMessage(client, message); - } else if (message.data.type === "CLIENT_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 + } else if (message.data.type === 'USERINFO_UPDATE') { + await handleUserInfoUpdate(socket, message); + } else if (message.data.type === 'CHAT_MESSAGE') { + await handleChatMessage(socket, message); + } else if (message.data.type === 'GET_CHAT_MESSAGES') { + await handleGetChatMessages(socket, message); + } else if (message.data.type === 'SAVE_REVISION') { + await handleSaveRevisionMessage(socket, message); + } else if (message.data.type === 'CLIENT_MESSAGE' && message.data.payload != null && - message.data.payload.type === "suggestUserName") { - handleSuggestUserName(client, message); + message.data.payload.type === 'suggestUserName') { + handleSuggestUserName(socket, message); } else { - messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); + messageLogger.warn(`Dropped message, unknown COLLABROOM Data Type ${message.data.type}`); } - } else if(message.type === "SWITCH_TO_PAD") { - handleSwitchToPad(client, message); + } else if (message.type === 'SWITCH_TO_PAD') { + await handleSwitchToPad(socket, message, authorID); } else { - messageLogger.warn("Dropped message, unknown Message Type " + message.type); + messageLogger.warn(`Dropped message, unknown Message Type ${message.type}`); } -} +}; /** * Handles a save revision message - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleSaveRevisionMessage(client, message) -{ - var padId = sessioninfos[client.id].padId; - var userId = sessioninfos[client.id].author; - - let pad = await padManager.getPad(padId); - pad.addSavedRevision(pad.head, userId); +async function handleSaveRevisionMessage(socket, message) { + const {padId, author: authorId} = sessioninfos[socket.id]; + const pad = await padManager.getPad(padId); + await pad.addSavedRevision(pad.head, authorId); } /** @@ -304,9 +296,9 @@ async function handleSaveRevisionMessage(client, message) * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = function(msg, sessionID) { - if (msg.data.type === "CUSTOM") { - if (sessionID){ +exports.handleCustomObjectMessage = function (msg, sessionID) { + if (msg.data.type === 'CUSTOM') { + if (sessionID) { // a sessionID is targeted: directly to this sessionID socketio.sockets.socket(sessionID).json.send(msg); } else { @@ -314,7 +306,7 @@ exports.handleCustomObjectMessage = function(msg, sessionID) { socketio.sockets.in(msg.data.payload.padId).json.send(msg); } } -} +}; /** * Handles a custom message (sent via HTTP API request) @@ -322,31 +314,28 @@ exports.handleCustomObjectMessage = function(msg, sessionID) { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = function(padID, msgString) { - let time = Date.now(); - let msg = { +exports.handleCustomMessage = function (padID, msgString) { + const time = Date.now(); + const msg = { type: 'COLLABROOM', data: { type: msgString, - time: time - } + time, + }, }; socketio.sockets.in(padID).json.send(msg); -} +}; /** * Handles a Chat Message - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -function handleChatMessage(client, message) -{ - var time = Date.now(); - var userId = sessioninfos[client.id].author; - var text = message.data.text; - var padId = sessioninfos[client.id].padId; - - exports.sendChatMessageToPadClients(time, userId, text, padId); +async function handleChatMessage(socket, message) { + const time = Date.now(); + const text = message.data.text; + const {padId, author: authorId} = sessioninfos[socket.id]; + await exports.sendChatMessageToPadClients(time, authorId, text, padId); } /** @@ -356,157 +345,157 @@ function handleChatMessage(client, message) * @param text the text of the chat message * @param padId the padId to send the chat message to */ -exports.sendChatMessageToPadClients = async function(time, userId, text, padId) -{ +exports.sendChatMessageToPadClients = async function (time, userId, text, padId) { // get the pad - let pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId); // get the author - let userName = await authorManager.getAuthorName(userId); + const userName = await authorManager.getAuthorName(userId); // save the chat message - pad.appendChatMessage(text, userId, time); + const promise = pad.appendChatMessage(text, userId, time); - let msg = { - type: "COLLABROOM", - data: { type: "CHAT_MESSAGE", userId, userName, time, text } + const msg = { + 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); -} + + await promise; +}; /** * Handles the clients request for more chat-messages - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleGetChatMessages(client, message) -{ +async function handleGetChatMessages(socket, message) { if (message.data.start == null) { - messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); + messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); return; } if (message.data.end == null) { - messageLogger.warn("Dropped message, GetChatMessages Message has no start!"); + messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); return; } - let start = message.data.start; - let end = message.data.end; - let count = end - start; + const start = message.data.start; + const end = message.data.end; + const count = end - start; if (count < 0 || count > 100) { - messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amount of messages!"); + messageLogger.warn('Dropped message, GetChatMessages Message, client requested invalid amount of messages!'); return; } - let padId = sessioninfos[client.id].padId; - let pad = await padManager.getPad(padId); + const padId = sessioninfos[socket.id].padId; + const pad = await padManager.getPad(padId); - let chatMessages = await pad.getChatMessages(start, end); - let infoMsg = { - type: "COLLABROOM", + const chatMessages = await pad.getChatMessages(start, end); + const infoMsg = { + type: 'COLLABROOM', data: { - type: "CHAT_MESSAGES", - messages: chatMessages - } + type: 'CHAT_MESSAGES', + messages: chatMessages, + }, }; // send the messages back to the client - client.json.send(infoMsg); + socket.json.send(infoMsg); } /** * Handles a handleSuggestUserName, that means a user have suggest a userName for a other user - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -function handleSuggestUserName(client, message) -{ +function handleSuggestUserName(socket, message) { // check if all ok if (message.data.payload.newName == null) { - messageLogger.warn("Dropped message, suggestUserName Message has no newName!"); + messageLogger.warn('Dropped message, suggestUserName Message has no newName!'); return; } if (message.data.payload.unnamedId == null) { - messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!"); + messageLogger.warn('Dropped message, suggestUserName Message has no unnamedId!'); return; } - var padId = sessioninfos[client.id].padId; - var roomClients = _getRoomClients(padId); + const padId = sessioninfos[socket.id].padId; // search the author and send him this message - roomClients.forEach(function(client) { - var session = sessioninfos[client.id]; + _getRoomSockets(padId).forEach((socket) => { + const session = sessioninfos[socket.id]; if (session && session.author === message.data.payload.unnamedId) { - client.json.send(message); + socket.json.send(message); } }); } /** * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -function handleUserInfoUpdate(client, message) -{ +async function handleUserInfoUpdate(socket, message) { // check if all ok if (message.data.userInfo == null) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no userInfo!"); + messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no userInfo!'); return; } if (message.data.userInfo.colorId == null) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!"); + messageLogger.warn('Dropped message, USERINFO_UPDATE Message has no colorId!'); return; } // Check that we have a valid session and author to update. - var session = sessioninfos[client.id]; + const session = sessioninfos[socket.id]; if (!session || !session.author || !session.padId) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Session not ready." + message.data); + messageLogger.warn(`Dropped message, USERINFO_UPDATE Session not ready.${message.data}`); return; } // Find out the author name of this session - var author = session.author; + const author = session.author; // Check colorId is a Hex color - var isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId) // for #f00 (Thanks Smamatti) + const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); // for #f00 (Thanks Smamatti) if (!isColor) { - messageLogger.warn("Dropped message, USERINFO_UPDATE Color is malformed." + message.data); + messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`); return; } // Tell the authorManager about the new attributes - authorManager.setAuthorColorId(author, message.data.userInfo.colorId); - authorManager.setAuthorName(author, message.data.userInfo.name); + const p = Promise.all([ + authorManager.setAuthorColorId(author, message.data.userInfo.colorId), + authorManager.setAuthorName(author, message.data.userInfo.name), + ]); - var padId = session.padId; + const padId = session.padId; - var infoMsg = { - type: "COLLABROOM", + const infoMsg = { + type: 'COLLABROOM', data: { // The Client doesn't know about USERINFO_UPDATE, use USER_NEWINFO - type: "USER_NEWINFO", + type: 'USER_NEWINFO', userInfo: { userId: author, // set a null name, when there is no name set. cause the client wants it null name: message.data.userInfo.name || null, colorId: message.data.userInfo.colorId, - userAgent: "Anonymous", - ip: "127.0.0.1", - } - } + }, + }, }; // Send the other clients on the pad the update message - client.broadcast.to(padId).json.send(infoMsg); + socket.broadcast.to(padId).json.send(infoMsg); + + // Block until the authorManager has stored the new attributes. + await p; } /** @@ -520,55 +509,51 @@ function handleUserInfoUpdate(client, message) * This function is based on a similar one in the original Etherpad. * See https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges() * - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleUserChanges(data) -{ - var client = data.client - , message = data.message - +async function handleUserChanges(socket, message) { // This one's no longer pending, as we're gonna process it now - stats.counter('pendingEdits').dec() + 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!"); + 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!"); + 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!"); + 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]; + // TODO: this might happen with other messages too => find one place to copy the session // 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 (!sessioninfos[client.id]) { - messageLogger.warn("Dropped message, disconnect happened in the mean time"); + if (!thisSession) { + messageLogger.warn('Dropped message, disconnect happened in the mean time'); return; } // get all Vars we need - var baseRev = message.data.baseRev; - var wireApool = (new AttributePool()).fromJsonable(message.data.apool); - var changeset = message.data.changeset; - - // The client might disconnect between our callbacks. We should still - // finish processing the changeset, so keep a reference to the session. - var thisSession = sessioninfos[client.id]; + const baseRev = message.data.baseRev; + const wireApool = (new AttributePool()).fromJsonable(message.data.apool); + let changeset = message.data.changeset; // Measure time to process edit - var stopWatch = stats.timer('edits').start(); + const stopWatch = stats.timer('edits').start(); // get the pad - let pad = await padManager.getPad(thisSession.padId); + const pad = await padManager.getPad(thisSession.padId); // create the changeset try { @@ -578,24 +563,24 @@ async function handleUserChanges(data) // Verify that the attribute indexes used in the changeset are all // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, function(n) { + Changeset.eachAttribNumber(changeset, (n) => { if (!wireApool.getAttrib(n)) { - throw new Error("Attribute pool is missing attribute " + n + " for changeset " + changeset); + 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 - var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops) - , op; + const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); + let op; while (iterator.hasNext()) { - op = iterator.next() + op = iterator.next(); // + can add text with attribs // = can change or add attribs // - can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool - op.attribs.split('*').forEach(function(attr) { + op.attribs.split('*').forEach((attr) => { if (!attr) return; attr = wireApool.getAttrib(attr); @@ -603,7 +588,7 @@ async function handleUserChanges(data) // 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("Trying to submit changes as another author in changeset " + changeset); + throw new Error(`Author ${thisSession.author} tried to submit changes as author ${attr[1]} in changeset ${changeset}`); } }); } @@ -612,16 +597,15 @@ async function handleUserChanges(data) // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - - } catch(e) { + } catch (e) { // There is an error in this changeset, so just refuse it - client.json.send({ disconnect: "badChangeset" }); + socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - throw new Error("Can't apply USER_CHANGES, because " + e.message); + throw new Error(`Can't apply USER_CHANGES from Socket ${socket.id} because: ${e.message}`); } // ex. applyUserChanges - let apool = pad.pool; + const apool = pad.pool; let r = baseRev; // The client's changeset might not be based on the latest revision, @@ -630,7 +614,7 @@ async function handleUserChanges(data) while (r < pad.getHeadRevisionNumber()) { r++; - let c = await pad.getRevisionChangeset(r); + const c = await pad.getRevisionChangeset(r); // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function @@ -642,44 +626,44 @@ async function handleUserChanges(data) // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES // of that revision if (baseRev + 1 === r && c === changeset) { - client.json.send({disconnect:"badChangeset"}); + socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); throw new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset"); } changeset = Changeset.follow(c, changeset, false, apool); - } catch(e) { - client.json.send({disconnect:"badChangeset"}); + } catch (e) { + socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - throw new Error("Can't apply USER_CHANGES, because " + e.message); + throw new Error(`Can't apply USER_CHANGES, because ${e.message}`); } } - let prevText = pad.text(); + const prevText = pad.text(); if (Changeset.oldLen(changeset) !== prevText.length) { - client.json.send({disconnect:"badChangeset"}); + 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); + throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); } try { - pad.appendRevision(changeset, thisSession.author); - } catch(e) { - client.json.send({ disconnect: "badChangeset" }); + await pad.appendRevision(changeset, thisSession.author); + } catch (e) { + socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); throw e; } - let correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); + const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { - pad.appendRevision(correctionChangeset); + await pad.appendRevision(correctionChangeset); } // Make sure the pad always ends with an empty line. - if (pad.text().lastIndexOf("\n") !== pad.text().length-1) { - var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, "\n"); - pad.appendRevision(nlChangeset); + if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { + const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); + await pad.appendRevision(nlChangeset); } await exports.updatePadClients(pad); @@ -690,14 +674,10 @@ async function handleUserChanges(data) stopWatch.end(); } -exports.updatePadClients = async function(pad) -{ +exports.updatePadClients = async function (pad) { // skip this if no-one is on this pad - let roomClients = _getRoomClients(pad.id); - - if (roomClients.length === 0) { - return; - } + const roomSockets = _getRoomSockets(pad.id); + if (roomSockets.length === 0) return; // since all clients usually get the same set of changesets, store them in local cache // to remove unnecessary roundtrip to the datalayer @@ -706,24 +686,24 @@ exports.updatePadClients = async function(pad) // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing // via async.forEach with sequential for() loop. There is no real benefits of running this in parallel, // but benefit of reusing cached revision object is HUGE - let revCache = {}; + const revCache = {}; // go through all sessions on this pad - for (let client of roomClients) { - let sid = client.id; + for (const socket of roomSockets) { + const sid = socket.id; // send them all new changesets while (sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()) { - let r = sessioninfos[sid].rev + 1; + const r = sessioninfos[sid].rev + 1; let revision = revCache[r]; if (!revision) { revision = await pad.getRevision(r); revCache[r] = revision; } - let author = revision.meta.author, - revChangeset = revision.changeset, - currentTime = revision.meta.timestamp; + const author = revision.meta.author; + const revChangeset = revision.changeset; + const currentTime = revision.meta.timestamp; // next if session has not been deleted if (sessioninfos[sid] == null) { @@ -731,20 +711,19 @@ exports.updatePadClients = async function(pad) } if (author === sessioninfos[sid].author) { - client.json.send({ "type": "COLLABROOM", "data":{ type: "ACCEPT_COMMIT", newRev: r }}); + socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}); } else { - let forWire = Changeset.prepareForWire(revChangeset, pad.pool); - let wireMsg = {"type": "COLLABROOM", - "data": { type:"NEW_CHANGES", - newRev:r, - changeset: forWire.translated, - apool: forWire.pool, - author: author, - currentTime: currentTime, - timeDelta: currentTime - sessioninfos[sid].time - }}; - - client.json.send(wireMsg); + const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const wireMsg = {type: 'COLLABROOM', + data: {type: 'NEW_CHANGES', + newRev: r, + changeset: forWire.translated, + apool: forWire.pool, + author, + currentTime, + timeDelta: currentTime - sessioninfos[sid].time}}; + + socket.json.send(wireMsg); } if (sessioninfos[sid]) { @@ -753,29 +732,27 @@ exports.updatePadClients = async function(pad) } } } -} +}; /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... */ function _correctMarkersInPad(atext, apool) { - var text = atext.text; + const text = atext.text; // collect char positions of line markers (e.g. bullets) in new atext // that aren't at the start of a line - var badMarkers = []; - var iter = Changeset.opIterator(atext.attribs); - var offset = 0; + const badMarkers = []; + const iter = Changeset.opIterator(atext.attribs); + let offset = 0; while (iter.hasNext()) { var op = iter.next(); - var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute) { - return Changeset.opAttributeValue(op, attribute, apool); - }) !== undefined; + const hasMarker = _.find(AttributeManager.lineAttributes, (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; if (hasMarker) { - for (var i = 0; i < op.chars; i++) { - if (offset > 0 && text.charAt(offset-1) !== '\n') { + for (let i = 0; i < op.chars; i++) { + if (offset > 0 && text.charAt(offset - 1) !== '\n') { badMarkers.push(offset); } offset++; @@ -792,194 +769,177 @@ function _correctMarkersInPad(atext, apool) { // create changeset that removes these bad markers offset = 0; - var builder = Changeset.builder(text.length); + const builder = Changeset.builder(text.length); - badMarkers.forEach(function(pos) { + badMarkers.forEach((pos) => { builder.keepText(text.substring(offset, pos)); builder.remove(1); - offset = pos+1; + offset = pos + 1; }); return builder.toString(); } -function handleSwitchToPad(client, message) -{ - // clear the session and leave the room - let currentSession = sessioninfos[client.id]; - let padId = currentSession.padId; - let roomClients = _getRoomClients(padId); +async function handleSwitchToPad(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); - roomClients.forEach(client => { - let sinfo = sessioninfos[client.id]; - if (sinfo && sinfo.author === currentSession.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[client.id] = {}; - client.leave(padId); + sessioninfos[socket.id] = {}; + socket.leave(padId); } }); // start up the new pad - createSessionInfoAuth(client, message); - handleClientReady(client, message); + const newSessionInfo = sessioninfos[socket.id]; + createSessionInfoAuth(newSessionInfo, message); + await handleClientReady(socket, message, authorID); } -// Creates/replaces the auth object in the client's session info. Session info for the client must -// already exist. -function createSessionInfoAuth(client, message) -{ +// Creates/replaces the auth object in the given session info. +function 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. - sessioninfos[client.id].auth = - { + sessionInfo.auth = { sessionID: message.sessionID, padID: message.padId, - token : message.token, - password: message.password + 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 * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad - * @param client the client that send this message + * @param socket the socket.io Socket object for the client * @param message the message from the client */ -async function handleClientReady(client, message) -{ +async function handleClientReady(socket, message, authorID) { // check if all ok if (!message.token) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no token!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); return; } if (!message.padId) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no padId!'); return; } if (!message.protocolVersion) { - messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!"); + messageLogger.warn('Dropped message, CLIENT_READY Message has no protocolVersion!'); return; } if (message.protocolVersion !== 2) { - messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!"); + messageLogger.warn(`Dropped message, CLIENT_READY Message has a unknown protocolVersion '${message.protocolVersion}'!`); return; } - hooks.callAll("clientReady", message); + hooks.callAll('clientReady', message); // Get ro/rw id:s - let padIds = await readOnlyManager.getIds(message.padId); - - // FIXME: Allow to override readwrite access with readonly - const {session: {user} = {}} = client.client.request; - const {accessStatus, authorID} = await securityManager.checkAccess( - padIds.padId, message.sessionID, message.token, message.password, user); - - // no access, send the client a message that tells him why - if (accessStatus !== "grant") { - client.json.send({ accessStatus }); - return; - } + const padIds = await readOnlyManager.getIds(message.padId); // get all authordata of this new user assert(authorID); - let value = await authorManager.getAuthor(authorID); - let authorColorId = value.colorId; - let authorName = value.name; + const value = await authorManager.getAuthor(authorID); + const authorColorId = value.colorId; + const authorName = value.name; // load the pad-object from the database - let pad = await padManager.getPad(padIds.padId); + const pad = await padManager.getPad(padIds.padId); // these db requests all need the pad object (timestamp of latest revision, author data) - let authors = pad.getAllAuthors(); + const authors = pad.getAllAuthors(); // get timestamp of latest revision needed for timeslider - let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); + const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); // get all author data out of the database (in parallel) - let historicalAuthorData = {}; - await Promise.all(authors.map(authorId => { - return authorManager.getAuthor(authorId).then(author => { - if (!author) { - messageLogger.error("There is no author for authorId: ", authorId, ". This is possibly related to https://github.com/ether/etherpad-lite/issues/2802"); - } else { - historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) - } - }); - })); - - let thisUserHasEditedThisPad = false; - if (historicalAuthorData[authorID]) { - /* - * This flag is set to true when a user contributes to a specific pad for - * the first time. It is used for deciding if importing to that pad is - * allowed or not. - */ - thisUserHasEditedThisPad = true; - } + const historicalAuthorData = {}; + await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => { + if (!author) { + messageLogger.error('There is no author for authorId: ', authorId, '. This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); + } else { + historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) + } + }))); // 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. - if (sessioninfos[client.id] === undefined) { - return; - } + const sessionInfo = sessioninfos[socket.id]; + if (sessionInfo == null) return; // Check if this author is already on the pad, if yes, kick the other sessions! - let roomClients = _getRoomClients(pad.id); + const roomSockets = _getRoomSockets(pad.id); - for (let client of roomClients) { - let sinfo = sessioninfos[client.id]; + for (const socket of roomSockets) { + const sinfo = sessioninfos[socket.id]; if (sinfo && sinfo.author === authorID) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[client.id] = {}; - client.leave(padIds.padId); - client.json.send({disconnect:"userdup"}); + sessioninfos[socket.id] = {}; + socket.leave(padIds.padId); + socket.json.send({disconnect: 'userdup'}); } } // Save in sessioninfos that this session belonges to this pad - sessioninfos[client.id].padId = padIds.padId; - sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; - sessioninfos[client.id].readonly = padIds.readonly; - - // Log creation/(re-)entering of a pad - let ip = remoteAddress[client.id]; - - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } - - if (pad.head > 0) { - accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); - } else if (pad.head === 0) { - accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the 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}` + + ` socket:${socket.id}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + + ` authorID:${authorID}`}${ + (user && user.username) ? ` username:${user.username}` : ''}`); 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 - client.join(padIds.padId); + socket.join(padIds.padId); // Save the revision in sessioninfos, we take the revision from the info the client send to us - sessioninfos[client.id].rev = message.client_rev; + sessionInfo.rev = message.client_rev; // During the client reconnect, client might miss some revisions from other clients. By using client revision, // this below code sends all the revisions missed during the client reconnect - var revisionsNeeded = []; - var changesets = {}; + const revisionsNeeded = []; + const changesets = {}; - var startNum = message.client_rev + 1; - var endNum = pad.getHeadRevisionNumber() + 1; + let startNum = message.client_rev + 1; + let endNum = pad.getHeadRevisionNumber() + 1; - var headNum = pad.getHeadRevisionNumber(); + const headNum = pad.getHeadRevisionNumber(); if (endNum > headNum + 1) { endNum = headNum + 1; @@ -995,52 +955,48 @@ async function handleClientReady(client, message) } // get changesets, author and timestamp needed for pending revisions (in parallel) - let promises = []; - for (let revNum of revisionsNeeded) { - let 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 )); + const promises = []; + for (const revNum of revisionsNeeded) { + 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); // return pending changesets - for (let r of revisionsNeeded) { - - let forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); - let wireMsg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - headRev:pad.getHeadRevisionNumber(), - newRev:r, - changeset:forWire.translated, - apool: forWire.pool, - author: changesets[r]['author'], - currentTime: changesets[r]['timestamp'] - }}; - client.json.send(wireMsg); + for (const r of revisionsNeeded) { + const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool); + const wireMsg = {type: 'COLLABROOM', + data: {type: 'CLIENT_RECONNECT', + headRev: pad.getHeadRevisionNumber(), + newRev: r, + changeset: forWire.translated, + apool: forWire.pool, + author: changesets[r].author, + currentTime: changesets[r].timestamp}}; + socket.json.send(wireMsg); } if (startNum === endNum) { - var Msg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - noChanges: true, - newRev: pad.getHeadRevisionNumber() - }}; - client.json.send(Msg); + const Msg = {type: 'COLLABROOM', + data: {type: 'CLIENT_RECONNECT', + noChanges: true, + newRev: pad.getHeadRevisionNumber()}}; + socket.json.send(Msg); } - } else { // This is a normal first connect // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted try { var atext = Changeset.cloneAText(pad.atext); - var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; - } catch(e) { - console.error(e.stack || e) - client.json.send({ disconnect:"corruptPad" }); // pull the brakes + } catch (e) { + console.error(e.stack || e); + socket.json.send({disconnect: 'corruptPad'}); // pull the brakes return; } @@ -1048,66 +1004,61 @@ async function handleClientReady(client, message) // 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... - var clientVars = { - "skinName": settings.skinName, - "skinVariants": settings.skinVariants, - "randomVersionString": settings.randomVersionString, - "accountPrivs": { - "maxRevisions": 100 + const clientVars = { + skinName: settings.skinName, + skinVariants: settings.skinVariants, + randomVersionString: settings.randomVersionString, + accountPrivs: { + maxRevisions: 100, }, - "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, - "initialRevisionList": [], - "initialOptions": { - "guestPolicy": "deny" + automaticReconnectionTimeout: settings.automaticReconnectionTimeout, + initialRevisionList: [], + initialOptions: {}, + savedRevisions: pad.getSavedRevisions(), + collab_client_vars: { + initialAttributedText: atext, + clientIp: '127.0.0.1', + padId: message.padId, + historicalAuthorData, + apool, + rev: pad.getHeadRevisionNumber(), + time: currentTime, }, - "savedRevisions": pad.getSavedRevisions(), - "collab_client_vars": { - "initialAttributedText": atext, - "clientIp": "127.0.0.1", - "padId": message.padId, - "historicalAuthorData": historicalAuthorData, - "apool": apool, - "rev": pad.getHeadRevisionNumber(), - "time": currentTime, - }, - "colorPalette": authorManager.getColorPalette(), - "clientIp": "127.0.0.1", - "userIsGuest": true, - "userColor": authorColorId, - "padId": message.padId, - "padOptions": settings.padOptions, - "padShortcutEnabled": settings.padShortcutEnabled, - "initialTitle": "Pad: " + message.padId, - "opts": {}, + colorPalette: authorManager.getColorPalette(), + clientIp: '127.0.0.1', + userColor: authorColorId, + padId: message.padId, + padOptions: settings.padOptions, + padShortcutEnabled: settings.padShortcutEnabled, + initialTitle: `Pad: ${message.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": roomClients.length, - "readOnlyId": padIds.readOnlyPadId, - "readonly": padIds.readonly, - "serverTimestamp": Date.now(), - "userId": authorID, - "abiwordAvailable": settings.abiwordAvailable(), - "sofficeAvailable": settings.sofficeAvailable(), - "exportAvailable": settings.exportAvailable(), - "plugins": { - "plugins": plugins.plugins, - "parts": plugins.parts, + chatHead: pad.chatHead, + numConnectedUsers: roomSockets.length, + readOnlyId: padIds.readOnlyPadId, + readonly: sessionInfo.readonly, + serverTimestamp: Date.now(), + userId: authorID, + abiwordAvailable: settings.abiwordAvailable(), + sofficeAvailable: settings.sofficeAvailable(), + exportAvailable: settings.exportAvailable(), + plugins: { + plugins: plugins.plugins, + parts: plugins.parts, }, - "indentationOnNewLine": settings.indentationOnNewLine, - "scrollWhenFocusLineIsOutOfViewport": { - "percentage" : { - "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, - "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, + indentationOnNewLine: settings.indentationOnNewLine, + scrollWhenFocusLineIsOutOfViewport: { + percentage: { + editionAboveViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, + editionBelowViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, }, - "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, - "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, - "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, + duration: settings.scrollWhenFocusLineIsOutOfViewport.duration, + scrollWhenCaretIsInTheLastLineOfViewport: settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, + percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, - "initialChangesets": [], // FIXME: REMOVE THIS SHIT - "thisUserHasEditedThisPad": thisUserHasEditedThisPad, - "allowAnyoneToImport": settings.allowAnyoneToImport - } + initialChangesets: [], // FIXME: REMOVE THIS SHIT + }; // Add a username to the clientVars if one avaiable if (authorName != null) { @@ -1115,36 +1066,32 @@ async function handleClientReady(client, message) } // call the clientVars-hook so plugins can modify them before they get sent to the client - let messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket: client}); + const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket}); // combine our old object with the new attributes from the hook - for (let msg of messages) { + for (const msg of messages) { Object.assign(clientVars, msg); } // Join the pad and start receiving updates - client.join(padIds.padId); + socket.join(padIds.padId); // Send the clientVars to the Client - client.json.send({type: "CLIENT_VARS", data: clientVars}); + socket.json.send({type: 'CLIENT_VARS', data: clientVars}); // Save the current revision in sessioninfos, should be the same as in clientVars - sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); - - sessioninfos[client.id].author = authorID; + sessionInfo.rev = pad.getHeadRevisionNumber(); // prepare the notification for the other users on the pad, that this user joined - let messageToTheOtherUsers = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", + const messageToTheOtherUsers = { + type: 'COLLABROOM', + data: { + type: 'USER_NEWINFO', userInfo: { - "ip": "127.0.0.1", - "colorId": authorColorId, - "userAgent": "Anonymous", - "userId": authorID, - } - } + colorId: authorColorId, + userId: authorID, + }, + }, }; // Add the authorname of this new User, if avaiable @@ -1153,68 +1100,51 @@ async function handleClientReady(client, message) } // notify all existing users about new user - client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); + socket.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); // Get sessions for this pad and update them (in parallel) - roomClients = _getRoomClients(pad.id); - await Promise.all(_getRoomClients(pad.id).map(async roomClient => { - + await Promise.all(_getRoomSockets(pad.id).map(async (roomSocket) => { // Jump over, if this session is the connection session - if (roomClient.id === client.id) { + if (roomSocket.id === socket.id) { return; } // Since sessioninfos might change while being enumerated, check if the // sessionID is still assigned to a valid session - if (sessioninfos[roomClient.id] === undefined) { - return; - } + const sessionInfo = sessioninfos[roomSocket.id]; + if (sessionInfo == null) return; // get the authorname & colorId - let author = sessioninfos[roomClient.id].author; - let cached = historicalAuthorData[author]; + const authorId = sessionInfo.author; + // The authorId of this other user might be unknown if the other user just connected and has + // not yet sent a CLIENT_READY message. + if (authorId == null) return; // reuse previously created cache of author's data - let authorInfo = cached ? cached : (await authorManager.getAuthor(author)); - - // default fallback color to use if authorInfo.colorId is null - const defaultColor = "#daf0b2"; - - if (!authorInfo) { - console.warn(`handleClientReady(): no authorInfo parameter was received. Default values are going to be used. See issue #3612. This can be caused by a user clicking undo after clearing all authorship colors see #2802`); - authorInfo = {}; - } - - // For some reason sometimes name isn't set - // Catch this issue here and use a fixed name. - if (!authorInfo.name) { - console.warn(`handleClientReady(): client submitted no author name. Using "Anonymous". See: issue #3612`); - authorInfo.name = "Anonymous"; - } - - // For some reason sometimes colorId isn't set - // Catch this issue here and use a fixed color. - if (!authorInfo.colorId) { - console.warn(`handleClientReady(): author "${authorInfo.name}" has no property colorId. Using the default color ${defaultColor}. See issue #3612`); - authorInfo.colorId = defaultColor; + const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId); + if (authorInfo == null) { + messageLogger.error( + `Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` + + 'the global author database. This should never happen because the author ID is ' + + 'generated by the same code that adds the author to the database.'); + // Don't bother telling the new user about this mystery author. + return; } // Send the new User a Notification about this other user - let msg = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", + const msg = { + type: 'COLLABROOM', + data: { + type: 'USER_NEWINFO', userInfo: { - "ip": "127.0.0.1", - "colorId": authorInfo.colorId, - "name": authorInfo.name, - "userAgent": "Anonymous", - "userId": author - } - } + colorId: authorInfo.colorId, + name: authorInfo.name, + userId: authorId, + }, + }, }; - client.json.send(msg); + socket.json.send(msg); })); } } @@ -1222,53 +1152,52 @@ async function handleClientReady(client, message) /** * Handles a request for a rough changeset, the timeslider client needs it */ -async function handleChangesetRequest(client, message) -{ +async function handleChangesetRequest(socket, message) { // check if all ok if (message.data == null) { - messageLogger.warn("Dropped message, changeset request has no data!"); + messageLogger.warn('Dropped message, changeset request has no data!'); return; } if (message.padId == null) { - messageLogger.warn("Dropped message, changeset request has no padId!"); + messageLogger.warn('Dropped message, changeset request has no padId!'); return; } if (message.data.granularity == null) { - messageLogger.warn("Dropped message, changeset request has no granularity!"); + messageLogger.warn('Dropped message, changeset request has no granularity!'); return; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill if (Math.floor(message.data.granularity) !== message.data.granularity) { - messageLogger.warn("Dropped message, changeset request granularity is not an integer!"); + messageLogger.warn('Dropped message, changeset request granularity is not an integer!'); return; } if (message.data.start == null) { - messageLogger.warn("Dropped message, changeset request has no start!"); + messageLogger.warn('Dropped message, changeset request has no start!'); return; } if (message.data.requestID == null) { - messageLogger.warn("Dropped message, changeset request has no requestID!"); + messageLogger.warn('Dropped message, changeset request has no requestID!'); return; } - let granularity = message.data.granularity; - let start = message.data.start; - let end = start + (100 * granularity); + const granularity = message.data.granularity; + const start = message.data.start; + const end = start + (100 * granularity); - let padIds = await readOnlyManager.getIds(message.padId); + const padIds = await readOnlyManager.getIds(message.padId); // build the requested rough changesets and send them back try { - let data = await getChangesetInfo(padIds.padId, start, end, granularity); + const data = await getChangesetInfo(padIds.padId, start, end, granularity); data.requestID = message.data.requestID; - client.json.send({ type: "CHANGESET_REQ", data }); + socket.json.send({type: 'CHANGESET_REQ', data}); } catch (err) { - console.error('Error while handling a changeset request for ' + padIds.padId, err.toString(), message.data); + console.error(`Error while handling a changeset request for ${padIds.padId}`, err.toString(), message.data); } } @@ -1276,10 +1205,9 @@ async function handleChangesetRequest(client, message) * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -async function getChangesetInfo(padId, startNum, endNum, granularity) -{ - let pad = await padManager.getPad(padId); - let head_revision = pad.getHeadRevisionNumber(); +async function getChangesetInfo(padId, startNum, endNum, granularity) { + const pad = await padManager.getPad(padId); + const head_revision = pad.getHeadRevisionNumber(); // calculate the last full endnum if (endNum > head_revision + 1) { @@ -1287,15 +1215,15 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) } endNum = Math.floor(endNum / granularity) * granularity; - let compositesChangesetNeeded = []; - let revTimesNeeded = []; + const compositesChangesetNeeded = []; + const revTimesNeeded = []; // figure out which composite Changeset and revTimes we need, to load them in bulk for (let start = startNum; start < endNum; start += granularity) { - let end = start + granularity; + const end = start + granularity; // add the composite Changeset we needed - compositesChangesetNeeded.push({ start, end }); + compositesChangesetNeeded.push({start, end}); // add the t1 time we need revTimesNeeded.push(start === 0 ? 0 : start - 1); @@ -1308,24 +1236,20 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) // it would make all the lookups run in series // get all needed composite Changesets - let composedChangesets = {}; - let p1 = Promise.all(compositesChangesetNeeded.map(item => { - return composePadChangesets(padId, item.start, item.end).then(changeset => { - composedChangesets[item.start + "/" + item.end] = changeset; - }); - })); + 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 - let revisionDate = []; - let p2 = Promise.all(revTimesNeeded.map(revNum => { - return pad.getRevisionDate(revNum).then(revDate => { - revisionDate[revNum] = Math.floor(revDate / 1000); - }); - })); + 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; - let p3 = getPadLines(padId, startNum - 1).then(_lines => { + const p3 = getPadLines(padId, startNum - 1).then((_lines) => { lines = _lines; }); @@ -1333,46 +1257,45 @@ async function getChangesetInfo(padId, startNum, endNum, granularity) await Promise.all([p1, p2, p3]); // doesn't know what happens here exactly :/ - let timeDeltas = []; - let forwardsChangesets = []; - let backwardsChangesets = []; - let apool = new AttributePool(); + const timeDeltas = []; + const forwardsChangesets = []; + const backwardsChangesets = []; + const apool = new AttributePool(); for (let compositeStart = startNum; compositeStart < endNum; compositeStart += granularity) { - let compositeEnd = compositeStart + granularity; + const compositeEnd = compositeStart + granularity; if (compositeEnd > endNum || compositeEnd > head_revision + 1) { break; } - let forwards = composedChangesets[compositeStart + "/" + compositeEnd]; - let backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; + const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); Changeset.mutateTextLines(forwards, lines.textlines); - let forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - let backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); - let t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; - let t2 = revisionDate[compositeEnd - 1]; + const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; + const t2 = revisionDate[compositeEnd - 1]; timeDeltas.push(t2 - t1); forwardsChangesets.push(forwards2); backwardsChangesets.push(backwards2); } - return { forwardsChangesets, backwardsChangesets, - apool: apool.toJsonable(), actualEndNum: endNum, - timeDeltas, start: startNum, granularity }; + return {forwardsChangesets, backwardsChangesets, + apool: apool.toJsonable(), actualEndNum: endNum, + timeDeltas, start: startNum, granularity}; } /** * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ -async function getPadLines(padId, revNum) -{ - let pad = await padManager.getPad(padId); +async function getPadLines(padId, revNum) { + const pad = await padManager.getPad(padId); // get the atext let atext; @@ -1380,12 +1303,12 @@ async function getPadLines(padId, revNum) if (revNum >= 0) { atext = await pad.getInternalRevisionAText(revNum); } else { - atext = Changeset.makeAText("\n"); + atext = Changeset.makeAText('\n'); } return { textlines: Changeset.splitTextLines(atext.text), - alines: Changeset.splitAttributionLines(atext.attribs, atext.text) + alines: Changeset.splitAttributionLines(atext.attribs, atext.text), }; } @@ -1393,85 +1316,79 @@ async function getPadLines(padId, revNum) * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ -async function composePadChangesets (padId, startNum, endNum) -{ - let pad = await padManager.getPad(padId); +async function composePadChangesets(padId, startNum, endNum) { + const pad = await padManager.getPad(padId); // fetch all changesets we need - let headNum = pad.getHeadRevisionNumber(); + const headNum = pad.getHeadRevisionNumber(); endNum = Math.min(endNum, headNum + 1); startNum = Math.max(startNum, 0); // create an array for all changesets, we will // replace the values with the changeset later - let changesetsNeeded = []; - for (let r = startNum ; r < endNum; r++) { + const changesetsNeeded = []; + for (let r = startNum; r < endNum; r++) { changesetsNeeded.push(r); } // get all changesets - let changesets = {}; - await Promise.all(changesetsNeeded.map(revNum => { - return pad.getRevisionChangeset(revNum).then(changeset => changesets[revNum] = changeset); - })); + const changesets = {}; + await Promise.all(changesetsNeeded.map((revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset))); // compose Changesets let r; try { let changeset = changesets[startNum]; - let pool = pad.apool(); + const pool = pad.apool(); for (r = startNum + 1; r < endNum; r++) { - let cs = changesets[r]; + const cs = changesets[r]; changeset = Changeset.compose(changeset, cs, pool); } 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); + console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); throw e; } } -function _getRoomClients(padID) { - var roomClients = []; - var room = socketio.sockets.adapter.rooms[padID]; +function _getRoomSockets(padID) { + const roomSockets = []; + const room = socketio.sockets.adapter.rooms[padID]; if (room) { - for (var id in room.sockets) { - roomClients.push(socketio.sockets.sockets[id]); + for (const id in room.sockets) { + roomSockets.push(socketio.sockets.sockets[id]); } } - return roomClients; + return roomSockets; } /** * Get the number of users in a pad */ -exports.padUsersCount = function(padID) { +exports.padUsersCount = function (padID) { return { - padUsersCount: _getRoomClients(padID).length - } -} + padUsersCount: _getRoomSockets(padID).length, + }; +}; /** * Get the list of users in a pad */ -exports.padUsers = async function(padID) { - - let padUsers = []; - let roomClients = _getRoomClients(padID); +exports.padUsers = async function (padID) { + const padUsers = []; // iterate over all clients (in parallel) - await Promise.all(roomClients.map(async roomClient => { - let s = sessioninfos[roomClient.id]; + await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { + const s = sessioninfos[roomSocket.id]; if (s) { - return authorManager.getAuthor(s.author).then(author => { + return authorManager.getAuthor(s.author).then((author) => { // Fixes: https://github.com/ether/etherpad-lite/issues/4120 // On restart author might not be populated? - if(author){ + if (author) { author.id = s.author; padUsers.push(author); } @@ -1479,7 +1396,7 @@ exports.padUsers = async function(padID) { } })); - return { padUsers }; -} + return {padUsers}; +}; exports.sessioninfos = sessioninfos; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index a5220d2f4bc..56e5c5be426 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -19,136 +19,70 @@ * limitations under the License. */ -var log4js = require('log4js'); -var messageLogger = log4js.getLogger("message"); -var securityManager = require("../db/SecurityManager"); -var readOnlyManager = require("../db/ReadOnlyManager"); -var remoteAddress = require("../utils/RemoteAddress").remoteAddress; -var settings = require('../utils/Settings'); +const log4js = require('log4js'); +const messageLogger = log4js.getLogger('message'); +const securityManager = require('../db/SecurityManager'); +const readOnlyManager = require('../db/ReadOnlyManager'); +const settings = require('../utils/Settings'); /** * Saves all components * key is the component name * value is the component module */ -var components = {}; +const components = {}; -var socket; +let socket; /** * adds a component */ -exports.addComponent = function(moduleName, module) -{ +exports.addComponent = function (moduleName, module) { // save the component components[moduleName] = module; // give the module the socket module.setSocketIO(socket); -} +}; /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = function(_socket) { +exports.setSocketIO = function (_socket) { // save this socket internaly socket = _socket; - socket.sockets.on('connection', function(client) - { - // Broken: See http://stackoverflow.com/questions/4647348/send-message-to-specific-client-with-socket-io-and-node-js - // Fixed by having a persistant object, ideally this would actually be in the database layer - // TODO move to database layer - if (settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined) { - remoteAddress[client.id] = client.handshake.headers['x-forwarded-for']; - } else { - remoteAddress[client.id] = client.handshake.address; - } - - var clientAuthorized = false; - + socket.sockets.on('connection', (client) => { // wrap the original send function to log the messages client._send = client.send; - client.send = function(message) { - messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message)); + client.send = function (message) { + messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); client._send(message); - } + }; // tell all components about this connect - for (let i in components) { + for (const i in components) { components[i].handleConnect(client); } - client.on('message', async function(message) { + client.on('message', async (message) => { if (message.protocolVersion && message.protocolVersion != 2) { - messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); + messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; } - - if (clientAuthorized) { - // client is authorized, everything ok - handleMessage(client, message); - } else { - // try to authorize the client - if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - // check for read-only pads - let padId = message.padId; - if (padId.indexOf("r.") === 0) { - padId = await readOnlyManager.getPadId(message.padId); - } - - const {session: {user} = {}} = client.client.request; - const {accessStatus} = await securityManager.checkAccess( - padId, message.sessionID, message.token, message.password, user); - - if (accessStatus === "grant") { - // access was granted, mark the client as authorized and handle the message - clientAuthorized = true; - handleMessage(client, message); - } else { - // no access, send the client a message that tells him why - messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); - client.json.send({ accessStatus }); - } - } else { - // drop message - messageLogger.warn("Dropped message because of bad permissions:" + stringifyWithoutPassword(message)); - } + if (!message.component || !components[message.component]) { + messageLogger.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); }); - client.on('disconnect', function() { + client.on('disconnect', () => { // tell all components about this disconnect - for (let i in components) { + for (const i in components) { components[i].handleDisconnect(client); } }); }); -} - -// try to handle the message of this client -function handleMessage(client, message) -{ - if (message.component && components[message.component]) { - // check if component is registered in the components array - if (components[message.component]) { - messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message)); - components[message.component].handleMessage(client, message); - } - } else { - messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message)); - } -} - -// returns a stringified representation of a message, removes the password -// this ensures there are no passwords in the log -function stringifyWithoutPassword(message) -{ - let newMessage = Object.assign({}, message); - - if (newMessage.password != null) { - newMessage.password = "xxx"; - } - - return JSON.stringify(newMessage); -} +}; diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 7ff7d4ffc62..b3d4f34e469 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,24 +1,43 @@ -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var express = require('express'); -var settings = require('../utils/Settings'); -var fs = require('fs'); -var path = require('path'); -var npm = require("npm/lib/npm.js"); -var _ = require("underscore"); - -var server; -var serverName; - -exports.createServer = function () { - console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") +'use strict'; + +const _ = require('underscore'); +const cookieParser = require('cookie-parser'); +const express = require('express'); +const expressSession = require('express-session'); +const fs = require('fs'); +const hooks = require('../../static/js/pluginfw/hooks'); +const log4js = require('log4js'); +const SessionStore = require('../db/SessionStore'); +const settings = require('../utils/Settings'); +const stats = require('../stats'); +const util = require('util'); + +const logger = log4js.getLogger('http'); +let serverName; + +exports.server = null; + +const closeServer = async () => { + if (exports.server == null) return; + logger.info('Closing HTTP server...'); + await Promise.all([ + util.promisify(exports.server.close.bind(exports.server))(), + hooks.aCallAll('expressCloseServer'), + ]); + exports.server = null; + logger.info('HTTP server closed'); +}; + +exports.createServer = async () => { + console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); - exports.restartServer(); + await exports.restartServer(); - if (settings.ip === "") { + if (settings.ip === '') { // using Unix socket for connectivity console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); } else { @@ -28,59 +47,59 @@ exports.createServer = function () { if (!_.isEmpty(settings.users)) { console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); } else { - console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); + console.warn('Admin username and password not set in settings.json. ' + + 'To access admin please uncomment and edit "users" in settings.json'); } - var env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV || 'development'; if (env !== 'production') { - console.warn("Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production"); + console.warn('Etherpad is running in Development mode. This mode is slower for users and ' + + 'less secure than production mode. You should set the NODE_ENV environment ' + + 'variable to production by using: export NODE_ENV=production'); } -} +}; -exports.restartServer = function () { - if (server) { - console.log("Restarting express server"); - server.close(); - } +exports.restartServer = async () => { + await closeServer(); - var app = express(); // New syntax for express v3 + const app = express(); // New syntax for express v3 if (settings.ssl) { - console.log("SSL -- enabled"); + console.log('SSL -- enabled'); console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); - var options = { - key: fs.readFileSync( settings.ssl.key ), - cert: fs.readFileSync( settings.ssl.cert ) + const options = { + key: fs.readFileSync(settings.ssl.key), + cert: fs.readFileSync(settings.ssl.cert), }; if (settings.ssl.ca) { options.ca = []; - for (var i = 0; i < settings.ssl.ca.length; i++) { - var caFileName = settings.ssl.ca[i]; + for (let i = 0; i < settings.ssl.ca.length; i++) { + const caFileName = settings.ssl.ca[i]; options.ca.push(fs.readFileSync(caFileName)); } } - var https = require('https'); - server = https.createServer(options, app); + const https = require('https'); + exports.server = https.createServer(options, app); } else { - var http = require('http'); - server = http.createServer(app); + const http = require('http'); + exports.server = http.createServer(app); } - app.use(function(req, res, next) { + app.use((req, res, next) => { // res.header("X-Frame-Options", "deny"); // breaks embedded pads if (settings.ssl) { // we use SSL - res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // Stop IE going into compatability mode // https://github.com/ether/etherpad-lite/issues/2547 - res.header("X-UA-Compatible", "IE=Edge,chrome=1"); + res.header('X-UA-Compatible', 'IE=Edge,chrome=1'); // Enable a strong referrer policy. Same-origin won't drop Referers when // loading local resources, but it will drop them when loading foreign resources. @@ -89,11 +108,11 @@ exports.restartServer = function () { // marked with // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy // https://github.com/ether/etherpad-lite/pull/3636 - res.header("Referrer-Policy", "same-origin"); + res.header('Referrer-Policy', 'same-origin'); // send git version in the Server response header if exposeVersion is true. if (settings.exposeVersion) { - res.header("Server", serverName); + res.header('Server', serverName); } next(); @@ -109,8 +128,66 @@ exports.restartServer = function () { app.enable('trust proxy'); } - hooks.callAll("expressConfigure", {"app": app}); - hooks.callAll("expressCreateServer", {"app": app, "server": server}); + // Measure response time + app.use((req, res, next) => { + const stopWatch = stats.timer('httpRequests').start(); + const sendFn = res.send.bind(res); + res.send = (...args) => { stopWatch.end(); sendFn(...args); }; + next(); + }); + + // If the log level specified in the config file is WARN or ERROR the application server never + // starts listening to requests as reported in issue #158. Not installing the log4js connect + // logger when the log level has a higher severity than INFO since it would not log at that level + // anyway. + if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + app.use(log4js.connectLogger(logger, { + level: log4js.levels.DEBUG, + format: ':status, :method :url', + })); + } + + exports.sessionMiddleware = expressSession({ + secret: settings.sessionKey, + store: new SessionStore(), + resave: false, + saveUninitialized: true, + // Set the cookie name to a javascript identifier compatible string. Makes code handling it + // cleaner :) + name: 'express_sid', + proxy: true, + cookie: { + sameSite: settings.cookie.sameSite, + + // The automatic express-session mechanism for determining if the application is being served + // over ssl is similar to the one used for setting the language cookie, which check if one of + // these conditions is true: + // + // 1. we are directly serving the nodejs application over SSL, using the "ssl" options in + // settings.json + // + // 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy + // that terminates SSL for us. In this case, the user has to set trustProxy = true in + // settings.json, and the information wheter the application is over SSL or not will be + // extracted from the X-Forwarded-Proto HTTP header + // + // Please note that this will not be compatible with applications being served over http and + // https at the same time. + // + // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure + secure: 'auto', + }, + }); + app.use(exports.sessionMiddleware); + + app.use(cookieParser(settings.sessionKey, {})); + + hooks.callAll('expressConfigure', {app}); + hooks.callAll('expressCreateServer', {app, server: exports.server}); + + await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); +}; - server.listen(settings.port, settings.ip); -} +exports.shutdown = async (hookName, context) => { + await closeServer(); +}; diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.js index 0884cde56a4..417939600e2 100644 --- a/src/node/hooks/express/admin.js +++ b/src/node/hooks/express/admin.js @@ -1,9 +1,9 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); +const eejs = require('ep_etherpad-lite/node/eejs'); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/admin', function(req, res) { - if('/' != req.path[req.path.length-1]) return res.redirect('./admin/'); - res.send( eejs.require("ep_etherpad-lite/templates/admin/index.html", {}) ); + args.app.get('/admin', (req, res) => { + if ('/' != req.path[req.path.length - 1]) return res.redirect('./admin/'); + res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); }); -} - + return cb(); +}; diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index f6f184ed396..0a6d9780897 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -1,129 +1,131 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); -var settings = require('ep_etherpad-lite/node/utils/Settings'); -var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); -var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); -var _ = require('underscore'); -var semver = require('semver'); -const UpdateCheck = require('ep_etherpad-lite/node/utils/UpdateCheck'); - -exports.expressCreateServer = function(hook_name, args, cb) { - args.app.get('/admin/plugins', function(req, res) { - var render_args = { +'use strict'; + +const eejs = require('../../eejs'); +const settings = require('../../utils/Settings'); +const installer = require('../../../static/js/pluginfw/installer'); +const plugins = require('../../../static/js/pluginfw/plugin_defs'); +const _ = require('underscore'); +const semver = require('semver'); +const UpdateCheck = require('../../utils/UpdateCheck'); + +exports.expressCreateServer = (hookName, args, cb) => { + args.app.get('/admin/plugins', (req, res) => { + res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { plugins: plugins.plugins, - search_results: {}, + req, errors: [], - }; - - res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args)); + })); }); - args.app.get('/admin/plugins/info', function(req, res) { - var gitCommit = settings.getGitCommit(); - var epVersion = settings.getEpVersion(); + args.app.get('/admin/plugins/info', (req, res) => { + const gitCommit = settings.getGitCommit(); + const epVersion = settings.getEpVersion(); - res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", { - gitCommit: gitCommit, - epVersion: epVersion, - latestVersion: UpdateCheck.getLatestVersion() + res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', { + gitCommit, + epVersion, + latestVersion: UpdateCheck.getLatestVersion(), + req, })); }); -} -exports.socketio = function(hook_name, args, cb) { - var io = args.io.of("/pluginfw/installer"); - io.on('connection', function(socket) { - if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; + return cb(); +}; + +exports.socketio = (hookName, args, cb) => { + const io = args.io.of('/pluginfw/installer'); + io.on('connection', (socket) => { + const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; + if (!isAdmin) return; - socket.on("getInstalled", function(query) { + socket.on('getInstalled', (query) => { // send currently installed plugins - var installed = Object.keys(plugins.plugins).map(function(plugin) { - return plugins.plugins[plugin].package - }); + const installed = + Object.keys(plugins.plugins).map((plugin) => plugins.plugins[plugin].package); - socket.emit("results:installed", {installed: installed}); + socket.emit('results:installed', {installed}); }); - socket.on("checkUpdates", async function() { + socket.on('checkUpdates', async () => { // Check plugins for updates try { - let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10); + const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); - var updatable = _(plugins.plugins).keys().filter(function(plugin) { + const updatable = _(plugins.plugins).keys().filter((plugin) => { if (!results[plugin]) return false; - var latestVersion = results[plugin].version; - var currentVersion = plugins.plugins[plugin].package.version; + const latestVersion = results[plugin].version; + const currentVersion = plugins.plugins[plugin].package.version; return semver.gt(latestVersion, currentVersion); }); - socket.emit("results:updatable", {updatable: updatable}); + socket.emit('results:updatable', {updatable}); } catch (er) { console.warn(er); - socket.emit("results:updatable", {updatable: {}}); + socket.emit('results:updatable', {updatable: {}}); } }); - socket.on("getAvailable", async function(query) { + socket.on('getAvailable', async (query) => { try { - let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ false); - socket.emit("results:available", results); + const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); + socket.emit('results:available', results); } catch (er) { console.error(er); - socket.emit("results:available", {}); + socket.emit('results:available', {}); } }); - socket.on("search", async function(query) { + socket.on('search', async (query) => { try { - let results = await installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10); - var res = Object.keys(results) - .map(function(pluginName) { - return results[pluginName]; - }) - .filter(function(plugin) { - return !plugins.plugins[plugin.name]; - }); + const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); + let res = Object.keys(results) + .map((pluginName) => results[pluginName]) + .filter((plugin) => !plugins.plugins[plugin.name]); res = sortPluginList(res, query.sortBy, query.sortDir) - .slice(query.offset, query.offset+query.limit); - socket.emit("results:search", {results: res, query: query}); + .slice(query.offset, query.offset + query.limit); + socket.emit('results:search', {results: res, query}); } catch (er) { console.error(er); - socket.emit("results:search", {results: {}, query: query}); + socket.emit('results:search', {results: {}, query}); } }); - socket.on("install", function(plugin_name) { - installer.install(plugin_name, function(er) { + socket.on('install', (pluginName) => { + installer.install(pluginName, (er) => { if (er) console.warn(er); - socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null}); + socket.emit('finished:install', { + plugin: pluginName, + code: er ? er.code : null, + error: er ? er.message : null, + }); }); }); - socket.on("uninstall", function(plugin_name) { - installer.uninstall(plugin_name, function(er) { + socket.on('uninstall', (pluginName) => { + installer.uninstall(pluginName, (er) => { if (er) console.warn(er); - socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null}); + socket.emit('finished:uninstall', {plugin: pluginName, error: er ? er.message : null}); }); }); }); -} + return cb(); +}; -function sortPluginList(plugins, property, /*ASC?*/dir) { - return plugins.sort(function(a, b) { - if (a[property] < b[property]) { - return dir? -1 : 1; - } +const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => { + if (a[property] < b[property]) { + return dir ? -1 : 1; + } - if (a[property] > b[property]) { - return dir? 1 : -1; - } + if (a[property] > b[property]) { + return dir ? 1 : -1; + } - // a must be equal to b - return 0; - }); -} + // a must be equal to b + return 0; +}); diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 1e0d6004f78..139cce1b126 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -1,57 +1,54 @@ -var eejs = require('ep_etherpad-lite/node/eejs'); -var settings = require('ep_etherpad-lite/node/utils/Settings'); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var fs = require('fs'); - -exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/admin/settings', function(req, res) { - - var render_args = { - settings: "", - search_results: {}, - errors: [] - }; - - res.send( eejs.require("ep_etherpad-lite/templates/admin/settings.html", render_args) ); - +'use strict'; + +const eejs = require('../../eejs'); +const fs = require('fs'); +const hooks = require('../../../static/js/pluginfw/hooks'); +const settings = require('../../utils/Settings'); + +exports.expressCreateServer = (hookName, args, cb) => { + args.app.get('/admin/settings', (req, res) => { + res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { + req, + settings: '', + errors: [], + })); }); -} + return cb(); +}; -exports.socketio = function (hook_name, args, cb) { - var io = args.io.of("/settings"); - io.on('connection', function (socket) { +exports.socketio = (hookName, args, cb) => { + const io = args.io.of('/settings'); + io.on('connection', (socket) => { + const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; + if (!isAdmin) return; - if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; - - socket.on("load", function (query) { - fs.readFile('settings.json', 'utf8', function (err,data) { + 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}); + if (settings.showSettingsInAdminPage === false) { + socket.emit('settings', {results: 'NOT_ALLOWED'}); + } else { + socket.emit('settings', {results: data}); } }); }); - socket.on("saveSettings", function (settings) { - fs.writeFile('settings.json', settings, function (err) { + socket.on('saveSettings', (settings) => { + fs.writeFile('settings.json', settings, (err) => { if (err) throw err; - socket.emit("saveprogress", "saved"); + socket.emit('saveprogress', 'saved'); }); }); - socket.on("restartServer", function () { - console.log("Admin request to restart server through a socket on /admin/settings"); + socket.on('restartServer', async () => { + console.log('Admin request to restart server through a socket on /admin/settings'); settings.reloadSettings(); - hooks.aCallAll("restartServer", {}, function () {}); - + await hooks.aCallAll('restartServer'); }); - }); -} + return cb(); +}; diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index c0967c35fea..c87998e94ea 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -1,32 +1,34 @@ -var log4js = require('log4js'); -var clientLogger = log4js.getLogger("client"); -var formidable = require('formidable'); -var apiHandler = require('../../handler/APIHandler'); +const log4js = require('log4js'); +const clientLogger = log4js.getLogger('client'); +const formidable = require('formidable'); +const apiHandler = require('../../handler/APIHandler'); exports.expressCreateServer = function (hook_name, args, cb) { - //The Etherpad client side sends information about how a disconnect happened - args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) { - new formidable.IncomingForm().parse(req, function(err, fields, files) { - clientLogger.info("DIAGNOSTIC-INFO: " + fields.diagnosticInfo); - res.end("OK"); + // The Etherpad client side sends information about how a disconnect happened + args.app.post('/ep/pad/connection-diagnostic-info', (req, res) => { + new formidable.IncomingForm().parse(req, (err, fields, files) => { + clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); + res.end('OK'); }); }); - //The Etherpad client side sends information about client side javscript errors - args.app.post('/jserror', function(req, res) { - new formidable.IncomingForm().parse(req, function(err, fields, files) { + // 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) => { try { - var data = JSON.parse(fields.errorInfo) - }catch(e){ - return res.end() + var data = JSON.parse(fields.errorInfo); + } catch (e) { + return res.end(); } - clientLogger.warn(data.msg+' --', data); - res.end("OK"); + clientLogger.warn(`${data.msg} --`, data); + res.end('OK'); }); }); - //Provide a possibility to query the latest available API version - args.app.get('/api', function (req, res) { - res.json({"currentVersion" : apiHandler.latestApiVersion}); + // Provide a possibility to query the latest available API version + args.app.get('/api', (req, res) => { + res.json({currentVersion: apiHandler.latestApiVersion}); }); -} + + return cb(); +}; diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 66553621cf4..4a20b70d215 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -1,73 +1,17 @@ -var os = require("os"); -var db = require('../../db/DB'); -var stats = require('ep_etherpad-lite/node/stats') - - -exports.onShutdown = false; -exports.gracefulShutdown = function(err) { - if(err && err.stack) { - console.error(err.stack); - } else if(err) { - console.error(err); - } - - // ensure there is only one graceful shutdown running - if (exports.onShutdown) { - return; - } - - exports.onShutdown = true; - - console.log("graceful shutdown..."); - - // do the db shutdown - db.doShutdown().then(function() { - console.log("db sucessfully closed."); - - process.exit(0); - }); - - setTimeout(function() { - process.exit(1); - }, 3000); -} - -process.on('uncaughtException', exports.gracefulShutdown); +const stats = require('ep_etherpad-lite/node/stats'); exports.expressCreateServer = function (hook_name, args, cb) { exports.app = args.app; // Handle errors - args.app.use(function(err, req, res, next) { + args.app.use((err, req, res, next) => { // if an error occurs Connect will pass it down // through these "error-handling" middleware // allowing you to respond however you like - res.status(500).send({ error: 'Sorry, something bad happened!' }); - console.error(err.stack? err.stack : err.toString()); - stats.meter('http500').mark() + res.status(500).send({error: 'Sorry, something bad happened!'}); + console.error(err.stack ? err.stack : err.toString()); + stats.meter('http500').mark(); }); - /* - * Connect graceful shutdown with sigint and uncaught exception - * - * Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were - * not hooked up under Windows, because old nodejs versions did not support - * them. - * - * According to nodejs 6.x documentation, it is now safe to do so. This - * allows to gracefully close the DB connection when hitting CTRL+C under - * Windows, for example. - * - * Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events - * - * - SIGTERM is not supported on Windows, it can be listened on. - * - SIGINT from the terminal is supported on all platforms, and can usually - * be generated with +C (though this may be configurable). It is not - * generated when terminal raw mode is enabled. - */ - process.on('SIGINT', exports.gracefulShutdown); - - // when running as PID1 (e.g. in docker container) - // allow graceful shutdown on SIGTERM c.f. #3265 - process.on('SIGTERM', exports.gracefulShutdown); -} + return cb(); +}; diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index 4aa06ecb80e..7a6c38655c8 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -1,94 +1,75 @@ const assert = require('assert').strict; -var hasPadAccess = require("../../padaccess"); -var settings = require('../../utils/Settings'); -var exportHandler = require('../../handler/ExportHandler'); -var importHandler = require('../../handler/ImportHandler'); -var padManager = require("../../db/PadManager"); -var authorManager = require("../../db/AuthorManager"); -const rateLimit = require("express-rate-limit"); -const securityManager = require("../../db/SecurityManager"); +const hasPadAccess = require('../../padaccess'); +const settings = require('../../utils/Settings'); +const exportHandler = require('../../handler/ExportHandler'); +const importHandler = require('../../handler/ImportHandler'); +const padManager = require('../../db/PadManager'); +const readOnlyManager = require('../../db/ReadOnlyManager'); +const authorManager = require('../../db/AuthorManager'); +const rateLimit = require('express-rate-limit'); +const securityManager = require('../../db/SecurityManager'); +const webaccess = require('./webaccess'); -settings.importExportRateLimiting.onLimitReached = function(req, res, options) { +settings.importExportRateLimiting.onLimitReached = function (req, res, options) { // when the rate limiter triggers, write a warning in the logs console.warn(`Import/Export rate limiter triggered on "${req.originalUrl}" for IP address ${req.ip}`); -} +}; -var limiter = rateLimit(settings.importExportRateLimiting); +const limiter = rateLimit(settings.importExportRateLimiting); exports.expressCreateServer = function (hook_name, args, cb) { - // handle export requests args.app.use('/p/:pad/:rev?/export/:type', limiter); - args.app.get('/p/:pad/:rev?/export/:type', async function(req, res, next) { - var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"]; - //send a 404 if we don't support this filetype + args.app.get('/p/:pad/:rev?/export/:type', async (req, res, next) => { + const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; + // send a 404 if we don't support this filetype if (types.indexOf(req.params.type) == -1) { return next(); } // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() == "no" && - ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) { + if (settings.exportAvailable() == 'no' && + ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format. There is no converter configured`); // ACHTUNG: do not include req.params.type in res.send() because there is no HTML escaping and it would lead to an XSS - res.send("This export is not enabled at this Etherpad instance. Set the path to Abiword or soffice (LibreOffice) in settings.json to enable this feature"); + res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword or soffice (LibreOffice) in settings.json to enable this feature'); return; } - res.header("Access-Control-Allow-Origin", "*"); + res.header('Access-Control-Allow-Origin', '*'); if (await hasPadAccess(req, res)) { - let exists = await padManager.doesPadExists(req.params.pad); + let padId = req.params.pad; + + let readOnlyId = null; + if (readOnlyManager.isReadOnlyId(padId)) { + readOnlyId = padId; + padId = await readOnlyManager.getPadId(readOnlyId); + } + + const exists = await padManager.doesPadExists(padId); if (!exists) { - console.warn(`Someone tried to export a pad that doesn't exist (${req.params.pad})`); + console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); return next(); } console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); - exportHandler.doExport(req, res, req.params.pad, req.params.type); + exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); } }); // handle import requests args.app.use('/p/:pad/import', limiter); - args.app.post('/p/:pad/import', async function(req, res, next) { - if (!(await padManager.doesPadExists(req.params.pad))) { - console.warn(`Someone tried to import into a pad that doesn't exist (${req.params.pad})`); - return next(); - } - + args.app.post('/p/:pad/import', async (req, res, next) => { const {session: {user} = {}} = req; - const {accessStatus, authorID} = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); - if (accessStatus !== 'grant') return res.status(403).send('Forbidden'); - assert(authorID); - - /* - * Starting from Etherpad 1.8.3 onwards, importing into a pad is allowed - * only if a user has his browser opened and connected to the pad (i.e. a - * Socket.IO session is estabilished for him) and he has already - * contributed to that specific pad. - * - * Note that this does not have anything to do with the "session", used - * for logging into "group pads". That kind of session is not needed here. - * - * This behaviour does not apply to API requests, only to /p/$PAD$/import - * - * See: https://github.com/ether/etherpad-lite/pull/3833#discussion_r407490205 - */ - if (!settings.allowAnyoneToImport) { - const authorsPads = await authorManager.listPadsOfAuthor(authorID); - if (!authorsPads) { - console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to any pad`); - return next(); - } - if (authorsPads.padIDs.indexOf(req.params.pad) === -1) { - console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to this pad`); - return next(); - } + const {accessStatus} = await securityManager.checkAccess( + req.params.pad, req.cookies.sessionID, req.cookies.token, user); + if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { + return res.status(403).send('Forbidden'); } - - importHandler.doImport(req, res, req.params.pad); + await importHandler.doImport(req, res, req.params.pad); }); -} + + return cb(); +}; diff --git a/src/node/hooks/express/isValidJSONPName.js b/src/node/hooks/express/isValidJSONPName.js index 47755ef8624..442c963e9f1 100644 --- a/src/node/hooks/express/isValidJSONPName.js +++ b/src/node/hooks/express/isValidJSONPName.js @@ -62,14 +62,14 @@ const RESERVED_WORDS = [ 'volatile', 'while', 'with', - 'yield' + 'yield', ]; const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|\'.+\'|\d+)\])*?$/; -module.exports.check = function(inputStr) { - var isValid = true; - inputStr.split(".").forEach(function(part) { +module.exports.check = function (inputStr) { + let isValid = true; + inputStr.split('.').forEach((part) => { if (!regex.test(part)) { isValid = false; } @@ -80,4 +80,4 @@ module.exports.check = function(inputStr) { }); return isValid; -} +}; diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 76ed6693247..8ea9529c760 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -14,7 +14,7 @@ const OpenAPIBackend = require('openapi-backend').default; const formidable = require('formidable'); -const { promisify } = require('util'); +const {promisify} = require('util'); const cloneDeep = require('lodash.clonedeep'); const createHTTPError = require('http-errors'); @@ -57,12 +57,12 @@ const resources = { create: { operationId: 'createGroup', summary: 'creates a new group', - responseSchema: { groupID: { type: 'string' } }, + responseSchema: {groupID: {type: 'string'}}, }, createIfNotExistsFor: { operationId: 'createGroupIfNotExistsFor', summary: 'this functions helps you to map your application group ids to Etherpad group ids', - responseSchema: { groupID: { type: 'string' } }, + responseSchema: {groupID: {type: 'string'}}, }, delete: { operationId: 'deleteGroup', @@ -71,7 +71,7 @@ const resources = { listPads: { operationId: 'listPads', summary: 'returns all pads of this group', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, createPad: { operationId: 'createGroupPad', @@ -80,12 +80,12 @@ const resources = { listSessions: { operationId: 'listSessionsOfGroup', summary: '', - responseSchema: { sessions: { type: 'array', items: { $ref: '#/components/schemas/SessionInfo' } } }, + responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, }, list: { operationId: 'listAllGroups', summary: '', - responseSchema: { groupIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {groupIDs: {type: 'array', items: {type: 'string'}}}, }, }, @@ -94,28 +94,28 @@ const resources = { create: { operationId: 'createAuthor', summary: 'creates a new author', - responseSchema: { authorID: { type: 'string' } }, + responseSchema: {authorID: {type: 'string'}}, }, createIfNotExistsFor: { operationId: 'createAuthorIfNotExistsFor', summary: 'this functions helps you to map your application author ids to Etherpad author ids', - responseSchema: { authorID: { type: 'string' } }, + responseSchema: {authorID: {type: 'string'}}, }, listPads: { operationId: 'listPadsOfAuthor', summary: 'returns an array of all pads this author contributed to', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, listSessions: { operationId: 'listSessionsOfAuthor', summary: 'returns all sessions of an author', - responseSchema: { sessions: { type: 'array', items: { $ref: '#/components/schemas/SessionInfo' } } }, + responseSchema: {sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}}, }, // We need an operation that return a UserInfo so it can be picked up by the codegen :( getName: { operationId: 'getAuthorName', summary: 'Returns the Author Name of the author', - responseSchema: { info: { $ref: '#/components/schemas/UserInfo' } }, + responseSchema: {info: {$ref: '#/components/schemas/UserInfo'}}, }, }, @@ -124,7 +124,7 @@ const resources = { create: { operationId: 'createSession', summary: 'creates a new session. validUntil is an unix timestamp in seconds', - responseSchema: { sessionID: { type: 'string' } }, + responseSchema: {sessionID: {type: 'string'}}, }, delete: { operationId: 'deleteSession', @@ -134,7 +134,7 @@ const resources = { info: { operationId: 'getSessionInfo', summary: 'returns informations about a session', - responseSchema: { info: { $ref: '#/components/schemas/SessionInfo' } }, + responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, }, }, @@ -143,7 +143,7 @@ const resources = { listAll: { operationId: 'listAllPads', summary: 'list all the pads', - responseSchema: { padIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, }, createDiffHTML: { operationId: 'createDiffHTML', @@ -158,7 +158,7 @@ const resources = { getText: { operationId: 'getText', summary: 'returns the text of a pad', - responseSchema: { text: { type: 'string' } }, + responseSchema: {text: {type: 'string'}}, }, setText: { operationId: 'setText', @@ -167,7 +167,7 @@ const resources = { getHTML: { operationId: 'getHTML', summary: 'returns the text of a pad formatted as HTML', - responseSchema: { html: { type: 'string' } }, + responseSchema: {html: {type: 'string'}}, }, setHTML: { operationId: 'setHTML', @@ -176,12 +176,12 @@ const resources = { getRevisionsCount: { operationId: 'getRevisionsCount', summary: 'returns the number of revisions of this pad', - responseSchema: { revisions: { type: 'integer' } }, + responseSchema: {revisions: {type: 'integer'}}, }, getLastEdited: { operationId: 'getLastEdited', summary: 'returns the timestamp of the last revision of the pad', - responseSchema: { lastEdited: { type: 'integer' } }, + responseSchema: {lastEdited: {type: 'integer'}}, }, delete: { operationId: 'deletePad', @@ -190,7 +190,7 @@ const resources = { getReadOnlyID: { operationId: 'getReadOnlyID', summary: 'returns the read only link of a pad', - responseSchema: { readOnlyID: { type: 'string' } }, + responseSchema: {readOnlyID: {type: 'string'}}, }, setPublicStatus: { operationId: 'setPublicStatus', @@ -199,31 +199,22 @@ const resources = { getPublicStatus: { operationId: 'getPublicStatus', summary: 'return true of false', - responseSchema: { publicStatus: { type: 'boolean' } }, - }, - setPassword: { - operationId: 'setPassword', - summary: 'returns ok or a error message', - }, - isPasswordProtected: { - operationId: 'isPasswordProtected', - summary: 'returns true or false', - responseSchema: { passwordProtection: { type: 'boolean' } }, + responseSchema: {publicStatus: {type: 'boolean'}}, }, authors: { operationId: 'listAuthorsOfPad', summary: 'returns an array of authors who contributed to this pad', - responseSchema: { authorIDs: { type: 'array', items: { type: 'string' } } }, + responseSchema: {authorIDs: {type: 'array', items: {type: 'string'}}}, }, usersCount: { operationId: 'padUsersCount', summary: 'returns the number of user that are currently editing this pad', - responseSchema: { padUsersCount: { type: 'integer' } }, + responseSchema: {padUsersCount: {type: 'integer'}}, }, users: { operationId: 'padUsers', summary: 'returns the list of users that are currently editing this pad', - responseSchema: { padUsers: { type: 'array', items: { $ref: '#/components/schemas/UserInfo' } } }, + responseSchema: {padUsers: {type: 'array', items: {$ref: '#/components/schemas/UserInfo'}}}, }, sendClientsMessage: { operationId: 'sendClientsMessage', @@ -236,13 +227,13 @@ const resources = { getChatHistory: { operationId: 'getChatHistory', summary: 'returns the chat history', - responseSchema: { messages: { type: 'array', items: { $ref: '#/components/schemas/Message' } } }, + responseSchema: {messages: {type: 'array', items: {$ref: '#/components/schemas/Message'}}}, }, // We need an operation that returns a Message so it can be picked up by the codegen :( getChatHead: { operationId: 'getChatHead', summary: 'returns the chatHead (chat-message) of the pad', - responseSchema: { chatHead: { $ref: '#/components/schemas/Message' } }, + responseSchema: {chatHead: {$ref: '#/components/schemas/Message'}}, }, appendChatMessage: { operationId: 'appendChatMessage', @@ -393,10 +384,10 @@ const defaultResponseRefs = { const operations = {}; for (const resource in resources) { for (const action in resources[resource]) { - const { operationId, responseSchema, ...operation } = resources[resource][action]; + const {operationId, responseSchema, ...operation} = resources[resource][action]; // add response objects - const responses = { ...defaultResponseRefs }; + const responses = {...defaultResponseRefs}; if (responseSchema) { responses[200] = cloneDeep(defaultResponses.Success); responses[200].content['application/json'].schema.properties.data = { @@ -487,14 +478,14 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { }, }, }, - security: [{ ApiKey: [] }], + security: [{ApiKey: []}], }; // build operations for (const funcName in apiHandler.version[version]) { let operation = {}; if (operations[funcName]) { - operation = { ...operations[funcName] }; + operation = {...operations[funcName]}; } else { // console.warn(`No operation found for function: ${funcName}`); operation = { @@ -506,7 +497,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { // set parameters operation.parameters = operation.parameters || []; for (const paramName of apiHandler.version[version][funcName]) { - operation.parameters.push({ $ref: `#/components/parameters/${paramName}` }); + operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); if (!definition.components.parameters[paramName]) { definition.components.parameters[paramName] = { name: paramName, @@ -541,8 +532,8 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { return definition; }; -exports.expressCreateServer = async (_, args) => { - const { app } = args; +exports.expressCreateServer = (hookName, args, cb) => { + const {app} = args; // create openapi-backend handlers for each api version under /api/{version}/* for (const version in apiHandler.version) { @@ -559,7 +550,7 @@ exports.expressCreateServer = async (_, args) => { app.get(`${apiRoot}/openapi.json`, (req, res) => { // For openapi definitions, wide CORS is probably fine res.header('Access-Control-Allow-Origin', '*'); - res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); // serve latest openapi definition file under /api/openapi.json @@ -567,7 +558,7 @@ exports.expressCreateServer = async (_, args) => { if (isLatestAPIVersion) { app.get(`/${style}/openapi.json`, (req, res) => { res.header('Access-Control-Allow-Origin', '*'); - res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); } @@ -584,10 +575,10 @@ exports.expressCreateServer = async (_, args) => { // register default handlers api.register({ notFound: () => { - throw new createHTTPError.notFound('no such function'); + throw new createHTTPError.NotFound('no such function'); }, notImplemented: () => { - throw new createHTTPError.notImplemented('function not implemented'); + throw new createHTTPError.NotImplemented('function not implemented'); }, }); @@ -595,7 +586,7 @@ exports.expressCreateServer = async (_, args) => { for (const funcName in apiHandler.version[version]) { const handler = async (c, req, res) => { // parse fields from request - const { header, params, query } = c.request; + const {header, params, query} = c.request; // read form data if method was POST let formData = {}; @@ -611,9 +602,9 @@ exports.expressCreateServer = async (_, args) => { apiLogger.info(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); // pass to api handler - let data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { + const data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { // convert all errors to http errors - if (err instanceof createHTTPError.HttpError) { + if (createHTTPError.isHttpError(err)) { // pass http errors thrown by handler forward throw err; } else if (err.name == 'apierror') { @@ -629,7 +620,7 @@ exports.expressCreateServer = async (_, args) => { }); // return in common format - let response = { code: 0, message: 'ok', data: data || null }; + const response = {code: 0, message: 'ok', data: data || null}; // log response apiLogger.info(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`); @@ -663,24 +654,24 @@ exports.expressCreateServer = async (_, args) => { // https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format switch (res.statusCode) { case 403: // forbidden - response = { code: 4, message: err.message, data: null }; + response = {code: 4, message: err.message, data: null}; break; case 401: // unauthorized (no or wrong api key) - response = { code: 4, message: err.message, data: null }; + response = {code: 4, message: err.message, data: null}; break; case 404: // not found (no such function) - response = { code: 3, message: err.message, data: null }; + response = {code: 3, message: err.message, data: null}; break; case 500: // server error (internal error) - response = { code: 2, message: err.message, data: null }; + response = {code: 2, message: err.message, data: null}; break; case 400: // bad request (wrong parameters) // respond with 200 OK to keep old behavior and pass tests res.statusCode = 200; // @TODO: this is bad api design - response = { code: 1, message: err.message, data: null }; + response = {code: 1, message: err.message, data: null}; break; default: - response = { code: 1, message: err.message, data: null }; + response = {code: 1, message: err.message, data: null}; break; } } @@ -696,6 +687,7 @@ exports.expressCreateServer = async (_, args) => { }); } } + return cb(); }; // helper to get api root diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 5264c17cdf8..f17f7f0d685 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -1,13 +1,12 @@ -var readOnlyManager = require("../../db/ReadOnlyManager"); -var hasPadAccess = require("../../padaccess"); -var exporthtml = require("../../utils/ExportHtml"); +const readOnlyManager = require('../../db/ReadOnlyManager'); +const hasPadAccess = require('../../padaccess'); +const exporthtml = require('../../utils/ExportHtml'); exports.expressCreateServer = function (hook_name, args, cb) { // serve read only pad - args.app.get('/ro/:id', async function(req, res) { - + args.app.get('/ro/:id', async (req, res) => { // translate the read only pad to a padId - let padId = await readOnlyManager.getPadId(req.params.id); + const padId = await readOnlyManager.getPadId(req.params.id); if (padId == null) { res.status(404).send('404 - Not Found'); return; @@ -18,9 +17,9 @@ exports.expressCreateServer = function (hook_name, args, cb) { if (await hasPadAccess(req, res)) { // render the html document - let html = await exporthtml.getPadHTMLDocument(padId, null); + const html = await exporthtml.getPadHTMLDocument(padId, null); res.send(html); } }); - -} + return cb(); +}; diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index ad8d3c43129..8a287a9619a 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -1,29 +1,29 @@ -var padManager = require('../../db/PadManager'); -var url = require('url'); +const padManager = require('../../db/PadManager'); +const url = require('url'); exports.expressCreateServer = function (hook_name, args, cb) { - // redirects browser to the pad's sanitized url if needed. otherwise, renders the html - args.app.param('pad', async function (req, res, next, padId) { + args.app.param('pad', async (req, res, next, padId) => { // ensure the padname is valid and the url doesn't end with a / if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } - let sanitizedPadId = await padManager.sanitizePadId(padId); + const sanitizedPadId = await padManager.sanitizePadId(padId); if (sanitizedPadId === padId) { // the pad id was fine, so just render it next(); } else { // the pad id was sanitized, so we redirect to the sanitized version - var real_url = sanitizedPadId; + let real_url = sanitizedPadId; real_url = encodeURIComponent(real_url); - var query = url.parse(req.url).query; - if ( query ) real_url += '?' + query; + const query = url.parse(req.url).query; + if (query) real_url += `?${query}`; res.header('Location', real_url); - res.status(302).send('You should be redirected to ' + real_url + ''); + res.status(302).send(`You should be redirected to ${real_url}`); } }); -} + return cb(); +}; diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index b1406afd280..3d9e9debe13 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,21 +1,40 @@ -var settings = require('../../utils/Settings'); -var socketio = require('socket.io'); -var socketIORouter = require("../../handler/SocketIORouter"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); -var webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); +'use strict'; -var padMessageHandler = require("../../handler/PadMessageHandler"); +const express = require('../express'); +const proxyaddr = require('proxy-addr'); +const settings = require('../../utils/Settings'); +const socketio = require('socket.io'); +const socketIORouter = require('../../handler/SocketIORouter'); +const hooks = require('../../../static/js/pluginfw/hooks'); +const padMessageHandler = require('../../handler/PadMessageHandler'); +const util = require('util'); -var cookieParser = require('cookie-parser'); -var sessionModule = require('express-session'); +let io; -exports.expressCreateServer = function (hook_name, args, cb) { - //init socket.io and redirect all requests to the MessageHandler +exports.expressCloseServer = async () => { + // According to the socket.io documentation every client is always in the default namespace (and + // may also be in other namespaces). + const ns = io.sockets; // The Namespace object for the default namespace. + // Disconnect all socket.io clients. This probably isn't necessary; closing the socket.io Engine + // (see below) probably gracefully disconnects all clients. But that is not documented, and this + // doesn't seem to hurt, so hedge against surprising and undocumented socket.io behavior. + for (const id of await util.promisify(ns.clients.bind(ns))()) { + ns.connected[id].disconnect(true); + } + // Don't call io.close() because that closes the underlying HTTP server, which is already done + // elsewhere. (Closing an HTTP server twice throws an exception.) The `engine` property of + // socket.io Server objects is undocumented, but I don't see any other way to shut down socket.io + // without also closing the HTTP server. + io.engine.close(); +}; + +exports.expressCreateServer = (hookName, args, cb) => { + // init socket.io and redirect all requests to the MessageHandler // there shouldn't be a browser that isn't compatible to all // transports in this list at once // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling - var io = socketio({ - transports: settings.socketTransportProtocols + io = socketio({ + transports: settings.socketTransportProtocols, }).listen(args.server, { /* * Do not set the "io" cookie. @@ -39,41 +58,23 @@ exports.expressCreateServer = function (hook_name, args, cb) { cookie: false, }); - // REQUIRE a signed express-session cookie to be present, then load the session. See - // http://www.danielbaulig.de/socket-ioexpress for more info. After the session is loaded, ensure - // that the user has authenticated (if authentication is required). - // - // !!!WARNING!!! Requests to /socket.io are NOT subject to the checkAccess middleware in - // webaccess.js. If this handler fails to check for a signed express-session cookie or fails to - // check whether the user has authenticated, then any random person on the Internet can read, - // modify, or create any pad (unless the pad is password protected or an HTTP API session is - // required). - var cookieParserFn = cookieParser(webaccess.secret, {}); io.use((socket, next) => { - var data = socket.request; - if (!data.headers.cookie) { + const req = socket.request; + // Express sets req.ip but socket.io does not. Replicate Express's behavior here. + if (req.ip == null) { + if (settings.trustProxy) { + req.ip = proxyaddr(req, args.app.get('trust proxy fn')); + } else { + req.ip = socket.handshake.address; + } + } + if (!req.headers.cookie) { // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the // token and express_sid cookies have to be passed via a query parameter for unit tests. - data.headers.cookie = socket.handshake.query.cookie; + req.headers.cookie = socket.handshake.query.cookie; } - if (!data.headers.cookie && settings.loadTest) { - console.warn('bypassing socket.io authentication check due to settings.loadTest'); - return next(null, true); - } - const fail = (msg) => { return next(new Error(msg), false); }; - cookieParserFn(data, {}, function(err) { - if (err) return fail('access denied: unable to parse express_sid cookie'); - const expressSid = data.signedCookies.express_sid; - if (!expressSid) return fail ('access denied: signed express_sid cookie is required'); - args.app.sessionStore.get(expressSid, (err, session) => { - if (err || !session) return fail('access denied: bad session or session has expired'); - data.session = new sessionModule.Session(data, session); - if (settings.requireAuthentication && data.session.user == null) { - return fail('access denied: authentication required'); - } - next(null, true); - }); - }); + // See: https://socket.io/docs/faq/#Usage-with-express-session + express.sessionMiddleware(req, {}, next); }); // var socketIOLogger = log4js.getLogger("socket.io"); @@ -81,15 +82,17 @@ exports.expressCreateServer = function (hook_name, args, cb) { // https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0 // This debug logging environment is set in Settings.js - //minify socket.io javascript + // minify socket.io javascript // Due to a shitty decision by the SocketIO team minification is // no longer available, details available at: // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - //Initalize the Socket.IO Router + // Initalize the Socket.IO Router socketIORouter.setSocketIO(io); - socketIORouter.addComponent("pad", padMessageHandler); + socketIORouter.addComponent('pad', padMessageHandler); + + hooks.callAll('socketio', {app: args.app, io, server: args.server}); - hooks.callAll("socketio", {"app": args.app, "io": io, "server": args.server}); -} + return cb(); +}; diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index b11f77a0075..f53ce1ac71a 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,89 +1,81 @@ -var path = require('path'); -var eejs = require('ep_etherpad-lite/node/eejs'); -var toolbar = require("ep_etherpad-lite/node/utils/toolbar"); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var settings = require('../../utils/Settings'); +const path = require('path'); +const eejs = require('ep_etherpad-lite/node/eejs'); +const toolbar = require('ep_etherpad-lite/node/utils/toolbar'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const settings = require('../../utils/Settings'); +const webaccess = require('./webaccess'); exports.expressCreateServer = function (hook_name, args, cb) { // expose current stats - args.app.get('/stats', function(req, res) { - res.json(require('ep_etherpad-lite/node/stats').toJSON()) - }) + args.app.get('/stats', (req, res) => { + res.json(require('ep_etherpad-lite/node/stats').toJSON()); + }); - //serve index.html under / - args.app.get('/', function(req, res) - { - res.send(eejs.require("ep_etherpad-lite/templates/index.html")); + // serve index.html under / + args.app.get('/', (req, res) => { + res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); - //serve javascript.html - args.app.get('/javascript', function(req, res) - { - res.send(eejs.require("ep_etherpad-lite/templates/javascript.html")); + // serve javascript.html + args.app.get('/javascript', (req, res) => { + res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); }); - //serve robots.txt - args.app.get('/robots.txt', function(req, res) - { - var filePath = path.join(settings.root, "src", "static", "skins", settings.skinName, "robots.txt"); - res.sendFile(filePath, function(err) - { - //there is no custom robots.txt, send the default robots.txt which dissallows all - if(err) - { - filePath = path.join(settings.root, "src", "static", "robots.txt"); + // serve robots.txt + args.app.get('/robots.txt', (req, res) => { + let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + res.sendFile(filePath, (err) => { + // there is no custom robots.txt, send the default robots.txt which dissallows all + if (err) { + filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); res.sendFile(filePath); } }); }); - //serve pad.html under /p - args.app.get('/p/:pad', function(req, res, next) - { + // serve pad.html under /p + args.app.get('/p/:pad', (req, res, next) => { // The below might break for pads being rewritten - var isReadOnly = req.url.indexOf("/p/r.") === 0; + const isReadOnly = + req.url.indexOf('/p/r.') === 0 || !webaccess.userCanModify(req.params.pad, req); - hooks.callAll("padInitToolbar", { - toolbar: toolbar, - isReadOnly: isReadOnly + hooks.callAll('padInitToolbar', { + toolbar, + isReadOnly, }); - res.send(eejs.require("ep_etherpad-lite/templates/pad.html", { - req: req, - toolbar: toolbar, - isReadOnly: isReadOnly + res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { + req, + toolbar, + isReadOnly, })); }); - //serve timeslider.html under /p/$padname/timeslider - args.app.get('/p/:pad/timeslider', function(req, res, next) - { - hooks.callAll("padInitToolbar", { - toolbar: toolbar + // serve timeslider.html under /p/$padname/timeslider + args.app.get('/p/:pad/timeslider', (req, res, next) => { + hooks.callAll('padInitToolbar', { + toolbar, }); - res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html", { - req: req, - toolbar: toolbar + res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { + req, + toolbar, })); }); - //serve favicon.ico from all path levels except as a pad name - args.app.get( /\/favicon.ico$/, function(req, res) - { - var filePath = path.join(settings.root, "src", "static", "skins", settings.skinName, "favicon.ico"); + // serve favicon.ico from all path levels except as a pad name + args.app.get(/\/favicon.ico$/, (req, res) => { + let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'); - res.sendFile(filePath, function(err) - { - //there is no custom favicon, send the default favicon - if(err) - { - filePath = path.join(settings.root, "src", "static", "favicon.ico"); + res.sendFile(filePath, (err) => { + // there is no custom favicon, send the default favicon + if (err) { + filePath = path.join(settings.root, 'src', 'static', 'favicon.ico'); res.sendFile(filePath); } }); }); - -} + return cb(); +}; diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index b8c6c9d52b7..2df757e644d 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -1,14 +1,13 @@ -var minify = require('../../utils/Minify'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs"); -var CachingMiddleware = require('../../utils/caching_middleware'); -var settings = require("../../utils/Settings"); -var Yajsml = require('etherpad-yajsml'); -var _ = require("underscore"); +const minify = require('../../utils/Minify'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const CachingMiddleware = require('../../utils/caching_middleware'); +const settings = require('../../utils/Settings'); +const Yajsml = require('etherpad-yajsml'); +const _ = require('underscore'); exports.expressCreateServer = function (hook_name, args, cb) { - // Cache both minified and static. - var assetCache = new CachingMiddleware; + const assetCache = new CachingMiddleware(); args.app.all(/\/javascripts\/(.*)/, assetCache.handle); // Minify will serve static files compressed (minify enabled). It also has @@ -18,41 +17,42 @@ exports.expressCreateServer = function (hook_name, args, cb) { // Setup middleware that will package JavaScript files served by minify for // CommonJS loader on the client-side. // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. - var jsServer = new (Yajsml.Server)({ - rootPath: 'javascripts/src/' - , rootURI: 'http://invalid.invalid/static/js/' - , libraryPath: 'javascripts/lib/' - , libraryURI: 'http://invalid.invalid/static/plugins/' - , requestURIs: minify.requestURIs // Loop-back is causing problems, this is a workaround. + const jsServer = new (Yajsml.Server)({ + rootPath: 'javascripts/src/', + rootURI: 'http://invalid.invalid/static/js/', + libraryPath: 'javascripts/lib/', + libraryURI: 'http://invalid.invalid/static/plugins/', + requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround. }); - var StaticAssociator = Yajsml.associators.StaticAssociator; - var associations = + const StaticAssociator = Yajsml.associators.StaticAssociator; + const associations = Yajsml.associators.associationsForSimpleMapping(minify.tar); - var associator = new StaticAssociator(associations); + const associator = new StaticAssociator(associations); jsServer.setAssociator(associator); args.app.use(jsServer.handle.bind(jsServer)); // serve plugin definitions // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); - args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { - - var clientParts = _(plugins.parts) - .filter(function(part){ return _(part).has('client_hooks') }); + args.app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { + const clientParts = _(plugins.parts) + .filter((part) => _(part).has('client_hooks')); - var clientPlugins = {}; + const clientPlugins = {}; _(clientParts).chain() - .map(function(part){ return part.plugin }) - .uniq() - .each(function(name){ - clientPlugins[name] = _(plugins.plugins[name]).clone(); - delete clientPlugins[name]['package']; - }); - - res.header("Content-Type","application/json; charset=utf-8"); - res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); + .map((part) => part.plugin) + .uniq() + .each((name) => { + clientPlugins[name] = _(plugins.plugins[name]).clone(); + delete clientPlugins[name].package; + }); + + res.header('Content-Type', 'application/json; charset=utf-8'); + res.write(JSON.stringify({plugins: clientPlugins, parts: clientParts})); res.end(); }); -} + + return cb(); +}; diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 216715d43a0..7b32a322d6e 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -1,92 +1,92 @@ -var path = require("path") - , npm = require("npm") - , fs = require("fs") - , util = require("util"); +const path = require('path'); +const npm = require('npm'); +const fs = require('fs'); +const util = require('util'); exports.expressCreateServer = function (hook_name, args, cb) { - args.app.get('/tests/frontend/specs_list.js', async function(req, res) { - let [coreTests, pluginTests] = await Promise.all([ + args.app.get('/tests/frontend/specs_list.js', async (req, res) => { + const [coreTests, pluginTests] = await Promise.all([ exports.getCoreTests(), - exports.getPluginTests() + exports.getPluginTests(), ]); // merge the two sets of results let files = [].concat(coreTests, pluginTests).sort(); - // Remove swap files from tests - files = files.filter(el => !/\.swp$/.test(el)) + // Keep only *.js files + files = files.filter((f) => f.endsWith('.js')); - console.debug("Sent browser the following test specs:", files); + console.debug('Sent browser the following test specs:', files); res.setHeader('content-type', 'text/javascript'); - res.end("var specs_list = " + JSON.stringify(files) + ";\n"); + res.end(`var specs_list = ${JSON.stringify(files)};\n`); }); // path.join seems to normalize by default, but we'll just be explicit - var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/")); + const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/')); - var url2FilePath = function(url) { - var subPath = url.substr("/tests/frontend".length); - if (subPath == "") { - subPath = "index.html" + const url2FilePath = function (url) { + let subPath = url.substr('/tests/frontend'.length); + if (subPath == '') { + subPath = 'index.html'; } - subPath = subPath.split("?")[0]; + subPath = subPath.split('?')[0]; - var filePath = path.normalize(path.join(rootTestFolder, subPath)); + let filePath = path.normalize(path.join(rootTestFolder, subPath)); // make sure we jail the paths to the test folder, otherwise serve index if (filePath.indexOf(rootTestFolder) !== 0) { - filePath = path.join(rootTestFolder, "index.html"); + filePath = path.join(rootTestFolder, 'index.html'); } return filePath; - } + }; - args.app.get('/tests/frontend/specs/*', function (req, res) { - var specFilePath = url2FilePath(req.url); - var specFileName = path.basename(specFilePath); + args.app.get('/tests/frontend/specs/*', (req, res) => { + const specFilePath = url2FilePath(req.url); + const specFileName = path.basename(specFilePath); - fs.readFile(specFilePath, function(err, content) { + fs.readFile(specFilePath, (err, content) => { if (err) { return res.send(500); } - content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });"; + content = `describe(${JSON.stringify(specFileName)}, function(){ ${content} });`; res.send(content); }); }); - args.app.get('/tests/frontend/*', function (req, res) { - var filePath = url2FilePath(req.url); + args.app.get('/tests/frontend/*', (req, res) => { + const filePath = url2FilePath(req.url); res.sendFile(filePath); }); - args.app.get('/tests/frontend', function (req, res) { + args.app.get('/tests/frontend', (req, res) => { res.redirect('/tests/frontend/index.html'); }); -} + + return cb(); +}; const readdir = util.promisify(fs.readdir); -exports.getPluginTests = async function(callback) { - const moduleDir = "node_modules/"; - const specPath = "/static/tests/frontend/specs/"; - const staticDir = "/static/plugins/"; - - let pluginSpecs = []; - - let plugins = await readdir(moduleDir); - let promises = plugins - .map(plugin => [ plugin, moduleDir + plugin + specPath] ) - .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists - .map(([plugin, specDir]) => { - return readdir(specDir) - .then(specFiles => specFiles.map(spec => { - pluginSpecs.push(staticDir + plugin + specPath + spec); - })); - }); +exports.getPluginTests = async function (callback) { + const moduleDir = 'node_modules/'; + const specPath = '/static/tests/frontend/specs/'; + const staticDir = '/static/plugins/'; + + const pluginSpecs = []; + + const plugins = await readdir(moduleDir); + const promises = plugins + .map((plugin) => [plugin, moduleDir + plugin + specPath]) + .filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists + .map(([plugin, specDir]) => readdir(specDir) + .then((specFiles) => specFiles.map((spec) => { + pluginSpecs.push(staticDir + plugin + specPath + spec); + }))); return Promise.all(promises).then(() => pluginSpecs); -} +}; -exports.getCoreTests = function() { +exports.getCoreTests = function () { // get the core test specs return readdir('tests/frontend/specs'); -} +}; diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index b83fbbd00e3..51d57ae2e9f 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -1,18 +1,30 @@ -const express = require('express'); +'use strict'; + +const assert = require('assert').strict; const log4js = require('log4js'); const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -const ueberStore = require('../../db/SessionStore'); -const stats = require('ep_etherpad-lite/node/stats'); -const sessionModule = require('express-session'); -const cookieParser = require('cookie-parser'); +const hooks = require('../../../static/js/pluginfw/hooks'); +const readOnlyManager = require('../../db/ReadOnlyManager'); + +hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; + +const staticPathsRE = new RegExp(`^/(?:${[ + 'api/.*', + 'favicon\\.ico', + 'javascripts/.*', + 'locales\\.json', + 'pluginfw/.*', + 'static/.*', +].join('|')})$`); exports.normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { case true: return 'create'; + case 'readOnly': + case 'modify': case 'create': return level; default: @@ -21,187 +33,160 @@ exports.normalizeAuthzLevel = (level) => { return false; }; -exports.checkAccess = (req, res, next) => { - const hookResultMangle = (cb) => { - return (err, data) => { - return cb(!err && data.length && data[0]); - }; - }; +exports.userCanModify = (padId, req) => { + if (readOnlyManager.isReadOnlyId(padId)) return false; + if (!settings.requireAuthentication) return true; + const {session: {user} = {}} = req; + assert(user); // If authn required and user == null, the request should have already been denied. + if (user.readOnly) return false; + assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. + const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + assert(level); // If !level, the request should have already been denied. + return level !== 'readOnly'; +}; + +// Exported so that tests can set this to 0 to avoid unnecessary test slowness. +exports.authnFailureDelayMs = 1000; + +const checkAccess = async (req, res, next) => { + // Promisified wrapper around hooks.aCallFirst. + const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { + hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); + }); + + const aCallFirst0 = + async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; - // This may be called twice per access: once before authentication is checked and once after (if - // settings.requireAuthorization is true). - const authorize = (fail) => { - // Do not require auth for static paths and the API...this could be a bit brittle - if (req.path.match(/^\/(static|javascripts|pluginfw|api)/)) return next(); + const requireAdmin = req.path.toLowerCase().indexOf('/admin') === 0; + // This helper is used in steps 2 and 4 below, so it may be called twice per access: once before + // authentication is checked and once after (if settings.requireAuthorization is true). + const authorize = async () => { const grant = (level) => { level = exports.normalizeAuthzLevel(level); - if (!level) return fail(); + if (!level) return false; const user = req.session.user; - if (user == null) return next(); // This will happen if authentication is not required. - const padID = (req.path.match(/^\/p\/(.*)$/) || [])[1]; - if (padID == null) return next(); + if (user == null) return true; // This will happen if authentication is not required. + const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; + if (encodedPadId == null) return true; + const padId = decodeURIComponent(encodedPadId); // The user was granted access to a pad. Remember the authorization level in the user's // settings so that SecurityManager can approve or deny specific actions. if (user.padAuthorizations == null) user.padAuthorizations = {}; - user.padAuthorizations[padID] = level; - return next(); + user.padAuthorizations[padId] = level; + return true; }; - - if (req.path.toLowerCase().indexOf('/admin') !== 0) { - if (!settings.requireAuthentication) return grant('create'); - if (!settings.requireAuthorization && req.session && req.session.user) return grant('create'); - } - - if (req.session && req.session.user && req.session.user.is_admin) return grant('create'); - - hooks.aCallFirst('authorize', {req, res, next, resource: req.path}, hookResultMangle(grant)); + const isAuthenticated = req.session && req.session.user; + if (isAuthenticated && req.session.user.is_admin) return grant('create'); + const requireAuthn = requireAdmin || settings.requireAuthentication; + if (!requireAuthn) return grant('create'); + if (!isAuthenticated) return grant(false); + if (requireAdmin && !req.session.user.is_admin) return grant(false); + if (!settings.requireAuthorization) return grant('create'); + return grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); }; - /* Authentication OR authorization failed. */ - const failure = () => { - return hooks.aCallFirst('authFailure', {req, res, next}, hookResultMangle((ok) => { - if (ok) return; - // No plugin handled the authn/authz failure. Fall back to basic authentication. + // /////////////////////////////////////////////////////////////////////////////////////////////// + // Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin + // pages). If any plugin explicitly grants or denies access, skip the remaining steps. Plugins can + // use the preAuthzFailure hook to override the default 403 error. + // /////////////////////////////////////////////////////////////////////////////////////////////// + + let results; + try { + results = await aCallFirst('preAuthorize', {req, res, next}, + // This predicate will cause aCallFirst to call the hook functions one at a time until one + // of them returns a non-empty list, with an exception: If the request is for an /admin + // page, truthy entries are filtered out before checking to see whether the list is empty. + // This prevents plugin authors from accidentally granting admin privileges to the general + // public. + (r) => (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0)); + } catch (err) { + httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`); + return res.status(500).send('Internal Server Error'); + } + if (staticPathsRE.test(req.path)) results.push(true); + if (requireAdmin) { + // Filter out all 'true' entries to prevent plugin authors from accidentally granting admin + // privileges to the general public. + results = results.filter((x) => !x); + } + if (results.length > 0) { + // Access was explicitly granted or denied. If any value is false then access is denied. + if (results.every((x) => x)) return next(); + if (await aCallFirst0('preAuthzFailure', {req, res})) return; + // No plugin handled the pre-authentication authorization failure. + return res.status(403).send('Forbidden'); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // Step 2: Try to just access the thing. If access fails (perhaps authentication has not yet + // completed, or maybe different credentials are required), go to the next step. + // /////////////////////////////////////////////////////////////////////////////////////////////// + + if (await authorize()) return next(); + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // Step 3: Authenticate the user. (Or, if already logged in, reauthenticate with different + // credentials if supported by the authn scheme.) If authentication fails, give the user a 401 + // error to request new credentials. Otherwise, go to the next step. Plugins can use the + // authnFailure hook to override the default error handling behavior (e.g., to redirect to a login + // page). + // /////////////////////////////////////////////////////////////////////////////////////////////// + + if (settings.users == null) settings.users = {}; + const ctx = {req, res, users: settings.users, next}; + // If the HTTP basic auth header is present, extract the username and password so it can be given + // to authn plugins. + const httpBasicAuth = + req.headers.authorization && req.headers.authorization.search('Basic ') === 0; + if (httpBasicAuth) { + const userpass = + Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); + ctx.username = userpass.shift(); + ctx.password = userpass.join(':'); + } + if (!(await aCallFirst0('authenticate', ctx))) { + // Fall back to HTTP basic auth. + const {[ctx.username]: {password} = {}} = settings.users; + if (!httpBasicAuth || password == null || password !== ctx.password) { + httpLogger.info(`Failed authentication from IP ${req.ip}`); + if (await aCallFirst0('authnFailure', {req, res})) return; + if (await aCallFirst0('authFailure', {req, res, next})) return; + // No plugin handled the authentication failure. Fall back to basic authentication. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); // Delay the error response for 1s to slow down brute force attacks. - setTimeout(() => { - res.status(401).send('Authentication Required'); - }, 1000); - })); - }; - - // Access checking is done in three steps: - // - // 1) Try to just access the thing. If access fails (perhaps authentication has not yet completed, - // or maybe different credentials are required), go to the next step. - // 2) Try to authenticate. (Or, if already logged in, reauthenticate with different credentials if - // supported by the authn scheme.) If authentication fails, give the user a 401 error to - // request new credentials. Otherwise, go to the next step. - // 3) Try to access the thing again. If this fails, give the user a 401 error. - // - // Plugins can use the 'next' callback (from the hook's context) to break out at any point (e.g., - // to process an OAuth callback). Plugins can use the authFailure hook to override the default - // error handling behavior (e.g., to redirect to a login page). - - let step1PreAuthenticate, step2Authenticate, step3Authorize; - - step1PreAuthenticate = () => authorize(step2Authenticate); - - step2Authenticate = () => { - if (settings.users == null) settings.users = {}; - const ctx = {req, res, users: settings.users, next}; - // If the HTTP basic auth header is present, extract the username and password so it can be - // given to authn plugins. - const httpBasicAuth = - req.headers.authorization && req.headers.authorization.search('Basic ') === 0; - if (httpBasicAuth) { - const userpass = - Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); - ctx.username = userpass.shift(); - ctx.password = userpass.join(':'); + await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); + res.status(401).send('Authentication Required'); + return; } - hooks.aCallFirst('authenticate', ctx, hookResultMangle((ok) => { - if (!ok) { - // Fall back to HTTP basic auth. - if (!httpBasicAuth) return failure(); - if (!(ctx.username in settings.users)) { - httpLogger.info(`Failed authentication from IP ${req.ip} - no such user`); - return failure(); - } - if (settings.users[ctx.username].password !== ctx.password) { - httpLogger.info(`Failed authentication from IP ${req.ip} for user ${ctx.username} - incorrect password`); - return failure(); - } - httpLogger.info(`Successful authentication from IP ${req.ip} for user ${ctx.username}`); - settings.users[ctx.username].username = ctx.username; - req.session.user = settings.users[ctx.username]; - } - if (req.session.user == null) { - httpLogger.error('authenticate hook failed to add user settings to session'); - res.status(500).send('Internal Server Error'); - return; - } - step3Authorize(); - })); - }; - - step3Authorize = () => authorize(failure); - - step1PreAuthenticate(); -}; - -exports.secret = null; - -exports.expressConfigure = (hook_name, args, cb) => { - // Measure response time - args.app.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); - const sendFn = res.send; - res.send = function() { // function, not arrow, due to use of 'arguments' - stopWatch.end(); - sendFn.apply(res, arguments); - }; - next(); - }); - - // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. - // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. - if (!(settings.loglevel === 'WARN' || settings.loglevel === 'ERROR')) - args.app.use(log4js.connectLogger(httpLogger, {level: log4js.levels.DEBUG, format: ':status, :method :url'})); - - /* Do not let express create the session, so that we can retain a - * reference to it for socket.io to use. Also, set the key (cookie - * name) to a javascript identifier compatible string. Makes code - * handling it cleaner :) */ - - if (!exports.sessionStore) { - exports.sessionStore = new ueberStore(); - exports.secret = settings.sessionKey; + settings.users[ctx.username].username = ctx.username; + // Make a shallow copy so that the password property can be deleted (to prevent it from + // appearing in logs or in the database) without breaking future authentication attempts. + req.session.user = {...settings.users[ctx.username]}; + delete req.session.user.password; } + if (req.session.user == null) { + httpLogger.error('authenticate hook failed to add user settings to session'); + return res.status(500).send('Internal Server Error'); + } + const {username = ''} = req.session.user; + httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`); + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can + // use the authzFailure hook to override the default error handling behavior (e.g., to redirect to + // a login page). + // /////////////////////////////////////////////////////////////////////////////////////////////// + + if (await authorize()) return next(); + if (await aCallFirst0('authzFailure', {req, res})) return; + if (await aCallFirst0('authFailure', {req, res, next})) return; + // No plugin handled the authorization failure. + res.status(403).send('Forbidden'); +}; - const sameSite = settings.ssl ? 'Strict' : 'Lax'; - - args.app.sessionStore = exports.sessionStore; - args.app.use(sessionModule({ - secret: exports.secret, - store: args.app.sessionStore, - resave: false, - saveUninitialized: true, - name: 'express_sid', - proxy: true, - cookie: { - /* - * Firefox started enforcing sameSite, see https://github.com/ether/etherpad-lite/issues/3989 - * for details. In response we set it based on if SSL certs are set in Etherpad. Note that if - * You use Nginx or so for reverse proxy this may cause problems. Use Certificate pinning to remedy. - */ - sameSite: sameSite, - /* - * The automatic express-session mechanism for determining if the - * application is being served over ssl is similar to the one used for - * setting the language cookie, which check if one of these conditions is - * true: - * - * 1. we are directly serving the nodejs application over SSL, using the - * "ssl" options in settings.json - * - * 2. we are serving the nodejs application in plaintext, but we are using - * a reverse proxy that terminates SSL for us. In this case, the user - * has to set trustProxy = true in settings.json, and the information - * wheter the application is over SSL or not will be extracted from the - * X-Forwarded-Proto HTTP header - * - * Please note that this will not be compatible with applications being - * served over http and https at the same time. - * - * reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure - */ - secure: 'auto', - } - })); - - args.app.use(cookieParser(settings.sessionKey, {})); - - args.app.use(exports.checkAccess); +exports.expressConfigure = (hookName, args, cb) => { + args.app.use((req, res, next) => { checkAccess(req, res, next).catch(next); }); + return cb(); }; diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js index 0928b293819..610c3f68f19 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.js @@ -1,58 +1,58 @@ -var languages = require('languages4translatewiki') - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , npm = require('npm') - , plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins - , semver = require('semver') - , existsSync = require('../utils/path_exists') - , settings = require('../utils/Settings') +const languages = require('languages4translatewiki'); +const fs = require('fs'); +const path = require('path'); +const _ = require('underscore'); +const npm = require('npm'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs.js').plugins; +const semver = require('semver'); +const existsSync = require('../utils/path_exists'); +const settings = require('../utils/Settings') ; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} function getAllLocales() { - var locales2paths = {}; + const locales2paths = {}; // Puts the paths of all locale files contained in a given directory // into `locales2paths` (files from various dirs are grouped by lang code) // (only json files with valid language code as name) function extractLangs(dir) { - if(!existsSync(dir)) return; - var stat = fs.lstatSync(dir); + if (!existsSync(dir)) return; + let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; - fs.readdirSync(dir).forEach(function(file) { + fs.readdirSync(dir).forEach((file) => { file = path.resolve(dir, file); stat = fs.lstatSync(file); if (stat.isDirectory() || stat.isSymbolicLink()) return; - var ext = path.extname(file) - , locale = path.basename(file, ext).toLowerCase(); + const ext = path.extname(file); + const locale = path.basename(file, ext).toLowerCase(); if ((ext == '.json') && languages.isValid(locale)) { - if(!locales2paths[locale]) locales2paths[locale] = []; + if (!locales2paths[locale]) locales2paths[locale] = []; locales2paths[locale].push(file); } }); } - //add core supported languages first - extractLangs(npm.root+"/ep_etherpad-lite/locales"); + // add core supported languages first + extractLangs(`${npm.root}/ep_etherpad-lite/locales`); - //add plugins languages (if any) - for(var pluginName in plugins) extractLangs(path.join(npm.root, pluginName, 'locales')); + // add plugins languages (if any) + for (const pluginName in plugins) extractLangs(path.join(npm.root, pluginName, 'locales')); // Build a locale index (merge all locale data other than user-supplied overrides) - var locales = {} - _.each (locales2paths, function(files, langcode) { - locales[langcode]={}; + const locales = {}; + _.each(locales2paths, (files, langcode) => { + locales[langcode] = {}; - files.forEach(function(file) { + files.forEach((file) => { let fileContents; try { - fileContents = JSON.parse(fs.readFileSync(file,'utf8')); + fileContents = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (err) { console.error(`failed to read JSON file ${file}: ${err}`); throw err; @@ -64,17 +64,17 @@ function getAllLocales() { // Add custom strings from settings.json // Since this is user-supplied, we'll do some extra sanity checks const wrongFormatErr = Error( - "customLocaleStrings in wrong format. See documentation " + - "for Customization for Administrators, under Localization.") + 'customLocaleStrings in wrong format. See documentation ' + + 'for Customization for Administrators, under Localization.'); if (settings.customLocaleStrings) { - if (typeof settings.customLocaleStrings !== "object") throw wrongFormatErr - _.each(settings.customLocaleStrings, function(overrides, langcode) { - if (typeof overrides !== "object") throw wrongFormatErr - _.each(overrides, function(localeString, key) { - if (typeof localeString !== "string") throw wrongFormatErr - locales[langcode][key] = localeString - }) - }) + if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; + _.each(settings.customLocaleStrings, (overrides, langcode) => { + if (typeof overrides !== 'object') throw wrongFormatErr; + _.each(overrides, (localeString, key) => { + if (typeof localeString !== 'string') throw wrongFormatErr; + locales[langcode][key] = localeString; + }); + }); } return locales; @@ -83,45 +83,44 @@ function getAllLocales() { // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "español", direction: "ltr"}, ... } function getAvailableLangs(locales) { - var result = {}; - _.each(_.keys(locales), function(langcode) { + const result = {}; + _.each(_.keys(locales), (langcode) => { result[langcode] = languages.getLanguageInfo(langcode); }); return result; } // returns locale index that will be served in /locales.json -var generateLocaleIndex = function (locales) { - var result = _.clone(locales) // keep English strings - _.each(_.keys(locales), function(langcode) { - if (langcode != 'en') result[langcode]='locales/'+langcode+'.json'; +const generateLocaleIndex = function (locales) { + const result = _.clone(locales); // keep English strings + _.each(_.keys(locales), (langcode) => { + if (langcode != 'en') result[langcode] = `locales/${langcode}.json`; }); return JSON.stringify(result); -} +}; -exports.expressCreateServer = function(n, args) { - - //regenerate locales on server restart - var locales = getAllLocales(); - var localeIndex = generateLocaleIndex(locales); +exports.expressCreateServer = function (n, args, cb) { + // regenerate locales on server restart + const locales = getAllLocales(); + const localeIndex = generateLocaleIndex(locales); exports.availableLangs = getAvailableLangs(locales); - args.app.get ('/locales/:locale', function(req, res) { - //works with /locale/en and /locale/en.json requests - var locale = req.params.locale.split('.')[0]; + args.app.get('/locales/:locale', (req, res) => { + // works with /locale/en and /locale/en.json requests + const locale = req.params.locale.split('.')[0]; if (exports.availableLangs.hasOwnProperty(locale)) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.send('{"'+locale+'":'+JSON.stringify(locales[locale])+'}'); + res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { res.status(404).send('Language not available'); } - }) + }); - args.app.get('/locales.json', function(req, res) { + args.app.get('/locales.json', (req, res) => { res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); - }) - -} + }); + return cb(); +}; diff --git a/src/node/padaccess.js b/src/node/padaccess.js index 6e294403ef3..617056a9753 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -1,13 +1,13 @@ -var securityManager = require('./db/SecurityManager'); +const securityManager = require('./db/SecurityManager'); // checks for padAccess module.exports = async function (req, res) { try { const {session: {user} = {}} = req; const accessObj = await securityManager.checkAccess( - req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user); + req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessObj.accessStatus === "grant") { + if (accessObj.accessStatus === 'grant') { // there is access, continue return true; } else { @@ -19,4 +19,4 @@ module.exports = async function (req, res) { // @TODO - send internal server error here? throw err; } -} +}; diff --git a/src/node/server.js b/src/node/server.js index a1f62df4ff6..3219f518564 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +'use strict'; + /** * This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. * Static file Requests are answered directly from this module, Socket.IO messages are passed @@ -21,65 +24,112 @@ * limitations under the License. */ -const log4js = require('log4js') - , NodeVersion = require('./utils/NodeVersion') - , UpdateCheck = require('./utils/UpdateCheck') - ; - +const log4js = require('log4js'); log4js.replaceConsole(); /* * early check for version compatibility before calling * any modules that require newer versions of NodeJS */ +const NodeVersion = require('./utils/NodeVersion'); NodeVersion.enforceMinNodeVersion('10.13.0'); - -/* - * Etherpad 1.8.3 will require at least nodejs 10.13.0. - */ NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); -// Check if Etherpad version is up-to-date -UpdateCheck.check(); +const UpdateCheck = require('./utils/UpdateCheck'); +const db = require('./db/DB'); +const express = require('./hooks/express'); +const hooks = require('../static/js/pluginfw/hooks'); +const npm = require('npm/lib/npm.js'); +const plugins = require('../static/js/pluginfw/plugins'); +const settings = require('./utils/Settings'); +const util = require('util'); -/* - * start up stats counting system - */ -var stats = require('./stats'); -stats.gauge('memoryUsage', function() { - return process.memoryUsage().rss; -}); +let started = false; +let stopped = false; -/* - * no use of let or await here because it would cause startup - * to fail completely on very early versions of NodeJS - */ -var npm = require("npm/lib/npm.js"); - -npm.load({}, function() { - var settings = require('./utils/Settings'); - var db = require('./db/DB'); - var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); - var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); - - db.init() - .then(plugins.update) - .then(function() { - console.info("Installed plugins: " + plugins.formatPluginsWithVersion()); - console.debug("Installed parts:\n" + plugins.formatParts()); - console.debug("Installed hooks:\n" + plugins.formatHooks()); - - // Call loadSettings hook - hooks.aCallAll("loadSettings", { settings: settings }); - - // initalize the http server - hooks.callAll("createServer", {}); - }) - .catch(function(e) { - console.error("exception thrown: " + e.message); - if (e.stack) { - console.log(e.stack); - } - process.exit(1); - }); -}); +exports.start = async () => { + if (started) return express.server; + started = true; + if (stopped) throw new Error('restart not supported'); + + // Check if Etherpad version is up-to-date + UpdateCheck.check(); + + // start up stats counting system + const stats = require('./stats'); + stats.gauge('memoryUsage', () => process.memoryUsage().rss); + + await util.promisify(npm.load)(); + + try { + await db.init(); + await plugins.update(); + console.info(`Installed plugins: ${plugins.formatPluginsWithVersion()}`); + console.debug(`Installed parts:\n${plugins.formatParts()}`); + console.debug(`Installed hooks:\n${plugins.formatHooks()}`); + await hooks.aCallAll('loadSettings', {settings}); + await hooks.aCallAll('createServer'); + } catch (e) { + console.error(`exception thrown: ${e.message}`); + if (e.stack) console.log(e.stack); + process.exit(1); + } + + process.on('uncaughtException', exports.exit); + + /* + * Connect graceful shutdown with sigint and uncaught exception + * + * Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were + * not hooked up under Windows, because old nodejs versions did not support + * them. + * + * According to nodejs 6.x documentation, it is now safe to do so. This + * allows to gracefully close the DB connection when hitting CTRL+C under + * Windows, for example. + * + * Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events + * + * - SIGTERM is not supported on Windows, it can be listened on. + * - SIGINT from the terminal is supported on all platforms, and can usually + * be generated with +C (though this may be configurable). It is not + * generated when terminal raw mode is enabled. + */ + process.on('SIGINT', exports.exit); + + // When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265. + // Pass undefined to exports.exit because this is not an abnormal termination. + process.on('SIGTERM', () => exports.exit()); + + // Return the HTTP server to make it easier to write tests. + return express.server; +}; + +exports.stop = async () => { + if (stopped) return; + stopped = true; + console.log('Stopping Etherpad...'); + await new Promise(async (resolve, reject) => { + const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); + await hooks.aCallAll('shutdown'); + clearTimeout(id); + resolve(); + }); +}; + +exports.exit = async (err) => { + let exitCode = 0; + if (err) { + exitCode = 1; + console.error(err.stack ? err.stack : err); + } + try { + await exports.stop(); + } catch (err) { + exitCode = 1; + console.error(err.stack ? err.stack : err); + } + process.exit(exitCode); +}; + +if (require.main === module) exports.start(); diff --git a/src/node/stats.js b/src/node/stats.js index ff1752fe9e1..cecaca20d16 100644 --- a/src/node/stats.js +++ b/src/node/stats.js @@ -1,3 +1,9 @@ -var measured = require('measured-core') +'use strict'; + +const measured = require('measured-core'); module.exports = measured.createCollection(); + +module.exports.shutdown = async (hookName, context) => { + module.exports.end(); +}; diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index eed844e73a1..b75487d7578 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -18,45 +18,39 @@ * limitations under the License. */ -var spawn = require('child_process').spawn; -var async = require("async"); -var settings = require("./Settings"); -var os = require('os'); - -var doConvertTask; - -//on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform -if(os.type().indexOf("Windows") > -1) -{ - var stdoutBuffer = ""; - - doConvertTask = function(task, callback) - { - //span an abiword process to perform the conversion - var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]); - - //delegate the processing of stdout to another function - abiword.stdout.on('data', function (data) - { - //add data to buffer - stdoutBuffer+=data.toString(); +const spawn = require('child_process').spawn; +const async = require('async'); +const settings = require('./Settings'); +const os = require('os'); + +let doConvertTask; + +// on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform +if (os.type().indexOf('Windows') > -1) { + let stdoutBuffer = ''; + + doConvertTask = function (task, callback) { + // span an abiword process to perform the conversion + const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]); + + // delegate the processing of stdout to another function + abiword.stdout.on('data', (data) => { + // add data to buffer + stdoutBuffer += data.toString(); }); - //append error messages to the buffer - abiword.stderr.on('data', function (data) - { + // append error messages to the buffer + abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - //throw exceptions if abiword is dieing - abiword.on('exit', function (code) - { - if(code != 0) { + // throw exceptions if abiword is dieing + abiword.on('exit', (code) => { + if (code != 0) { return callback(`Abiword died with exit code ${code}`); } - if(stdoutBuffer != "") - { + if (stdoutBuffer != '') { console.log(stdoutBuffer); } @@ -64,55 +58,48 @@ if(os.type().indexOf("Windows") > -1) }); }; - exports.convertFile = function(srcFile, destFile, type, callback) - { - doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback); + exports.convertFile = function (srcFile, destFile, type, callback) { + doConvertTask({srcFile, destFile, type}, callback); }; } -//on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout -//thats much faster, about factor 10 -else -{ - //spawn the abiword process - var abiword; - var stdoutCallback = null; - var spawnAbiword = function (){ - abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]); - var stdoutBuffer = ""; - var firstPrompt = true; - - //append error messages to the buffer - abiword.stderr.on('data', function (data) - { +// on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout +// thats much faster, about factor 10 +else { + // spawn the abiword process + let abiword; + let stdoutCallback = null; + var spawnAbiword = function () { + abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); + let stdoutBuffer = ''; + let firstPrompt = true; + + // append error messages to the buffer + abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - //abiword died, let's restart abiword and return an error with the callback - abiword.on('exit', function (code) - { + // abiword died, let's restart abiword and return an error with the callback + abiword.on('exit', (code) => { spawnAbiword(); stdoutCallback(`Abiword died with exit code ${code}`); }); - //delegate the processing of stdout to a other function - abiword.stdout.on('data',function (data) - { - //add data to buffer - stdoutBuffer+=data.toString(); - - //we're searching for the prompt, cause this means everything we need is in the buffer - if(stdoutBuffer.search("AbiWord:>") != -1) - { - //filter the feedback message - var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer; - - //reset the buffer - stdoutBuffer = ""; - - //call the callback with the error message - //skip the first prompt - if(stdoutCallback != null && !firstPrompt) - { + // delegate the processing of stdout to a other function + abiword.stdout.on('data', (data) => { + // add data to buffer + stdoutBuffer += data.toString(); + + // we're searching for the prompt, cause this means everything we need is in the buffer + if (stdoutBuffer.search('AbiWord:>') != -1) { + // filter the feedback message + const err = stdoutBuffer.search('OK') != -1 ? null : stdoutBuffer; + + // reset the buffer + stdoutBuffer = ''; + + // call the callback with the error message + // skip the first prompt + if (stdoutCallback != null && !firstPrompt) { stdoutCallback(err); stdoutCallback = null; } @@ -123,26 +110,23 @@ else }; spawnAbiword(); - doConvertTask = function(task, callback) - { - abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n"); - //create a callback that calls the task callback and the caller callback - stdoutCallback = function (err) - { + doConvertTask = function (task, callback) { + abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); + // create a callback that calls the task callback and the caller callback + stdoutCallback = function (err) { callback(); - console.log("queue continue"); - try{ + console.log('queue continue'); + try { task.callback(err); - }catch(e){ - console.error("Abiword File failed to convert", e); + } catch (e) { + console.error('Abiword File failed to convert', e); } }; }; - //Queue with the converts we have to do - var queue = async.queue(doConvertTask, 1); - exports.convertFile = function(srcFile, destFile, type, callback) - { - queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback}); + // Queue with the converts we have to do + const queue = async.queue(doConvertTask, 1); + exports.convertFile = function (srcFile, destFile, type, callback) { + queue.push({srcFile, destFile, type, callback}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js index 9d864c474e8..22294cfe282 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.js @@ -18,17 +18,17 @@ * limitations under the License. */ -var log4js = require('log4js'); -var path = require('path'); -var _ = require('underscore'); +const log4js = require('log4js'); +const path = require('path'); +const _ = require('underscore'); -var absPathLogger = log4js.getLogger('AbsolutePaths'); +const absPathLogger = log4js.getLogger('AbsolutePaths'); /* * findEtherpadRoot() computes its value only on first invocation. * Subsequent invocations are served from this variable. */ -var etherpadRoot = null; +let etherpadRoot = null; /** * If stringArray's last elements are exactly equal to lastDesiredElements, @@ -40,9 +40,9 @@ var etherpadRoot = null; * @return {string[]|boolean} The shortened array, or false if there was no * overlap. */ -var popIfEndsWith = function(stringArray, lastDesiredElements) { +const popIfEndsWith = function (stringArray, lastDesiredElements) { if (stringArray.length <= lastDesiredElements.length) { - absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1 } elements`); + absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`); return false; } @@ -72,7 +72,7 @@ var popIfEndsWith = function(stringArray, lastDesiredElements) { * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = function() { +exports.findEtherpadRoot = function () { if (etherpadRoot !== null) { return etherpadRoot; } @@ -87,7 +87,7 @@ exports.findEtherpadRoot = function() { * * \src */ - var maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); + let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) { /* @@ -126,7 +126,7 @@ exports.findEtherpadRoot = function() { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = function(somePath) { +exports.makeAbsolute = function (somePath) { if (path.isAbsolute(somePath)) { return somePath; } @@ -145,7 +145,7 @@ exports.makeAbsolute = function(somePath) { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = function(parent, arbitraryDir) { +exports.isSubdir = function (parent, arbitraryDir) { // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 const relative = path.relative(parent, arbitraryDir); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.js index 04c532fa007..6297a4f8ce4 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.js @@ -22,30 +22,30 @@ // An object containing the parsed command-line options exports.argv = {}; -var argv = process.argv.slice(2); -var arg, prevArg; +const argv = process.argv.slice(2); +let arg, prevArg; // Loop through args -for ( var i = 0; i < argv.length; i++ ) { +for (let i = 0; i < argv.length; i++) { arg = argv[i]; // Override location of settings.json file - if ( prevArg == '--settings' || prevArg == '-s' ) { + if (prevArg == '--settings' || prevArg == '-s') { exports.argv.settings = arg; } // Override location of credentials.json file - if ( prevArg == '--credentials' ) { + if (prevArg == '--credentials') { exports.argv.credentials = arg; } // Override location of settings.json file - if ( prevArg == '--sessionkey' ) { + if (prevArg == '--sessionkey') { exports.argv.sessionkey = arg; } // Override location of settings.json file - if ( prevArg == '--apikey' ) { + if (prevArg == '--apikey') { exports.argv.apikey = arg; } diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 0e8ef3bf1c2..ace298ab748 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -15,40 +15,39 @@ */ -let db = require("../db/DB"); +const db = require('../db/DB'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -exports.getPadRaw = async function(padId) { +exports.getPadRaw = async function (padId) { + const padKey = `pad:${padId}`; + const padcontent = await db.get(padKey); - let padKey = "pad:" + padId; - let padcontent = await db.get(padKey); - - let records = [ padKey ]; + const records = [padKey]; for (let i = 0; i <= padcontent.head; i++) { - records.push(padKey + ":revs:" + i); + records.push(`${padKey}:revs:${i}`); } for (let i = 0; i <= padcontent.chatHead; i++) { - records.push(padKey + ":chat:" + i); + records.push(`${padKey}:chat:${i}`); } - let data = {}; - for (let key of records) { - + const data = {}; + for (const key of records) { // For each piece of info about a pad. - let entry = data[key] = await db.get(key); + const entry = data[key] = await db.get(key); // Get the Pad Authors if (entry.pool && entry.pool.numToAttrib) { - let authors = entry.pool.numToAttrib; + const authors = entry.pool.numToAttrib; - for (let k of Object.keys(authors)) { - if (authors[k][0] === "author") { - let authorId = authors[k][1]; + for (const k of Object.keys(authors)) { + if (authors[k][0] === 'author') { + const authorId = authors[k][1]; // Get the author info - let authorEntry = await db.get("globalAuthor:" + authorId); + const authorEntry = await db.get(`globalAuthor:${authorId}`); if (authorEntry) { - data["globalAuthor:" + authorId] = authorEntry; + data[`globalAuthor:${authorId}`] = authorEntry; if (authorEntry.padIDs) { authorEntry.padIDs = padId; } @@ -58,5 +57,13 @@ exports.getPadRaw = async function(padId) { } } + // get content that has a different prefix IE comments:padId:foo + // a plugin would return something likle ['comments', 'cakes'] + const prefixes = await hooks.aCallAll('exportEtherpadAdditionalContent'); + await Promise.all(prefixes.map(async (prefix) => { + const key = `${prefix}:${padId}`; + data[key] = await db.get(key); + })); + return data; -} +}; diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index f6ec4486ed8..e498d4c4263 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -18,24 +18,23 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -exports.getPadPlainText = function(pad, revNum){ - var _analyzeLine = exports._analyzeLine; - var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - var apool = pad.pool; +exports.getPadPlainText = function (pad, revNum) { + const _analyzeLine = exports._analyzeLine; + const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const apool = pad.pool; - var pieces = []; - for (var i = 0; i < textLines.length; i++){ - var line = _analyzeLine(textLines[i], attribLines[i], apool); - if (line.listLevel){ - var numSpaces = line.listLevel * 2 - 1; - var bullet = '*'; + const pieces = []; + for (let i = 0; i < textLines.length; i++) { + const line = _analyzeLine(textLines[i], attribLines[i], apool); + if (line.listLevel) { + const numSpaces = line.listLevel * 2 - 1; + const bullet = '*'; pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n'); - } - else{ + } else { pieces.push(line.text, '\n'); } } @@ -44,38 +43,37 @@ exports.getPadPlainText = function(pad, revNum){ }; -exports._analyzeLine = function(text, aline, apool){ - var line = {}; +exports._analyzeLine = function (text, aline, apool) { + const line = {}; // identify list - var lineMarker = 0; + let lineMarker = 0; line.listLevel = 0; - if (aline){ - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()){ - var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); - if (listType){ + if (aline) { + const opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) { + let listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + if (listType) { lineMarker = 1; listType = /([a-z]+)([0-9]+)/.exec(listType); - if (listType){ + if (listType) { line.listTypeName = listType[1]; line.listLevel = Number(listType[2]); } } } - var opIter2 = Changeset.opIterator(aline); - if (opIter2.hasNext()){ - var start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); - if (start){ - line.start = start; + const opIter2 = Changeset.opIterator(aline); + if (opIter2.hasNext()) { + const start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); + if (start) { + line.start = start; } } } - if (lineMarker){ + if (lineMarker) { line.text = text.substring(1); line.aline = Changeset.subattribution(aline, 1); - } - else{ + } else { line.text = text; line.aline = aline; } @@ -83,8 +81,6 @@ exports._analyzeLine = function(text, aline, apool){ }; -exports._encodeWhitespace = function(s){ - return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, function(c){ - return "&#" +c.codePointAt(0) + ";"; - }); +exports._encodeWhitespace = function (s) { + return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); }; diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index d0ebf20de8a..2f5a77c9ac5 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -14,17 +14,17 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); -var _ = require('underscore'); -var Security = require('ep_etherpad-lite/static/js/security'); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var eejs = require('ep_etherpad-lite/node/eejs'); -var _analyzeLine = require('./ExportHelper')._analyzeLine; -var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; - -async function getPadHTML(pad, revNum) -{ +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const padManager = require('../db/PadManager'); +const _ = require('underscore'); +const Security = require('ep_etherpad-lite/static/js/security'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const eejs = require('ep_etherpad-lite/node/eejs'); +const _analyzeLine = require('./ExportHelper')._analyzeLine; +const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; +const padutils = require('../../static/js/pad_utils').padutils; + +async function getPadHTML(pad, revNum) { let atext = pad.atext; // fetch revision atext @@ -33,113 +33,110 @@ async function getPadHTML(pad, revNum) } // convert atext to html - return getHTMLFromAtext(pad, atext); + return await getHTMLFromAtext(pad, atext); } exports.getPadHTML = getPadHTML; exports.getHTMLFromAtext = getHTMLFromAtext; -function getHTMLFromAtext(pad, atext, authorColors) -{ - var apool = pad.apool(); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - - var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; - var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - - // prepare tags stored as ['tag', true] to be exported - hooks.aCallAll("exportHtmlAdditionalTags", pad, function(err, newProps){ - newProps.forEach(function (propName, i) { - tags.push(propName); - props.push(propName); - }); - }); +async function getHTMLFromAtext(pad, atext, authorColors) { + const apool = pad.apool(); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML - // with tags like - hooks.aCallAll("exportHtmlAdditionalTagsWithData", pad, function(err, newProps){ - newProps.forEach(function (propName, i) { - tags.push('span data-' + propName[0] + '="' + propName[1] + '"'); - props.push(propName); - }); - }); + const tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; + const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + + await Promise.all([ + // prepare tags stored as ['tag', true] to be exported + hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => { + newProps.forEach((prop) => { + tags.push(prop); + props.push(prop); + }); + }), + // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags + // like + hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { + newProps.forEach((prop) => { + tags.push(`span data-${prop[0]}="${prop[1]}"`); + props.push(prop); + }); + }), + ]); // holds a map of used styling attributes (*1, *2, etc) in the apool // and maps them to an index in props // *3:2 -> the attribute *3 means strong // *2:5 -> the attribute *2 means s(trikethrough) - var anumMap = {}; - var css = ""; + const anumMap = {}; + let css = ''; - var stripDotFromAuthorID = function(id){ - return id.replace(/\./g,'_'); + const stripDotFromAuthorID = function (id) { + return id.replace(/\./g, '_'); }; - if(authorColors){ - css+=""; + css += ''; } // iterates over all props(h1,h2,strong,...), checks if it is used in // this pad, and if yes puts its attrib id->props value into anumMap - props.forEach(function (propName, i) - { - var attrib = [propName, true]; + props.forEach((propName, i) => { + let attrib = [propName, true]; if (_.isArray(propName)) { // propName can be in the form of ['color', 'red'], // see hook exportHtmlAdditionalTagsWithData attrib = propName; } - var propTrueNum = apool.putAttrib(attrib, true); - if (propTrueNum >= 0) - { + const propTrueNum = apool.putAttrib(attrib, true); + if (propTrueNum >= 0) { anumMap[propTrueNum] = i; } }); - function getLineHTML(text, attribs) - { + function getLineHTML(text, attribs) { // Use order of tags (b/i/u) as order of nesting, for simplicity // and decent nesting. For example, // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - var taker = Changeset.stringIterator(text); - var assem = Changeset.stringAssembler(); - var openTags = []; + const taker = Changeset.stringIterator(text); + const assem = Changeset.stringAssembler(); + const openTags = []; - function getSpanClassFor(i){ - //return if author colors are disabled + function getSpanClassFor(i) { + // return if author colors are disabled if (!authorColors) return false; - var property = props[i]; + const property = props[i]; // we are not insterested on properties in the form of ['color', 'red'], // see hook exportHtmlAdditionalTagsWithData @@ -147,12 +144,12 @@ function getHTMLFromAtext(pad, atext, authorColors) return false; } - if(property.substr(0,6) === "author"){ + if (property.substr(0, 6) === 'author') { return stripDotFromAuthorID(property); } - if(property === "removed"){ - return "removed"; + if (property === 'removed') { + return 'removed'; } return false; @@ -160,17 +157,16 @@ function getHTMLFromAtext(pad, atext, authorColors) // tags added by exportHtmlAdditionalTagsWithData will be exported as with // data attributes - function isSpanWithData(i){ - var property = props[i]; + function isSpanWithData(i) { + const property = props[i]; return _.isArray(property); } - function emitOpenTag(i) - { + function emitOpenTag(i) { openTags.unshift(i); - var spanClass = getSpanClassFor(i); + const spanClass = getSpanClassFor(i); - if(spanClass){ + if (spanClass) { assem.append(''); @@ -182,13 +178,12 @@ function getHTMLFromAtext(pad, atext, authorColors) } // this closes an open tag and removes its reference from openTags - function emitCloseTag(i) - { + function emitCloseTag(i) { openTags.shift(); - var spanClass = getSpanClassFor(i); - var spanWithData = isSpanWithData(i); + const spanClass = getSpanClassFor(i); + const spanWithData = isSpanWithData(i); - if(spanClass || spanWithData){ + if (spanClass || spanWithData) { assem.append(''); } else { assem.append(' { + if (a in anumMap) { usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. } }); - var outermostTag = -1; + let outermostTag = -1; // find the outer most open tag that is no longer used - for (var i = openTags.length - 1; i >= 0; i--) - { - if (usedAttribs.indexOf(openTags[i]) === -1) - { + for (var i = openTags.length - 1; i >= 0; i--) { + if (usedAttribs.indexOf(openTags[i]) === -1) { outermostTag = i; break; } } // close all tags upto the outer most - if (outermostTag !== -1) - { - while ( outermostTag >= 0 ) - { + if (outermostTag !== -1) { + while (outermostTag >= 0) { emitCloseTag(openTags[0]); outermostTag--; } } // open all tags that are used but not open - for (i=0; i < usedAttribs.length; i++) - { - if (openTags.indexOf(usedAttribs[i]) === -1) - { + for (i = 0; i < usedAttribs.length; i++) { + if (openTags.indexOf(usedAttribs[i]) === -1) { emitOpenTag(usedAttribs[i]); } } - var chars = o.chars; - if (o.lines) - { + let chars = o.chars; + if (o.lines) { chars--; // exclude newline at end of line, if present } - var s = taker.take(chars); + let s = taker.take(chars); - //removes the characters with the code 12. Don't know where they come - //from but they break the abiword parser and are completly useless - s = s.replace(String.fromCharCode(12), ""); + // removes the characters with the code 12. Don't know where they come + // from but they break the abiword parser and are completly useless + s = s.replace(String.fromCharCode(12), ''); assem.append(_encodeWhitespace(Security.escapeHTML(s))); } // end iteration over spans in line // close all the tags that are open after the last op - while (openTags.length > 0) - { + while (openTags.length > 0) { emitCloseTag(openTags[0]); } } // end processNextChars - if (urls) - { - urls.forEach(function (urlData) - { - var startIndex = urlData[0]; - var url = urlData[1]; - var urlLength = url.length; + if (urls) { + urls.forEach((urlData) => { + const startIndex = urlData[0]; + const url = urlData[1]; + const urlLength = url.length; processNextChars(startIndex - idx); // Using rel="noreferrer" stops leaking the URL/location of the exported HTML when clicking links in the document. // Not all browsers understand this attribute, but it's part of the HTML5 standard. @@ -292,16 +272,16 @@ function getHTMLFromAtext(pad, atext, authorColors) // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - assem.append(''); + assem.append(``); processNextChars(urlLength); assem.append(''); }); } processNextChars(text.length - idx); - + return _processSpaces(assem.toString()); } // end getLineHTML - var pieces = [css]; + const pieces = [css]; // Need to deal with constraints imposed on HTML lists; can // only gain one level of nesting at once, can't change type @@ -310,57 +290,48 @@ function getHTMLFromAtext(pad, atext, authorColors) // so we want to do something reasonable there. We also // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - var openLists = []; - for (var i = 0; i < textLines.length; i++) - { + let openLists = []; + for (let i = 0; i < textLines.length; i++) { var context; var line = _analyzeLine(textLines[i], attribLines[i], apool); - var lineContent = getLineHTML(line.text, line.aline); - if (line.listLevel)//If we are inside a list + const lineContent = getLineHTML(line.text, line.aline); + if (line.listLevel)// If we are inside a list { context = { - line: line, - lineContent: lineContent, - apool: apool, + line, + lineContent, + apool, attribLine: attribLines[i], text: textLines[i], - padId: pad.id + padId: pad.id, }; - var prevLine = null; - var nextLine = null; - if (i > 0) - { - prevLine = _analyzeLine(textLines[i -1], attribLines[i -1], apool); + let prevLine = null; + let nextLine = null; + if (i > 0) { + prevLine = _analyzeLine(textLines[i - 1], attribLines[i - 1], apool); } - if (i < textLines.length) - { + if (i < textLines.length) { nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); } - hooks.aCallAll('getLineHTMLForExport', context); - //To create list parent elements - if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) - { - var exists = _.find(openLists, function (item) - { - return (item.level === line.listLevel && item.type === line.listTypeName); - }); + await hooks.aCallAll('getLineHTMLForExport', context); + // To create list parent elements + if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) { + const exists = _.find(openLists, (item) => (item.level === line.listLevel && item.type === line.listTypeName)); if (!exists) { - var prevLevel = 0; + let prevLevel = 0; if (prevLine && prevLine.listLevel) { prevLevel = prevLine.listLevel; } - if (prevLine && line.listTypeName !== prevLine.listTypeName) - { + if (prevLine && line.listTypeName !== prevLine.listTypeName) { prevLevel = 0; } for (var diff = prevLevel; diff < line.listLevel; diff++) { openLists.push({level: diff, type: line.listTypeName}); - var prevPiece = pieces[pieces.length - 1]; + const prevPiece = pieces[pieces.length - 1]; - if (prevPiece.indexOf("") === 0) - { - /* + if (prevPiece.indexOf('') === 0) { + /* uncommenting this breaks nested ols.. if the previous item is NOT a ul, NOT an ol OR closing li then close the list so we consider this HTML, I inserted ** where it throws a problem in Example Wrong.. @@ -376,19 +347,16 @@ function getHTMLFromAtext(pad, atext, authorColors) // pieces.push(""); */ - if( (nextLine.listTypeName === 'number') && (nextLine.text === '') ){ + if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) { // is the listTypeName check needed here? null text might be completely fine! // TODO Check against Uls // don't do anything because the next item is a nested ol openener so we need to keep the li open - }else{ - pieces.push("
  • "); + } else { + pieces.push('
  • '); } - - } - if (line.listTypeName === "number") - { + if (line.listTypeName === 'number') { // We introduce line.start here, this is useful for continuing Ordered list line numbers // in case you have a bullet in a list IE you Want // 1. hello @@ -399,182 +367,140 @@ function getHTMLFromAtext(pad, atext, authorColors) // TODO: This logic could also be used to continue OL with indented content // but that's a job for another day.... - if(line.start){ - pieces.push("
      "); - }else{ - pieces.push("
        "); + if (line.start) { + pieces.push(`
          `); + } else { + pieces.push(`
            `); } - } - else - { - pieces.push("
              "); + } else { + pieces.push(`
                `); } } } } // if we're going up a level we shouldn't be adding.. - if(context.lineContent){ - pieces.push("
              • ", context.lineContent); + if (context.lineContent) { + pieces.push('
              • ', context.lineContent); } // To close list elements - if (nextLine && nextLine.listLevel === line.listLevel && line.listTypeName === nextLine.listTypeName) - { - if(context.lineContent){ - if( (nextLine.listTypeName === 'number') && (nextLine.text === '') ){ + if (nextLine && nextLine.listLevel === line.listLevel && line.listTypeName === nextLine.listTypeName) { + if (context.lineContent) { + if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) { // is the listTypeName check needed here? null text might be completely fine! // TODO Check against Uls // don't do anything because the next item is a nested ol openener so we need to keep the li open - }else{ - pieces.push("
              • "); + } else { + pieces.push(''); } - } } - if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) - { - var nextLevel = 0; + if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) { + let nextLevel = 0; if (nextLine && nextLine.listLevel) { nextLevel = nextLine.listLevel; } - if (nextLine && line.listTypeName !== nextLine.listTypeName) - { + if (nextLine && line.listTypeName !== nextLine.listTypeName) { nextLevel = 0; } - for (var diff = nextLevel; diff < line.listLevel; diff++) - { - openLists = openLists.filter(function(el) - { - return el.level !== diff && el.type !== line.listTypeName; - }); + for (var diff = nextLevel; diff < line.listLevel; diff++) { + openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName); - if (pieces[pieces.length - 1].indexOf(""); + if (pieces[pieces.length - 1].indexOf(''); } - if (line.listTypeName === "number") - { - pieces.push("
          "); - } - else - { - pieces.push(""); + if (line.listTypeName === 'number') { + pieces.push('
        '); + } else { + pieces.push(''); } } } - } - else//outside any list, need to close line.listLevel of lists + } else// outside any list, need to close line.listLevel of lists { context = { - line: line, - lineContent: lineContent, - apool: apool, + line, + lineContent, + apool, attribLine: attribLines[i], text: textLines[i], - padId: pad.id + padId: pad.id, }; - hooks.aCallAll("getLineHTMLForExport", context); - pieces.push(context.lineContent, "
        "); + await hooks.aCallAll('getLineHTMLForExport', context); + pieces.push(context.lineContent, '
        '); } } return pieces.join(''); } -exports.getPadHTMLDocument = async function (padId, revNum) -{ - let pad = await padManager.getPad(padId); +exports.getPadHTMLDocument = async function (padId, revNum) { + const pad = await padManager.getPad(padId); // Include some Styles into the Head for Export - let stylesForExportCSS = ""; - let stylesForExport = await hooks.aCallAll("stylesForExport", padId); - stylesForExport.forEach(function(css){ + let stylesForExportCSS = ''; + const stylesForExport = await hooks.aCallAll('stylesForExport', padId); + stylesForExport.forEach((css) => { stylesForExportCSS += css; }); let html = await getPadHTML(pad, revNum); - return eejs.require("ep_etherpad-lite/templates/export_html.html", { + for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) { + html += hookHtml; + } + + return eejs.require('ep_etherpad-lite/templates/export_html.html', { body: html, padId: Security.escapeHTML(padId), - extraCSS: stylesForExportCSS + extraCSS: stylesForExportCSS, }); -} +}; // copied from ACE -var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; -var _REGEX_SPACE = /\s/; -var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); -var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); - -// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] - - -function _findURLs(text) -{ - _REGEX_URL.lastIndex = 0; - var urls = null; - var execResult; - while ((execResult = _REGEX_URL.exec(text))) - { - urls = (urls || []); - var startIndex = execResult.index; - var url = execResult[0]; - urls.push([startIndex, url]); - } - - return urls; -} - - -// copied from ACE -function _processSpaces(s){ - var doesWrap = true; - if (s.indexOf("<") < 0 && !doesWrap){ +function _processSpaces(s) { + const doesWrap = true; + if (s.indexOf('<') < 0 && !doesWrap) { // short-cut return s.replace(/ /g, ' '); } - var parts = []; - s.replace(/<[^>]*>?| |[^ <]+/g, function (m){ + const parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { parts.push(m); }); - if (doesWrap){ - var endOfLine = true; - var beforeSpace = false; + if (doesWrap) { + let endOfLine = true; + let beforeSpace = false; // last space in a run is normal, others are nbsp, // end of line is nbsp - for (var i = parts.length - 1; i >= 0; i--){ + for (var i = parts.length - 1; i >= 0; i--) { var p = parts[i]; - if (p == " "){ + if (p == ' ') { if (endOfLine || beforeSpace) parts[i] = ' '; endOfLine = false; beforeSpace = true; - } - else if (p.charAt(0) != "<"){ + } else if (p.charAt(0) != '<') { endOfLine = false; beforeSpace = false; } } // beginning of line is nbsp - for (i = 0; i < parts.length; i++){ + for (i = 0; i < parts.length; i++) { p = parts[i]; - if (p == " "){ + if (p == ' ') { parts[i] = ' '; break; - } - else if (p.charAt(0) != "<"){ + } else if (p.charAt(0) != '<') { break; } } - } - else - { - for (i = 0; i < parts.length; i++){ + } else { + for (i = 0; i < parts.length; i++) { p = parts[i]; - if (p == " "){ + if (p == ' ') { parts[i] = ' '; } } diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index 4a9c0ba407a..9d47896bc0d 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -18,13 +18,12 @@ * limitations under the License. */ -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); -var _analyzeLine = require('./ExportHelper')._analyzeLine; +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const padManager = require('../db/PadManager'); +const _analyzeLine = require('./ExportHelper')._analyzeLine; // This is slightly different than the HTML method as it passes the output to getTXTFromAText -var getPadTXT = async function(pad, revNum) -{ +const getPadTXT = async function (pad, revNum) { let atext = pad.atext; if (revNum != undefined) { @@ -34,58 +33,57 @@ var getPadTXT = async function(pad, revNum) // convert atext to html return getTXTFromAtext(pad, atext); -} +}; // This is different than the functionality provided in ExportHtml as it provides formatting // functionality that is designed specifically for TXT exports -function getTXTFromAtext(pad, atext, authorColors) -{ - var apool = pad.apool(); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - - var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - var anumMap = {}; - var css = ""; - - props.forEach(function(propName, i) { - var propTrueNum = apool.putAttrib([propName, true], true); +function getTXTFromAtext(pad, atext, authorColors) { + const apool = pad.apool(); + const textLines = atext.text.slice(0, -1).split('\n'); + const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + + const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + const anumMap = {}; + const css = ''; + + props.forEach((propName, i) => { + const propTrueNum = apool.putAttrib([propName, true], true); if (propTrueNum >= 0) { anumMap[propTrueNum] = i; } }); function getLineTXT(text, attribs) { - var propVals = [false, false, false]; - var ENTER = 1; - var STAY = 2; - var LEAVE = 0; + const propVals = [false, false, false]; + const ENTER = 1; + const STAY = 2; + const LEAVE = 0; // Use order of tags (b/i/u) as order of nesting, for simplicity // and decent nesting. For example, // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - var taker = Changeset.stringIterator(text); - var assem = Changeset.stringAssembler(); + const taker = Changeset.stringIterator(text); + const assem = Changeset.stringAssembler(); - var idx = 0; + let idx = 0; function processNextChars(numChars) { if (numChars <= 0) { return; } - var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; while (iter.hasNext()) { - var o = iter.next(); + const o = iter.next(); var propChanged = false; - Changeset.eachAttribNumber(o.attribs, function(a) { + Changeset.eachAttribNumber(o.attribs, (a) => { if (a in anumMap) { - var i = anumMap[a]; // i = 0 => bold, etc. + const i = anumMap[a]; // i = 0 => bold, etc. if (!propVals[i]) { propVals[i] = ENTER; @@ -110,20 +108,18 @@ function getTXTFromAtext(pad, atext, authorColors) // according to what happens at start of span if (propChanged) { // leaving bold (e.g.) also leaves italics, etc. - var left = false; + let left = false; for (var i = 0; i < propVals.length; i++) { - var v = propVals[i]; + const v = propVals[i]; if (!left) { if (v === LEAVE) { left = true; } - } else { - if (v === true) { - // tag will be closed and re-opened - propVals[i] = STAY; - } + } else if (v === true) { + // tag will be closed and re-opened + propVals[i] = STAY; } } @@ -131,11 +127,11 @@ function getTXTFromAtext(pad, atext, authorColors) for (var i = propVals.length - 1; i >= 0; i--) { if (propVals[i] === LEAVE) { - //emitCloseTag(i); + // emitCloseTag(i); tags2close.push(i); propVals[i] = false; } else if (propVals[i] === STAY) { - //emitCloseTag(i); + // emitCloseTag(i); tags2close.push(i); } } @@ -148,13 +144,13 @@ function getTXTFromAtext(pad, atext, authorColors) // propVals is now all {true,false} again } // end if (propChanged) - var chars = o.chars; + let chars = o.chars; if (o.lines) { // exclude newline at end of line, if present chars--; } - var s = taker.take(chars); + const s = taker.take(chars); // removes the characters with the code 12. Don't know where they come // from but they break the abiword parser and are completly useless @@ -174,14 +170,13 @@ function getTXTFromAtext(pad, atext, authorColors) propVals[i] = false; } } - } // end processNextChars processNextChars(text.length - idx); - return(assem.toString()); + return (assem.toString()); } // end getLineHTML - var pieces = [css]; + const pieces = [css]; // Need to deal with constraints imposed on HTML lists; can // only gain one level of nesting at once, can't change type @@ -191,34 +186,33 @@ function getTXTFromAtext(pad, atext, authorColors) // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - var listNumbers = {}; - var prevListLevel; + const listNumbers = {}; + let prevListLevel; - for (var i = 0; i < textLines.length; i++) { + for (let i = 0; i < textLines.length; i++) { + const line = _analyzeLine(textLines[i], attribLines[i], apool); + let lineContent = getLineTXT(line.text, line.aline); - var line = _analyzeLine(textLines[i], attribLines[i], apool); - var lineContent = getLineTXT(line.text, line.aline); - - if (line.listTypeName == "bullet") { - lineContent = "* " + lineContent; // add a bullet + if (line.listTypeName == 'bullet') { + lineContent = `* ${lineContent}`; // add a bullet } - if (line.listTypeName !== "number") { + if (line.listTypeName !== 'number') { // We're no longer in an OL so we can reset counting - for (var key in listNumbers) { + for (const key in listNumbers) { delete listNumbers[key]; } } if (line.listLevel > 0) { - for (var j = line.listLevel - 1; j >= 0; j--) { + for (let j = line.listLevel - 1; j >= 0; j--) { pieces.push('\t'); // tab indent list numbers.. - if(!listNumbers[line.listLevel]){ + if (!listNumbers[line.listLevel]) { listNumbers[line.listLevel] = 0; } } - if (line.listTypeName == "number") { + if (line.listTypeName == 'number') { /* * listLevel == amount of indentation * listNumber(s) == item number @@ -232,19 +226,19 @@ function getTXTFromAtext(pad, atext, authorColors) * To handle going back to 2.1 when prevListLevel is lower number * than current line.listLevel then reset the object value */ - if(line.listLevel < prevListLevel){ + if (line.listLevel < prevListLevel) { delete listNumbers[prevListLevel]; } listNumbers[line.listLevel]++; - if(line.listLevel > 1){ - var x = 1; - while(x <= line.listLevel-1){ - pieces.push(listNumbers[x]+".") + if (line.listLevel > 1) { + let x = 1; + while (x <= line.listLevel - 1) { + pieces.push(`${listNumbers[x]}.`); x++; } } - pieces.push(listNumbers[line.listLevel]+". ") + pieces.push(`${listNumbers[line.listLevel]}. `); prevListLevel = line.listLevel; } @@ -259,8 +253,7 @@ function getTXTFromAtext(pad, atext, authorColors) exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async function(padId, revNum) -{ - let pad = await padManager.getPad(padId); +exports.getPadTXTDocument = async function (padId, revNum) { + const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); -} +}; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index a5b1074e6ee..0c0dbcc7a7c 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -14,14 +14,14 @@ * limitations under the License. */ -var log4js = require('log4js'); -const db = require("../db/DB"); +const log4js = require('log4js'); +const db = require('../db/DB'); +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -exports.setPadRaw = function(padId, records) -{ +exports.setPadRaw = function (padId, records) { records = JSON.parse(records); - Object.keys(records).forEach(async function(key) { + Object.keys(records).forEach(async (key) => { let value = records[key]; if (!value) { @@ -36,7 +36,7 @@ exports.setPadRaw = function(padId, records) newKey = key; // Does this author already exist? - let author = await db.get(key); + const author = await db.get(key); if (author) { // Yes, add the padID to the author @@ -47,24 +47,31 @@ exports.setPadRaw = function(padId, records) value = author; } else { // No, create a new array with the author info in - value.padIDs = [ padId ]; + value.padIDs = [padId]; } } else { // Not author data, probably pad data // we can split it to look to see if it's pad data - let oldPadId = key.split(":"); + const oldPadId = key.split(':'); // we know it's pad data - if (oldPadId[0] === "pad") { + if (oldPadId[0] === 'pad') { // so set the new pad id for the author oldPadId[1] = padId; // and create the value - newKey = oldPadId.join(":"); // create the new key + newKey = oldPadId.join(':'); // create the new key + } + + // is this a key that is supported through a plugin? + // get content that has a different prefix IE comments:padId:foo + // a plugin would return something likle ['comments', 'cakes'] + for (const prefix of await hooks.aCallAll('exportEtherpadAdditionalContent')) { + if (prefix === oldPadId[0]) newKey = `${prefix}:${padId}`; } } // Write the value to the server await db.set(newKey, value); }); -} +}; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 28ce4f68958..831b3f53ee7 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -14,76 +14,69 @@ * limitations under the License. */ -const log4js = require('log4js'); -const Changeset = require("ep_etherpad-lite/static/js/Changeset"); -const contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); -const cheerio = require("cheerio"); -const rehype = require("rehype") -const format = require("rehype-format") +const log4js = require('log4js'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const contentcollector = require('ep_etherpad-lite/static/js/contentcollector'); +const cheerio = require('cheerio'); +const rehype = require('rehype'); +const minifyWhitespace = require('rehype-minify-whitespace'); - -exports.setPadHTML = function(pad, html) -{ - var apiLogger = log4js.getLogger("ImportHtml"); - - var opts = { - indentInitial: false, - indent: -1 - } +exports.setPadHTML = async (pad, html) => { + const apiLogger = log4js.getLogger('ImportHtml'); rehype() - .use(format, opts) - .process(html, function(err, output){ - html = String(output).replace(/(\r\n|\n|\r)/gm,""); - }) + .use(minifyWhitespace, {newlines: false}) + .process(html, (err, output) => { + html = String(output); + }); - var $ = cheerio.load(html); + const $ = cheerio.load(html); // Appends a line break, used by Etherpad to ensure a caret is available // below the last line of an import - $('body').append("

        "); + $('body').append('

        '); - var doc = $('html')[0]; + const doc = $('body')[0]; apiLogger.debug('html:'); apiLogger.debug(html); // Convert a dom tree into a list of lines and attribute liens // using the content collector object - var cc = contentcollector.makeContentCollector(true, null, pad.pool); + 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); + } catch (e) { + apiLogger.warn('HTML was not properly formed', e); // don't process the HTML because it was bad throw e; } - var result = cc.finish(); + const result = cc.finish(); apiLogger.debug('Lines:'); - var i; + let i; for (i = 0; i < result.lines.length; i++) { - apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); - apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); + apiLogger.debug(`Line ${i + 1} text: ${result.lines[i]}`); + apiLogger.debug(`Line ${i + 1} attributes: ${result.lineAttribs[i]}`); } // Get the new plain text and its attributes - var newText = result.lines.join('\n'); + const newText = result.lines.join('\n'); apiLogger.debug('newText:'); apiLogger.debug(newText); - var newAttribs = result.lineAttribs.join('|1+1') + '|1+1'; + const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; - function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) { - var attribsIter = Changeset.opIterator(attribs); - var textIndex = 0; - var newTextStart = 0; - var newTextEnd = newText.length; + function eachAttribRun(attribs, func /* (startInNewText, endInNewText, attribs)*/) { + const attribsIter = Changeset.opIterator(attribs); + let textIndex = 0; + const newTextStart = 0; + const newTextEnd = newText.length; while (attribsIter.hasNext()) { - var op = attribsIter.next(); - var nextIndex = textIndex + op.chars; + 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); } @@ -92,17 +85,19 @@ exports.setPadHTML = function(pad, html) } // create a new changeset with a helper builder object - var builder = Changeset.builder(1); + const builder = Changeset.builder(1); // assemble each line into the builder - eachAttribRun(newAttribs, function(start, end, attribs) { + eachAttribRun(newAttribs, (start, end, attribs) => { builder.insert(newText.substring(start, end), attribs); }); // the changeset is ready! - var theChangeset = builder.toString(); - - apiLogger.debug('The changeset: ' + theChangeset); - pad.setText("\n"); - pad.appendRevision(theChangeset); -} + const theChangeset = builder.toString(); + + apiLogger.debug(`The changeset: ${theChangeset}`); + await Promise.all([ + pad.setText('\n'), + pad.appendRevision(theChangeset), + ]); +}; diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js index dfbee8fa5f5..74496cabd77 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.js @@ -16,18 +16,18 @@ * limitations under the License. */ -var async = require("async"); -var fs = require("fs"); -var log4js = require('log4js'); -var os = require("os"); -var path = require("path"); -var settings = require("./Settings"); -var spawn = require("child_process").spawn; +const async = require('async'); +const fs = require('fs'); +const log4js = require('log4js'); +const os = require('os'); +const path = require('path'); +const settings = require('./Settings'); +const spawn = require('child_process').spawn; // Conversion tasks will be queued up, so we don't overload the system -var queue = async.queue(doConvertTask, 1); +const queue = async.queue(doConvertTask, 1); -var libreOfficeLogger = log4js.getLogger('LibreOffice'); +const libreOfficeLogger = log4js.getLogger('LibreOffice'); /** * Convert a file from one type to another @@ -37,18 +37,18 @@ var libreOfficeLogger = log4js.getLogger('LibreOffice'); * @param {String} type The type to convert into * @param {Function} callback Standard callback function */ -exports.convertFile = function(srcFile, destFile, type, callback) { +exports.convertFile = function (srcFile, destFile, type, callback) { // Used for the moving of the file, not the conversion - var fileExtension = type; + const fileExtension = type; - if (type === "html") { + if (type === 'html') { // "html:XHTML Writer File:UTF8" does a better job than normal html exports - if (path.extname(srcFile).toLowerCase() === ".doc") { - type = "html"; + if (path.extname(srcFile).toLowerCase() === '.doc') { + type = 'html'; } // PDF files need to be converted with LO Draw ref https://github.com/ether/etherpad-lite/issues/4151 - if (path.extname(srcFile).toLowerCase() === ".pdf") { - type = "html:XHTML Draw File" + if (path.extname(srcFile).toLowerCase() === '.pdf') { + type = 'html:XHTML Draw File'; } } @@ -57,52 +57,61 @@ exports.convertFile = function(srcFile, destFile, type, callback) { // to avoid `Error: no export filter for /tmp/xxxx.doc` error if (type === 'doc') { queue.push({ - "srcFile": srcFile, - "destFile": destFile.replace(/\.doc$/, '.odt'), - "type": 'odt', - "callback": function () { - queue.push({"srcFile": srcFile.replace(/\.html$/, '.odt'), "destFile": destFile, "type": type, "callback": callback, "fileExtension": fileExtension }); - } + srcFile, + destFile: destFile.replace(/\.doc$/, '.odt'), + type: 'odt', + callback() { + queue.push({srcFile: srcFile.replace(/\.html$/, '.odt'), destFile, type, callback, fileExtension}); + }, }); } else { - queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback, "fileExtension": fileExtension}); + queue.push({srcFile, destFile, type, callback, fileExtension}); } }; function doConvertTask(task, callback) { - var tmpDir = os.tmpdir(); + const tmpDir = os.tmpdir(); async.series([ /* * use LibreOffice to convert task.srcFile to another format, given in * task.type */ - function(callback) { + function (callback) { libreOfficeLogger.debug(`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); - var soffice = spawn(settings.soffice, [ + const soffice = spawn(settings.soffice, [ '--headless', '--invisible', '--nologo', '--nolockcheck', '--writer', - '--convert-to', task.type, + '--convert-to', + task.type, task.srcFile, - '--outdir', tmpDir + '--outdir', + tmpDir, ]); + // Soffice/libreoffice is buggy and often hangs. + // To remedy this we kill the spawned process after a while. + const hangTimeout = setTimeout(() => { + soffice.stdin.pause(); // required to kill hanging threads + soffice.kill(); + }, 120000); - var stdoutBuffer = ''; + let stdoutBuffer = ''; // Delegate the processing of stdout to another function - soffice.stdout.on('data', function(data) { + soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); // Append error messages to the buffer - soffice.stderr.on('data', function(data) { + soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - soffice.on('exit', function(code) { + soffice.on('exit', (code) => { + clearTimeout(hangTimeout); if (code != 0) { // Throw an exception if libreoffice failed return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`); @@ -110,18 +119,18 @@ function doConvertTask(task, callback) { // if LibreOffice exited succesfully, go on with processing callback(); - }) + }); }, // Move the converted file to the correct place - function(callback) { - var filename = path.basename(task.srcFile); - var sourceFilename = filename.substr(0, filename.lastIndexOf('.')) + '.' + task.fileExtension; - var sourcePath = path.join(tmpDir, sourceFilename); + function (callback) { + const filename = path.basename(task.srcFile); + const sourceFilename = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; + const sourcePath = path.join(tmpDir, sourceFilename); libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); fs.rename(sourcePath, task.destFile, callback); - } - ], function(err) { + }, + ], (err) => { // Invoke the callback for the local queue callback(); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index a4194eb9eb0..66a1926a9fa 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This Module manages all /minified/* requests. It controls the * minification && compression of Javascript and CSS. @@ -19,142 +21,132 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); -var settings = require('./Settings'); -var async = require('async'); -var fs = require('fs'); -var StringDecoder = require('string_decoder').StringDecoder; -var CleanCSS = require('clean-css'); -var path = require('path'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs"); -var RequireKernel = require('etherpad-require-kernel'); -var urlutil = require('url'); -var mime = require('mime-types') -var Threads = require('threads') -var log4js = require('log4js'); - -var logger = log4js.getLogger("Minify"); - -var ROOT_DIR = path.normalize(__dirname + "/../../static/"); -var TAR_PATH = path.join(__dirname, 'tar.json'); -var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); - -var threadsPool = Threads.Pool(function () { - return Threads.spawn(new Threads.Worker("./MinifyWorker")) -}, 2) - -var LIBRARY_WHITELIST = [ - 'async' - , 'security' - , 'tinycon' - , 'underscore' - , 'unorm' - ]; +const ERR = require('async-stacktrace'); +const settings = require('./Settings'); +const async = require('async'); +const fs = require('fs'); +const path = require('path'); +const plugins = require('../../static/js/pluginfw/plugin_defs'); +const RequireKernel = require('etherpad-require-kernel'); +const urlutil = require('url'); +const mime = require('mime-types'); +const Threads = require('threads'); +const log4js = require('log4js'); + +const logger = log4js.getLogger('Minify'); + +const ROOT_DIR = path.normalize(`${__dirname}/../../static/`); +const TAR_PATH = path.join(__dirname, 'tar.json'); +const tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8')); + +const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); + +const LIBRARY_WHITELIST = [ + 'async', + 'js-cookie', + 'security', + 'tinycon', + 'underscore', + 'unorm', +]; // Rewrite tar to include modules with no extensions and proper rooted paths. -var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; +const LIBRARY_PREFIX = 'ep_etherpad-lite/static/js'; exports.tar = {}; -function prefixLocalLibraryPath(path) { - if (path.charAt(0) == '$') { +const prefixLocalLibraryPath = (path) => { + if (path.charAt(0) === '$') { return path.slice(1); } else { - return LIBRARY_PREFIX + '/' + path; + return `${LIBRARY_PREFIX}/${path}`; } -} - -for (var key in tar) { - exports.tar[prefixLocalLibraryPath(key)] = - tar[key].map(prefixLocalLibraryPath).concat( - tar[key].map(prefixLocalLibraryPath).map(function (p) { - return p.replace(/\.js$/, ''); - }) - ).concat( - tar[key].map(prefixLocalLibraryPath).map(function (p) { - return p.replace(/\.js$/, '') + '/index.js'; - }) - ); +}; +for (const [key, relativeFiles] of Object.entries(tar)) { + const files = relativeFiles.map(prefixLocalLibraryPath); + exports.tar[prefixLocalLibraryPath(key)] = files + .concat(files.map((p) => p.replace(/\.js$/, ''))) + .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); } // What follows is a terrible hack to avoid loop-back within the server. // TODO: Serve files from another service, or directly from the file system. -function requestURI(url, method, headers, callback) { - var parsedURL = urlutil.parse(url); - - var status = 500, headers = {}, content = []; - - var mockRequest = { - url: url - , method: method - , params: {filename: parsedURL.path.replace(/^\/static\//, '')} - , headers: headers +const requestURI = (url, method, headers, callback) => { + const parsedURL = urlutil.parse(url); + + let status = 500; + var headers = {}; + const content = []; + + const mockRequest = { + url, + method, + params: {filename: parsedURL.path.replace(/^\/static\//, '')}, + headers, }; - var mockResponse = { - writeHead: function (_status, _headers) { + const mockResponse = { + writeHead: (_status, _headers) => { status = _status; - for (var header in _headers) { + for (const header in _headers) { if (Object.prototype.hasOwnProperty.call(_headers, header)) { headers[header] = _headers[header]; } } - } - , setHeader: function (header, value) { + }, + setHeader: (header, value) => { headers[header.toLowerCase()] = value.toString(); - } - , header: function (header, value) { + }, + header: (header, value) => { headers[header.toLowerCase()] = value.toString(); - } - , write: function (_content) { - _content && content.push(_content); - } - , end: function (_content) { + }, + write: (_content) => { + _content && content.push(_content); + }, + end: (_content) => { _content && content.push(_content); callback(status, headers, content.join('')); - } + }, }; minify(mockRequest, mockResponse); -} -function requestURIs(locations, method, headers, callback) { - var pendingRequests = locations.length; - var responses = []; - - function respondFor(i) { - return function (status, headers, content) { - responses[i] = [status, headers, content]; - if (--pendingRequests == 0) { - completed(); - } - }; - } +}; - for (var i = 0, ii = locations.length; i < ii; i++) { - requestURI(locations[i], method, headers, respondFor(i)); - } +const requestURIs = (locations, method, headers, callback) => { + let pendingRequests = locations.length; + const responses = []; - function completed() { - var statuss = responses.map(function (x) {return x[0];}); - var headerss = responses.map(function (x) {return x[1];}); - var contentss = responses.map(function (x) {return x[2];}); + const completed = () => { + const statuss = responses.map((x) => x[0]); + const headerss = responses.map((x) => x[1]); + const contentss = responses.map((x) => x[2]); callback(statuss, headerss, contentss); + }; + + const respondFor = (i) => (status, headers, content) => { + responses[i] = [status, headers, content]; + if (--pendingRequests === 0) { + completed(); + } + }; + + for (let i = 0, ii = locations.length; i < ii; i++) { + requestURI(locations[i], method, headers, respondFor(i)); } -} +}; /** * creates the minifed javascript for the given minified name * @param req the Express request * @param res the Express response */ -function minify(req, res) -{ - var filename = req.params['filename']; +const minify = (req, res) => { + let filename = req.params.filename; // No relative paths, especially if they may go up the file hierarchy. filename = path.normalize(path.join(ROOT_DIR, filename)); - filename = filename.replace(/\.\./g, '') + filename = filename.replace(/\.\./g, ''); - if (filename.indexOf(ROOT_DIR) == 0) { + if (filename.indexOf(ROOT_DIR) === 0) { filename = filename.slice(ROOT_DIR.length); - filename = filename.replace(/\\/g, '/') + filename = filename.replace(/\\/g, '/'); } else { res.writeHead(404, {}); res.end(); @@ -166,36 +158,36 @@ function minify(req, res) are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js, commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js */ - var match = filename.match(/^plugins\/([^\/]+)(\/(?:(static\/.*)|.*))?$/); + const match = filename.match(/^plugins\/([^/]+)(\/(?:(static\/.*)|.*))?$/); if (match) { - var library = match[1]; - var libraryPath = match[2] || ''; + const library = match[1]; + const libraryPath = match[2] || ''; if (plugins.plugins[library] && match[3]) { - var plugin = plugins.plugins[library]; - var pluginPath = plugin.package.realPath; + const plugin = plugins.plugins[library]; + const pluginPath = plugin.package.realPath; filename = path.relative(ROOT_DIR, pluginPath + libraryPath); filename = filename.replace(/\\/g, '/'); // windows path fix - } else if (LIBRARY_WHITELIST.indexOf(library) != -1) { + } else if (LIBRARY_WHITELIST.indexOf(library) !== -1) { // Go straight into node_modules // Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js' // would end up resolving to logically distinct resources. - filename = '../node_modules/' + library + libraryPath; + filename = `../node_modules/${library}${libraryPath}`; } } - var contentType = mime.lookup(filename); + const contentType = mime.lookup(filename); - statFile(filename, function (error, date, exists) { + statFile(filename, (error, date, exists) => { if (date) { date = new Date(date); date.setMilliseconds(0); res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); if (settings.maxAge !== undefined) { - var expiresDate = new Date(Date.now()+settings.maxAge*1000); + const expiresDate = new Date(Date.now() + settings.maxAge * 1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', 'max-age=' + settings.maxAge); + res.setHeader('cache-control', `max-age=${settings.maxAge}`); } } @@ -208,74 +200,76 @@ function minify(req, res) } else if (new Date(req.headers['if-modified-since']) >= date) { res.writeHead(304, {}); res.end(); - } else { - if (req.method == 'HEAD') { - res.header("Content-Type", contentType); - res.writeHead(200, {}); - res.end(); - } else if (req.method == 'GET') { - getFileCompressed(filename, contentType, function (error, content) { - if(ERR(error, function(){ - res.writeHead(500, {}); - res.end(); - })) return; - res.header("Content-Type", contentType); - res.writeHead(200, {}); - res.write(content); + } else if (req.method === 'HEAD') { + res.header('Content-Type', contentType); + res.writeHead(200, {}); + res.end(); + } else if (req.method === 'GET') { + getFileCompressed(filename, contentType, (error, content) => { + if (ERR(error, () => { + res.writeHead(500, {}); res.end(); - }); - } else { - res.writeHead(405, {'allow': 'HEAD, GET'}); + })) return; + res.header('Content-Type', contentType); + res.writeHead(200, {}); + res.write(content); res.end(); - } + }); + } else { + res.writeHead(405, {allow: 'HEAD, GET'}); + res.end(); } }, 3); -} +}; // find all includes in ace.js and embed them. -function getAceFile(callback) { - fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) { - if(ERR(err, callback)) return; +const getAceFile = (callback) => { + fs.readFile(`${ROOT_DIR}js/ace.js`, 'utf8', (err, data) => { + if (ERR(err, callback)) return; // Find all includes in ace.js and embed them - var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi); - if (!settings.minify) { - founds = []; + const filenames = []; + if (settings.minify) { + const regex = /\$\$INCLUDE_[a-zA-Z_]+\((['"])([^'"]*)\1\)/gi; + // This logic can be simplified via String.prototype.matchAll() once support for Node.js + // v11.x and older is dropped. + let matches; + while ((matches = regex.exec(data)) != null) { + filenames.push(matches[2]); + } } // Always include the require kernel. - founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")'); + filenames.push('../static/js/require-kernel.js'); data += ';\n'; data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n'; // Request the contents of the included file on the server-side and write // them into the file. - async.forEach(founds, function (item, callback) { - var filename = item.match(/"([^"]*)"/)[1]; - + async.forEach(filenames, (filename, callback) => { // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. - var baseURI = 'http://invalid.invalid'; - var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); + const baseURI = 'http://invalid.invalid'; + let resourceURI = baseURI + path.normalize(path.join('/static/', filename)); resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) - requestURI(resourceURI, 'GET', {}, function (status, headers, body) { - var error = !(status == 200 || status == 404); + requestURI(resourceURI, 'GET', {}, (status, headers, body) => { + const error = !(status === 200 || status === 404); if (!error) { - data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = ' - + JSON.stringify(status == 200 ? body || '' : null) + ';\n'; + data += `Ace2Editor.EMBEDED[${JSON.stringify(filename)}] = ${ + JSON.stringify(status === 200 ? body || '' : null)};\n`; } else { console.error(`getAceFile(): error getting ${resourceURI}. Status code: ${status}`); } callback(); }); - }, function(error) { + }, (error) => { callback(error, data); }); }); -} +}; // Check for the existance of the file and get the last modification date. -function statFile(filename, callback, dirStatLimit) { +const statFile = (filename, callback, dirStatLimit) => { /* * The only external call to this function provides an explicit value for * dirStatLimit: this check could be removed. @@ -284,24 +278,24 @@ function statFile(filename, callback, dirStatLimit) { dirStatLimit = 3; } - if (dirStatLimit < 1 || filename == '' || filename == '/') { + if (dirStatLimit < 1 || filename === '' || filename === '/') { callback(null, null, false); - } else if (filename == 'js/ace.js') { + } else if (filename === 'js/ace.js') { // Sometimes static assets are inlined into this file, so we have to stat // everything. - lastModifiedDateOfEverything(function (error, date) { + lastModifiedDateOfEverything((error, date) => { callback(error, date, !error); }); - } else if (filename == 'js/require-kernel.js') { + } else if (filename === 'js/require-kernel.js') { callback(null, requireLastModified(), true); } else { - fs.stat(ROOT_DIR + filename, function (error, stats) { + fs.stat(ROOT_DIR + filename, (error, stats) => { if (error) { - if (error.code == "ENOENT") { + if (error.code === 'ENOENT') { // Stat the directory instead. - statFile(path.dirname(filename), function (error, date, exists) { + statFile(path.dirname(filename), (error, date, exists) => { callback(error, date, false); - }, dirStatLimit-1); + }, dirStatLimit - 1); } else { callback(error); } @@ -312,35 +306,31 @@ function statFile(filename, callback, dirStatLimit) { } }); } -} -function lastModifiedDateOfEverything(callback) { - var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/']; - var latestModification = 0; - //go trough this two folders - async.forEach(folders2check, function(path, callback) - { - //read the files in the folder - fs.readdir(path, function(err, files) - { - if(ERR(err, callback)) return; - - //we wanna check the directory itself for changes too - files.push("."); - - //go trough all files in this folder - async.forEach(files, function(filename, callback) - { - //get the stat data of this file - fs.stat(path + "/" + filename, function(err, stats) - { - if(ERR(err, callback)) return; - - //get the modification time - var modificationTime = stats.mtime.getTime(); - - //compare the modification time to the highest found - if(modificationTime > latestModification) - { +}; + +const lastModifiedDateOfEverything = (callback) => { + const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; + let latestModification = 0; + // go trough this two folders + async.forEach(folders2check, (path, callback) => { + // read the files in the folder + fs.readdir(path, (err, files) => { + if (ERR(err, callback)) return; + + // we wanna check the directory itself for changes too + files.push('.'); + + // go trough all files in this folder + async.forEach(files, (filename, callback) => { + // get the stat data of this file + fs.stat(`${path}/${filename}`, (err, stats) => { + if (ERR(err, callback)) return; + + // get the modification time + const modificationTime = stats.mtime.getTime(); + + // compare the modification time to the highest found + if (modificationTime > latestModification) { latestModification = modificationTime; } @@ -348,29 +338,25 @@ function lastModifiedDateOfEverything(callback) { }); }, callback); }); - }, function () { + }, () => { callback(null, latestModification); }); -} +}; // This should be provided by the module, but until then, just use startup // time. -var _requireLastModified = new Date(); -function requireLastModified() { - return _requireLastModified.toUTCString(); -} -function requireDefinition() { - return 'var require = ' + RequireKernel.kernelSource + ';\n'; -} +const _requireLastModified = new Date(); +const requireLastModified = () => _requireLastModified.toUTCString(); +const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`; -function getFileCompressed(filename, contentType, callback) { - getFile(filename, function (error, content) { +const getFileCompressed = (filename, contentType, callback) => { + getFile(filename, (error, content) => { if (error || !content || !settings.minify) { callback(error, content); - } else if (contentType == 'text/javascript') { - threadsPool.queue(async ({ compressJS }) => { + } else if (contentType === 'application/javascript') { + threadsPool.queue(async ({compressJS}) => { try { - logger.info('Compress JS file %s.', filename) + logger.info('Compress JS file %s.', filename); content = content.toString(); const compressResult = await compressJS(content); @@ -381,15 +367,16 @@ function getFileCompressed(filename, contentType, callback) { content = compressResult.code.toString(); // Convert content obj code to string } } catch (error) { - console.error(`getFile() returned an error in getFileCompressed(${filename}, ${contentType}): ${error}`); + console.error('getFile() returned an error in ' + + `getFileCompressed(${filename}, ${contentType}): ${error}`); } callback(null, content); - }) - } else if (contentType == 'text/css') { - threadsPool.queue(async ({ compressCSS }) => { + }); + } else if (contentType === 'text/css') { + threadsPool.queue(async ({compressCSS}) => { try { - logger.info('Compress CSS file %s.', filename) + logger.info('Compress CSS file %s.', filename); content = await compressCSS(filename, ROOT_DIR); } catch (error) { @@ -397,24 +384,28 @@ function getFileCompressed(filename, contentType, callback) { } callback(null, content); - }) + }); } else { callback(null, content); } }); -} +}; -function getFile(filename, callback) { - if (filename == 'js/ace.js') { +const getFile = (filename, callback) => { + if (filename === 'js/ace.js') { getAceFile(callback); - } else if (filename == 'js/require-kernel.js') { + } else if (filename === 'js/require-kernel.js') { callback(undefined, requireDefinition()); } else { fs.readFile(ROOT_DIR + filename, callback); } -} +}; exports.minify = minify; exports.requestURI = requestURI; exports.requestURIs = requestURIs; + +exports.shutdown = async (hookName, context) => { + await threadsPool.terminate(); +}; diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 8ac6d3a7fd7..c8bc09eb15c 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -2,18 +2,16 @@ * Worker thread to minify JS & CSS files out of the main NodeJS thread */ -var CleanCSS = require('clean-css'); -var Terser = require("terser"); -var path = require('path'); -var Threads = require('threads') +const CleanCSS = require('clean-css'); +const Terser = require('terser'); +const path = require('path'); +const Threads = require('threads'); -function compressJS(content) -{ +function compressJS(content) { return Terser.minify(content); } -function compressCSS(filename, ROOT_DIR) -{ +function compressCSS(filename, ROOT_DIR) { return new Promise((res, rej) => { try { const absPath = path.join(ROOT_DIR, filename); @@ -48,20 +46,20 @@ function compressCSS(filename, ROOT_DIR) new CleanCSS({ rebase: true, rebaseTo: basePath, - }).minify([absPath], function (errors, minified) { - if (errors) return rej(errors) + }).minify([absPath], (errors, minified) => { + if (errors) return rej(errors); - return res(minified.styles) + return res(minified.styles); }); } catch (error) { // on error, just yield the un-minified original, but write a log message console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`); callback(null, content); } - }) + }); } Threads.expose({ compressJS, - compressCSS -}) + compressCSS, +}); diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js index 1ebbcbca0d3..f237e663719 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.js @@ -25,17 +25,17 @@ const semver = require('semver'); * * @param {String} minNodeVersion Minimum required Node version */ -exports.enforceMinNodeVersion = function(minNodeVersion) { +exports.enforceMinNodeVersion = function (minNodeVersion) { const currentNodeVersion = process.version; // we cannot use template literals, since we still do not know if we are // running under Node >= 4.0 if (semver.lt(currentNodeVersion, minNodeVersion)) { - console.error('Running Etherpad on Node ' + currentNodeVersion + ' is not supported. Please upgrade at least to Node ' + minNodeVersion); + console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. Please upgrade at least to Node ${minNodeVersion}`); process.exit(1); } - console.debug('Running on Node ' + currentNodeVersion + ' (minimum required Node version: ' + minNodeVersion + ')'); + console.debug(`Running on Node ${currentNodeVersion} (minimum required Node version: ${minNodeVersion})`); }; /** @@ -44,7 +44,7 @@ exports.enforceMinNodeVersion = function(minNodeVersion) { * @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are deprecated * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases */ -exports.checkDeprecationStatus = function(lowestNonDeprecatedNodeVersion, epRemovalVersion) { +exports.checkDeprecationStatus = function (lowestNonDeprecatedNodeVersion, epRemovalVersion) { const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { diff --git a/src/node/utils/RemoteAddress.js b/src/node/utils/RemoteAddress.js deleted file mode 100644 index 86a4a5b261d..00000000000 --- a/src/node/utils/RemoteAddress.js +++ /dev/null @@ -1 +0,0 @@ -exports.remoteAddress = {}; diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 6a03668c6f5..c4245b77c19 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -26,17 +26,17 @@ * limitations under the License. */ -var absolutePaths = require('./AbsolutePaths'); -var fs = require("fs"); -var os = require("os"); -var path = require('path'); -var argv = require('./Cli').argv; -var npm = require("npm/lib/npm.js"); -var jsonminify = require("jsonminify"); -var log4js = require("log4js"); -var randomString = require("./randomstring"); -var suppressDisableMsg = " -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n"; -var _ = require("underscore"); +const absolutePaths = require('./AbsolutePaths'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const argv = require('./Cli').argv; +const npm = require('npm/lib/npm.js'); +const jsonminify = require('jsonminify'); +const log4js = require('log4js'); +const randomString = require('./randomstring'); +const suppressDisableMsg = ' -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n'; +const _ = require('underscore'); /* Root path of the installation */ exports.root = absolutePaths.findEtherpadRoot(); @@ -59,14 +59,14 @@ console.log(`Random string used for versioning assets: ${exports.randomVersionSt /** * The app title, visible e.g. in the browser window */ -exports.title = "Etherpad"; +exports.title = 'Etherpad'; /** * The app favicon fully specified url, visible e.g. in the browser window */ -exports.favicon = "favicon.ico"; -exports.faviconPad = "../" + exports.favicon; -exports.faviconTimeslider = "../../" + exports.favicon; +exports.favicon = 'favicon.ico'; +exports.faviconPad = `../${exports.favicon}`; +exports.faviconTimeslider = `../../${exports.favicon}`; /* * Skin name. @@ -76,12 +76,12 @@ exports.faviconTimeslider = "../../" + exports.favicon; */ exports.skinName = null; -exports.skinVariants = "super-light-toolbar super-light-editor light-background"; +exports.skinVariants = 'super-light-toolbar super-light-editor light-background'; /** * The IP ep-lite should listen to */ -exports.ip = "0.0.0.0"; +exports.ip = '0.0.0.0'; /** * The Port ep-lite should listen to @@ -107,60 +107,60 @@ exports.socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile']; /* * The Type of the database */ -exports.dbType = "dirty"; +exports.dbType = 'dirty'; /** * This setting is passed with dbType to ueberDB to set up the database */ -exports.dbSettings = { "filename" : path.join(exports.root, "var/dirty.db") }; +exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; /** * The default Text of a new pad */ -exports.defaultPadText = "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad on Github: https:\/\/github.com\/ether\/etherpad-lite\n"; +exports.defaultPadText = 'Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad on Github: https:\/\/github.com\/ether\/etherpad-lite\n'; /** * The default Pad Settings for a user (Can be overridden by changing the setting */ exports.padOptions = { - "noColors": false, - "showControls": true, - "showChat": true, - "showLineNumbers": true, - "useMonospaceFont": false, - "userName": false, - "userColor": false, - "rtl": false, - "alwaysShowChat": false, - "chatAndUsers": false, - "lang": "en-gb" + noColors: false, + showControls: true, + showChat: true, + showLineNumbers: true, + useMonospaceFont: false, + userName: false, + userColor: false, + rtl: false, + alwaysShowChat: false, + chatAndUsers: false, + lang: 'en-gb', }, /** * Whether certain shortcut keys are enabled for a user in the pad */ exports.padShortcutEnabled = { - "altF9" : true, - "altC" : true, - "delete" : true, - "cmdShift2" : true, - "return" : true, - "esc" : true, - "cmdS" : true, - "tab" : true, - "cmdZ" : true, - "cmdY" : true, - "cmdB" : true, - "cmdI" : true, - "cmdU" : true, - "cmd5" : true, - "cmdShiftL" : true, - "cmdShiftN" : true, - "cmdShift1" : true, - "cmdShiftC" : true, - "cmdH" : true, - "ctrlHome" : true, - "pageUp" : true, - "pageDown" : true, + altF9: true, + altC: true, + delete: true, + cmdShift2: true, + return: true, + esc: true, + cmdS: true, + tab: true, + cmdZ: true, + cmdY: true, + cmdB: true, + cmdI: true, + cmdU: true, + cmd5: true, + cmdShiftL: true, + cmdShiftN: true, + cmdShift1: true, + cmdShiftC: true, + cmdH: true, + ctrlHome: true, + pageUp: true, + pageDown: true, }, /** @@ -168,20 +168,20 @@ exports.padShortcutEnabled = { */ exports.toolbar = { left: [ - ["bold", "italic", "underline", "strikethrough"], - ["orderedlist", "unorderedlist", "indent", "outdent"], - ["undo", "redo"], - ["clearauthorship"] + ['bold', 'italic', 'underline', 'strikethrough'], + ['orderedlist', 'unorderedlist', 'indent', 'outdent'], + ['undo', 'redo'], + ['clearauthorship'], ], right: [ - ["importexport", "timeslider", "savedrevision"], - ["settings", "embed"], - ["showusers"] + ['importexport', 'timeslider', 'savedrevision'], + ['settings', 'embed'], + ['showusers'], ], timeslider: [ - ["timeslider_export", "timeslider_settings", "timeslider_returnToPad"] - ] -} + ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], + ], +}; /** * A flag that requires any user to have a valid session (via the api) before accessing a pad @@ -193,15 +193,10 @@ exports.requireSession = false; */ exports.editOnly = false; -/** - * A flag that bypasses password prompts for users with valid sessions - */ -exports.sessionNoPassword = false; - /** * Max age that responses will have (affects caching layer). */ -exports.maxAge = 1000*60*60*6; // 6 hours +exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours /** * A flag that shows if minification is enabled or not @@ -231,7 +226,7 @@ exports.allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = "INFO"; +exports.loglevel = 'INFO'; /** * Disable IP logging @@ -256,7 +251,7 @@ exports.indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = { appenders: [{ type: "console" }]}; +exports.logconfig = {appenders: [{type: 'console'}]}; /* * Session Key, do not sure this. @@ -268,6 +263,24 @@ exports.sessionKey = false; */ exports.trustProxy = false; +/* + * Settings controlling the session cookie issued by Etherpad. + */ +exports.cookie = { + /* + * Value of the SameSite cookie property. "Lax" is recommended unless + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + sameSite: 'Lax', +}; + /* * This setting is used if you need authentication and/or * authorization. Note: /admin always requires authentication, and @@ -290,28 +303,28 @@ exports.scrollWhenFocusLineIsOutOfViewport = { /* * Percentage of viewport height to be additionally scrolled. */ - "percentage": { - "editionAboveViewport": 0, - "editionBelowViewport": 0 + percentage: { + editionAboveViewport: 0, + editionBelowViewport: 0, }, /* * Time (in milliseconds) used to animate the scroll transition. Set to 0 to * disable animation */ - "duration": 0, + duration: 0, /* * Percentage of viewport height to be additionally scrolled when user presses arrow up * in the line of the top of the viewport. */ - "percentageToScrollWhenUserPressesArrowUp": 0, + percentageToScrollWhenUserPressesArrowUp: 0, /* * Flag to control if it should scroll when user places the caret in the last * line of the viewport */ - "scrollWhenCaretIsInTheLastLineOfViewport": false + scrollWhenCaretIsInTheLastLineOfViewport: false, }; /* @@ -337,10 +350,10 @@ exports.customLocaleStrings = {}; */ exports.importExportRateLimiting = { // duration of the rate limit window (milliseconds) - "windowMs": 90000, + windowMs: 90000, // maximum number of requests per IP to allow during the rate limit window - "max": 10 + max: 10, }; /* @@ -353,10 +366,10 @@ exports.importExportRateLimiting = { */ exports.commitRateLimiting = { // duration of the rate limit window (seconds) - "duration": 1, + duration: 1, // maximum number of chanes per IP to allow during the rate limit window - "points": 10 + points: 10, }; /* @@ -367,76 +380,65 @@ exports.commitRateLimiting = { */ exports.importMaxFileSize = 50 * 1024 * 1024; - -/* - * From Etherpad 1.8.3 onwards import was restricted to authors who had - * content within the pad. - * - * This setting will override that restriction and allow any user to import - * without the requirement to add content to a pad. - * - * This setting is useful for when you use a plugin for authentication so you - * can already trust each user. - */ -exports.allowAnyoneToImport = false, - - // checks if abiword is avaiable -exports.abiwordAvailable = function() -{ +exports.abiwordAvailable = function () { if (exports.abiword != null) { - return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes"; + return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; } else { - return "no"; + return 'no'; } }; -exports.sofficeAvailable = function() { +exports.sofficeAvailable = function () { if (exports.soffice != null) { - return os.type().indexOf("Windows") != -1 ? "withoutPDF": "yes"; + return os.type().indexOf('Windows') != -1 ? 'withoutPDF' : 'yes'; } else { - return "no"; + return 'no'; } }; -exports.exportAvailable = function() { - var abiword = exports.abiwordAvailable(); - var soffice = exports.sofficeAvailable(); +exports.exportAvailable = function () { + const abiword = exports.abiwordAvailable(); + const soffice = exports.sofficeAvailable(); - if (abiword == "no" && soffice == "no") { - return "no"; - } else if ((abiword == "withoutPDF" && soffice == "no") || (abiword == "no" && soffice == "withoutPDF")) { - return "withoutPDF"; + if (abiword == 'no' && soffice == 'no') { + return 'no'; + } else if ((abiword == 'withoutPDF' && soffice == 'no') || (abiword == 'no' && soffice == 'withoutPDF')) { + return 'withoutPDF'; } else { - return "yes"; + return 'yes'; } }; // Provide git version if available -exports.getGitCommit = function() { - var version = ""; +exports.getGitCommit = function () { + let version = ''; try { - var rootPath = exports.root; - if (fs.lstatSync(rootPath + '/.git').isFile()) { - rootPath = fs.readFileSync(rootPath + '/.git', "utf8"); + let rootPath = exports.root; + if (fs.lstatSync(`${rootPath}/.git`).isFile()) { + rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = rootPath.split(' ').pop().trim(); } else { rootPath += '/.git'; } - var ref = fs.readFileSync(rootPath + "/HEAD", "utf-8"); - var refPath = rootPath + "/" + ref.substring(5, ref.indexOf("\n")); - version = fs.readFileSync(refPath, "utf-8"); + const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); + if (ref.startsWith('ref: ')) { + const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; + version = fs.readFileSync(refPath, 'utf-8'); + } else { + version = ref; + } version = version.substring(0, 7); - } catch(e) { - console.warn("Can't get git version for server header\n" + e.message) + } catch (e) { + console.warn(`Can't get git version for server header\n${e.message}`); } return version; -} +}; // Return etherpad version from package.json -exports.getEpVersion = function() { +exports.getEpVersion = function () { return require('ep_etherpad-lite/package.json').version; -} +}; /** * Receives a settingsObj and, if the property name is a valid configuration @@ -446,9 +448,9 @@ exports.getEpVersion = function() { * both "settings.json" and "credentials.json". */ function storeSettings(settingsObj) { - for (var i in settingsObj) { + for (const i in settingsObj) { // test if the setting starts with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { + if (i.charAt(0).search('[a-z]') !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } @@ -480,26 +482,26 @@ function storeSettings(settingsObj) { * in the literal string "null", instead. */ function coerceValue(stringValue) { - // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number - const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); + // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number + const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); - if (isNumeric) { - // detected numeric string. Coerce to a number + if (isNumeric) { + // detected numeric string. Coerce to a number - return +stringValue; - } + return +stringValue; + } - // the boolean literal case is easy. - if (stringValue === "true" ) { - return true; - } + // the boolean literal case is easy. + if (stringValue === 'true') { + return true; + } - if (stringValue === "false") { - return false; - } + if (stringValue === 'false') { + return false; + } - // otherwise, return this value as-is - return stringValue; + // otherwise, return this value as-is + return stringValue; } /** @@ -622,24 +624,24 @@ function lookupEnvironmentVariables(obj) { * The isSettings variable only controls the error logging. */ function parseSettings(settingsFilename, isSettings) { - let settingsStr = ""; + let settingsStr = ''; let settingsType, notFoundMessage, notFoundFunction; if (isSettings) { - settingsType = "settings"; - notFoundMessage = "Continuing using defaults!"; + settingsType = 'settings'; + notFoundMessage = 'Continuing using defaults!'; notFoundFunction = console.warn; } else { - settingsType = "credentials"; - notFoundMessage = "Ignoring."; + settingsType = 'credentials'; + notFoundMessage = 'Ignoring.'; notFoundFunction = console.info; } try { - //read the settings file + // read the settings file settingsStr = fs.readFileSync(settingsFilename).toString(); - } catch(e) { + } catch (e) { notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); // or maybe undefined! @@ -647,7 +649,7 @@ function parseSettings(settingsFilename, isSettings) { } try { - settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); const settings = JSON.parse(settingsStr); @@ -656,7 +658,7 @@ function parseSettings(settingsFilename, isSettings) { const replacedSettings = lookupEnvironmentVariables(settings); return replacedSettings; - } catch(e) { + } catch (e) { console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); process.exit(1); @@ -665,123 +667,71 @@ function parseSettings(settingsFilename, isSettings) { exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives - var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); + const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); // Discover if a credential file exists - var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); + const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); // try to parse the settings - var settings = parseSettings(settingsFilename, true); + const settings = parseSettings(settingsFilename, true); // try to parse the credentials - var credentials = parseSettings(credentialsFilename, false); + const credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); - log4js.configure(exports.logconfig);//Configure the logging appenders - log4js.setGlobalLogLevel(exports.loglevel);//set loglevel - process.env['DEBUG'] = 'socket.io:' + exports.loglevel; // Used by SocketIO for Debug + log4js.configure(exports.logconfig);// Configure the logging appenders + log4js.setGlobalLogLevel(exports.loglevel);// set loglevel + process.env.DEBUG = `socket.io:${exports.loglevel}`; // Used by SocketIO for Debug log4js.replaceConsole(); if (!exports.skinName) { - console.warn(`No "skinName" parameter found. Please check out settings.json.template and update your settings.json. Falling back to the default "colibris".`); - exports.skinName = "colibris"; + console.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'; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" if (exports.skinName) { - const skinBasePath = path.join(exports.root, "src", "static", "skins"); + const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); 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".`); - exports.skinName = "colibris"; + exports.skinName = 'colibris'; } // informative variable, just for the log messages - var skinPath = path.normalize(path.join(skinBasePath, exports.skinName)); + let skinPath = path.normalize(path.join(skinBasePath, exports.skinName)); // 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".`); - exports.skinName = "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".`); - exports.skinName = "colibris"; + exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } console.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); } - if (exports.users) { - /* - * Each user must have exactly one of ("password", "hash") attributes set, - * and its value must be not null. - * - * Prune from export.users any user that does not satisfy this condition, - * including the ones that (by chance) have both "password" and "hash" set. - * - * This mechanism is used by the settings.json in the default Dockerfile to - * eschew creating an admin user if no password (or hash) is set. - */ - var filteredUsers = _.pick(exports.users, function(userProperties, username) { - if ((userProperties.hasOwnProperty("password") === false) && (userProperties.hasOwnProperty("hash") === false)) { - console.warn(`Removing user "${username}", because it has no "password" or "hash" field.`); - - return false; - } - - if (userProperties.hasOwnProperty("password") && userProperties.hasOwnProperty("hash")) { - console.warn(`Removing user "${username}", because it has both "password" and "hash" fields set. THIS SHOULD NEVER HAPPEN.`); - - return false; - } - - /* - * If we arrive here, the user has exactly a password or a hash set. - * They may still be null - */ - if (userProperties.hasOwnProperty("password") && (userProperties.password === null)) { - console.warn(`Removing user "${username}", because its "password" is null.`); - - return false; - } - - if (userProperties.hasOwnProperty("hash") && (userProperties.hash === null)) { - console.warn(`Removing user "${username}", because its "hash" value is null.`); - - return false; - } - - /* - * This user has a password, and its password is not null, or it has an - * hash, and its hash is not null (not both). - * - * Keep it. - */ - return true; - }); - - exports.users = filteredUsers; - } - if (exports.abiword) { // Check abiword actually exists if (exports.abiword != null) { - fs.exists(exports.abiword, function(exists) { + fs.exists(exports.abiword, (exists) => { if (!exists) { - var abiwordError = "Abiword does not exist at this path, check your settings file."; + const abiwordError = 'Abiword does not exist at this path, check your settings file.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nError: ${abiwordError}${suppressDisableMsg}`; } - console.error(abiwordError + ` File location: ${exports.abiword}`); + console.error(`${abiwordError} File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -789,46 +739,46 @@ exports.reloadSettings = function reloadSettings() { } if (exports.soffice) { - fs.exists(exports.soffice, function(exists) { + fs.exists(exports.soffice, (exists) => { if (!exists) { - var sofficeError = "soffice (libreoffice) does not exist at this path, check your settings file."; + const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nError: ${sofficeError}${suppressDisableMsg}`; } - console.error(sofficeError + ` File location: ${exports.soffice}`); + console.error(`${sofficeError} File location: ${exports.soffice}`); exports.soffice = null; } }); } if (!exports.sessionKey) { - var sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || "./SESSIONKEY.txt"); + const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename,"utf8"); + exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); console.info(`Session key loaded from: ${sessionkeyFilename}`); - } catch(e) { + } catch (e) { console.info(`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename,exports.sessionKey,"utf8"); + 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."); + 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.'); } - if (exports.dbType === "dirty") { - var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production."; + if (exports.dbType === 'dirty') { + const dirtyWarning = 'DirtyDB is used. This is fine for testing but not recommended for production.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg; + exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - console.warn(dirtyWarning + ` File location: ${exports.dbSettings.filename}`); + console.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); } - if (exports.ip === "") { + 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.`); + 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.'); } }; diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.js index 26d48a62fd8..42e3e3547e1 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.js @@ -2,42 +2,41 @@ * Tidy up the HTML in a given file */ -var log4js = require('log4js'); -var settings = require('./Settings'); -var spawn = require('child_process').spawn; +const log4js = require('log4js'); +const settings = require('./Settings'); +const spawn = require('child_process').spawn; -exports.tidy = function(srcFile) { - var logger = log4js.getLogger('TidyHtml'); +exports.tidy = function (srcFile) { + const logger = log4js.getLogger('TidyHtml'); return new Promise((resolve, reject) => { - // Don't do anything if Tidy hasn't been enabled if (!settings.tidyHtml) { logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); return resolve(null); } - var errMessage = ''; + let errMessage = ''; // Spawn a new tidy instance that cleans up the file inline - logger.debug('Tidying ' + srcFile); - var tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); + logger.debug(`Tidying ${srcFile}`); + const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); // Keep track of any error messages - tidy.stderr.on('data', function (data) { + tidy.stderr.on('data', (data) => { errMessage += data.toString(); }); - tidy.on('close', function(code) { + tidy.on('close', (code) => { // Tidy returns a 0 when no errors occur and a 1 exit code when // the file could be tidied but a few warnings were generated if (code === 0 || code === 1) { - logger.debug('Tidied ' + srcFile + ' successfully'); + logger.debug(`Tidied ${srcFile} successfully`); resolve(null); } else { - logger.error('Failed to tidy ' + srcFile + '\n' + errMessage); - reject('Tidy died with exit code ' + code); + logger.error(`Failed to tidy ${srcFile}\n${errMessage}`); + reject(`Tidy died with exit code ${code}`); } }); }); -} +}; diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.js index 0181f1ea205..8332ff2047a 100644 --- a/src/node/utils/UpdateCheck.js +++ b/src/node/utils/UpdateCheck.js @@ -1,44 +1,44 @@ -const semver = require('semver'); -const settings = require('./Settings'); -const request = require('request'); - -let infos; - -function loadEtherpadInformations() { - return new Promise(function(resolve, reject) { - request('https://static.etherpad.org/info.json', function (er, response, body) { - if (er) return reject(er); - - try { - infos = JSON.parse(body); - return resolve(infos); - } catch (err) { - return reject(err); - } - }); - }) -} - -exports.getLatestVersion = function() { - exports.needsUpdate(); - return infos.latestVersion; -} - -exports.needsUpdate = function(cb) { - loadEtherpadInformations().then(function(info) { - if (semver.gt(info.latestVersion, settings.getEpVersion())) { - if (cb) return cb(true); - } - }).catch(function (err) { - console.error('Can not perform Etherpad update check: ' + err) - if (cb) return cb(false); - }) -} - -exports.check = function() { - exports.needsUpdate(function (needsUpdate) { - if (needsUpdate) { - console.warn('Update available: Download the actual version ' + infos.latestVersion) - } - }) -} \ No newline at end of file +const semver = require('semver'); +const settings = require('./Settings'); +const request = require('request'); + +let infos; + +function loadEtherpadInformations() { + return new Promise((resolve, reject) => { + request('https://static.etherpad.org/info.json', (er, response, body) => { + if (er) return reject(er); + + try { + infos = JSON.parse(body); + return resolve(infos); + } catch (err) { + return reject(err); + } + }); + }); +} + +exports.getLatestVersion = function () { + exports.needsUpdate(); + return infos.latestVersion; +}; + +exports.needsUpdate = function (cb) { + loadEtherpadInformations().then((info) => { + if (semver.gt(info.latestVersion, settings.getEpVersion())) { + if (cb) return cb(true); + } + }).catch((err) => { + console.error(`Can not perform Etherpad update check: ${err}`); + if (cb) return cb(false); + }); +}; + +exports.check = function () { + exports.needsUpdate((needsUpdate) => { + if (needsUpdate) { + console.warn(`Update available: Download the actual version ${infos.latestVersion}`); + } + }); +}; diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index bc387ca2c68..8bb3ab00dc5 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -14,14 +14,13 @@ * limitations under the License. */ -var async = require('async'); -var Buffer = require('buffer').Buffer; -var fs = require('fs'); -var path = require('path'); -var zlib = require('zlib'); -var settings = require('./Settings'); -var semver = require('semver'); -var existsSync = require('./path_exists'); +const async = require('async'); +const Buffer = require('buffer').Buffer; +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); +const settings = require('./Settings'); +const existsSync = require('./path_exists'); /* * The crypto module can be absent on reduced node installations. @@ -43,13 +42,13 @@ try { _crypto = undefined; } -var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); +let CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; -var responseCache = {}; +const responseCache = {}; function djb2Hash(data) { - const chars = data.split("").map(str => str.charCodeAt(0)); + const chars = data.split('').map((str) => str.charCodeAt(0)); return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; } @@ -82,23 +81,23 @@ function CachingMiddleware() { } CachingMiddleware.prototype = new function () { function handle(req, res, next) { - if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) { + if (!(req.method == 'GET' || req.method == 'HEAD') || !CACHE_DIR) { return next(undefined, req, res); } - var old_req = {}; - var old_res = {}; + const old_req = {}; + const old_res = {}; - var supportsGzip = + const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') != -1; - var path = require('url').parse(req.url).path; - var cacheKey = generateCacheKey(path); + const path = require('url').parse(req.url).path; + const cacheKey = generateCacheKey(path); - fs.stat(CACHE_DIR + 'minified_' + cacheKey, function (error, stats) { - var modifiedSince = (req.headers['if-modified-since'] - && new Date(req.headers['if-modified-since'])); - var lastModifiedCache = !error && stats.mtime; + fs.stat(`${CACHE_DIR}minified_${cacheKey}`, (error, stats) => { + const modifiedSince = (req.headers['if-modified-since'] && + new Date(req.headers['if-modified-since'])); + const lastModifiedCache = !error && stats.mtime; if (lastModifiedCache && responseCache[cacheKey]) { req.headers['if-modified-since'] = lastModifiedCache.toUTCString(); } else { @@ -109,13 +108,13 @@ CachingMiddleware.prototype = new function () { old_req.method = req.method; req.method = 'GET'; - var expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {})['expires']); + const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); if (expirationDate > new Date()) { // Our cached version is still valid. return respond(); } - var _headers = {}; + const _headers = {}; old_res.setHeader = res.setHeader; res.setHeader = function (key, value) { // Don't set cookies, see issue #707 @@ -127,46 +126,46 @@ CachingMiddleware.prototype = new function () { old_res.writeHead = res.writeHead; res.writeHead = function (status, headers) { - var lastModified = (res.getHeader('last-modified') - && new Date(res.getHeader('last-modified'))); + const lastModified = (res.getHeader('last-modified') && + new Date(res.getHeader('last-modified'))); res.writeHead = old_res.writeHead; if (status == 200) { // Update cache - var buffer = ''; + let buffer = ''; - Object.keys(headers || {}).forEach(function (key) { + Object.keys(headers || {}).forEach((key) => { res.setHeader(key, headers[key]); }); headers = _headers; old_res.write = res.write; old_res.end = res.end; - res.write = function(data, encoding) { + res.write = function (data, encoding) { buffer += data.toString(encoding); }; - res.end = function(data, encoding) { + res.end = function (data, encoding) { async.parallel([ function (callback) { - var path = CACHE_DIR + 'minified_' + cacheKey; - fs.writeFile(path, buffer, function (error, stats) { + const path = `${CACHE_DIR}minified_${cacheKey}`; + fs.writeFile(path, buffer, (error, stats) => { callback(); }); - } - , function (callback) { - var path = CACHE_DIR + 'minified_' + cacheKey + '.gz'; - zlib.gzip(buffer, function(error, content) { + }, + function (callback) { + const path = `${CACHE_DIR}minified_${cacheKey}.gz`; + zlib.gzip(buffer, (error, content) => { if (error) { callback(); } else { - fs.writeFile(path, content, function (error, stats) { + fs.writeFile(path, content, (error, stats) => { callback(); }); } }); - } - ], function () { - responseCache[cacheKey] = {statusCode: status, headers: headers}; + }, + ], () => { + responseCache[cacheKey] = {statusCode: status, headers}; respond(); }); }; @@ -174,8 +173,8 @@ CachingMiddleware.prototype = new function () { // Nothing new changed from the cached version. old_res.write = res.write; old_res.end = res.end; - res.write = function(data, encoding) {}; - res.end = function(data, encoding) { respond(); }; + res.write = function (data, encoding) {}; + res.end = function (data, encoding) { respond(); }; } else { res.writeHead(status, headers); } @@ -192,23 +191,24 @@ CachingMiddleware.prototype = new function () { res.write = old_res.write || res.write; res.end = old_res.end || res.end; - var headers = responseCache[cacheKey].headers; - var statusCode = responseCache[cacheKey].statusCode; + const headers = {}; + Object.assign(headers, (responseCache[cacheKey].headers || {})); + const statusCode = responseCache[cacheKey].statusCode; - var pathStr = CACHE_DIR + 'minified_' + cacheKey; - if (supportsGzip && (headers['content-type'] || '').match(/^text\//)) { - pathStr = pathStr + '.gz'; + let pathStr = `${CACHE_DIR}minified_${cacheKey}`; + if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { + pathStr += '.gz'; headers['content-encoding'] = 'gzip'; } - var lastModified = (headers['last-modified'] - && new Date(headers['last-modified'])); + const lastModified = (headers['last-modified'] && + new Date(headers['last-modified'])); if (statusCode == 200 && lastModified <= modifiedSince) { res.writeHead(304, headers); res.end(); } else if (req.method == 'GET') { - var readStream = fs.createReadStream(pathStr); + const readStream = fs.createReadStream(pathStr); res.writeHead(statusCode, headers); readStream.pipe(res); } else { diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 7cf29aba49d..e14ba9aca60 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,17 +1,17 @@ -var Changeset = require("../../static/js/Changeset"); -var exportHtml = require('./ExportHtml'); +const Changeset = require('../../static/js/Changeset'); +const exportHtml = require('./ExportHtml'); -function PadDiff (pad, fromRev, toRev) { +function PadDiff(pad, fromRev, toRev) { // check parameters if (!pad || !pad.id || !pad.atext || !pad.pool) { throw new Error('Invalid pad'); } - var range = pad.getValidRevisionRange(fromRev, toRev); + const range = pad.getValidRevisionRange(fromRev, toRev); if (!range) { - throw new Error('Invalid revision range.' + - ' startRev: ' + fromRev + - ' endRev: ' + toRev); + throw new Error(`${'Invalid revision range.' + + ' startRev: '}${fromRev + } endRev: ${toRev}`); } this._pad = pad; @@ -21,12 +21,12 @@ function PadDiff (pad, fromRev, toRev) { this._authors = []; } -PadDiff.prototype._isClearAuthorship = function(changeset) { +PadDiff.prototype._isClearAuthorship = function (changeset) { // unpack - var unpacked = Changeset.unpack(changeset); + const unpacked = Changeset.unpack(changeset); // check if there is nothing in the charBank - if (unpacked.charBank !== "") { + if (unpacked.charBank !== '') { return false; } @@ -36,10 +36,10 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { } // lets iterator over the operators - var iterator = Changeset.opIterator(unpacked.ops); + const iterator = Changeset.opIterator(unpacked.ops); // get the first operator, this should be a clear operator - var clearOperator = iterator.next(); + const clearOperator = iterator.next(); // check if there is only one operator if (iterator.hasNext() === true) { @@ -47,18 +47,18 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { } // check if this operator doesn't change text - if (clearOperator.opcode !== "=") { + if (clearOperator.opcode !== '=') { return false; } // check that this operator applys to the complete text // if the text ends with a new line, its exactly one character less, else it has the same length - if (clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen) { + if (clearOperator.chars !== unpacked.oldLen - 1 && clearOperator.chars !== unpacked.oldLen) { return false; } - var attributes = []; - Changeset.eachAttribNumber(changeset, function(attrNum) { + const attributes = []; + Changeset.eachAttribNumber(changeset, (attrNum) => { attributes.push(attrNum); }); @@ -67,90 +67,84 @@ PadDiff.prototype._isClearAuthorship = function(changeset) { return false; } - var appliedAttribute = this._pad.pool.getAttrib(attributes[0]); + const appliedAttribute = this._pad.pool.getAttrib(attributes[0]); // check if the applied attribute is an anonymous author attribute - if (appliedAttribute[0] !== "author" || appliedAttribute[1] !== "") { + if (appliedAttribute[0] !== 'author' || appliedAttribute[1] !== '') { return false; } return true; }; -PadDiff.prototype._createClearAuthorship = async function(rev) { - - let atext = await this._pad.getInternalRevisionAText(rev); +PadDiff.prototype._createClearAuthorship = async function (rev) { + const atext = await this._pad.getInternalRevisionAText(rev); // build clearAuthorship changeset - var builder = Changeset.builder(atext.text.length); - builder.keepText(atext.text, [['author','']], this._pad.pool); - var changeset = builder.toString(); + const builder = Changeset.builder(atext.text.length); + builder.keepText(atext.text, [['author', '']], this._pad.pool); + const changeset = builder.toString(); return changeset; -} - -PadDiff.prototype._createClearStartAtext = async function(rev) { +}; +PadDiff.prototype._createClearStartAtext = async function (rev) { // get the atext of this revision - let atext = this._pad.getInternalRevisionAText(rev); + const atext = await this._pad.getInternalRevisionAText(rev); // create the clearAuthorship changeset - let changeset = await this._createClearAuthorship(rev); + const changeset = await this._createClearAuthorship(rev); // apply the clearAuthorship changeset - let newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); + const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); return newAText; -} - -PadDiff.prototype._getChangesetsInBulk = async function(startRev, count) { +}; +PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) { // find out which revisions we need - let revisions = []; + const revisions = []; for (let i = startRev; i < (startRev + count) && i <= this._pad.head; i++) { revisions.push(i); } // get all needed revisions (in parallel) - let changesets = [], authors = []; - await Promise.all(revisions.map(rev => { - return this._pad.getRevision(rev).then(revision => { - let arrayNum = rev - startRev; - changesets[arrayNum] = revision.changeset; - authors[arrayNum] = revision.meta.author; - }); - })); - - return { changesets, authors }; -} + const changesets = []; const + authors = []; + await Promise.all(revisions.map((rev) => this._pad.getRevision(rev).then((revision) => { + const arrayNum = rev - startRev; + changesets[arrayNum] = revision.changeset; + authors[arrayNum] = revision.meta.author; + }))); + + return {changesets, authors}; +}; -PadDiff.prototype._addAuthors = function(authors) { - var self = this; +PadDiff.prototype._addAuthors = function (authors) { + const self = this; // add to array if not in the array - authors.forEach(function(author) { + authors.forEach((author) => { if (self._authors.indexOf(author) == -1) { self._authors.push(author); } }); }; -PadDiff.prototype._createDiffAtext = async function() { - - let bulkSize = 100; +PadDiff.prototype._createDiffAtext = async function () { + const bulkSize = 100; // get the cleaned startAText let atext = await this._createClearStartAtext(this._fromRev); let superChangeset = null; - let rev = this._fromRev + 1; + const rev = this._fromRev + 1; for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) { - // get the bulk - let { changesets, authors } = await this._getChangesetsInBulk(rev, bulkSize); + const {changesets, authors} = await this._getChangesetsInBulk(rev, bulkSize); - let addedAuthors = []; + const addedAuthors = []; // run through all changesets for (let i = 0; i < changesets.length && (rev + i) <= this._toRev; ++i) { @@ -180,7 +174,7 @@ PadDiff.prototype._createDiffAtext = async function() { // if there are only clearAuthorship changesets, we don't get a superChangeset, so we can skip this step if (superChangeset) { - let deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); + const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); // apply the superChangeset, which includes all addings atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool); @@ -190,59 +184,57 @@ PadDiff.prototype._createDiffAtext = async function() { } return atext; -} - -PadDiff.prototype.getHtml = async function() { +}; +PadDiff.prototype.getHtml = async function () { // cache the html if (this._html != null) { return this._html; } // get the diff atext - let atext = await this._createDiffAtext(); + const atext = await this._createDiffAtext(); // get the authorColor table - let authorColors = await this._pad.getAllAuthorColors(); + const authorColors = await this._pad.getAllAuthorColors(); // convert the atext to html - this._html = exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); + this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); return this._html; -} - -PadDiff.prototype.getAuthors = async function() { +}; +PadDiff.prototype.getAuthors = async function () { // check if html was already produced, if not produce it, this generates the author array at the same time if (this._html == null) { await this.getHtml(); } return self._authors; -} +}; -PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) { +PadDiff.prototype._extendChangesetWithAuthor = function (changeset, author, apool) { // unpack - var unpacked = Changeset.unpack(changeset); + const unpacked = Changeset.unpack(changeset); - var iterator = Changeset.opIterator(unpacked.ops); - var assem = Changeset.opAssembler(); + const iterator = Changeset.opIterator(unpacked.ops); + const assem = Changeset.opAssembler(); // create deleted attribs - var authorAttrib = apool.putAttrib(["author", author || ""]); - var deletedAttrib = apool.putAttrib(["removed", true]); - var attribs = "*" + Changeset.numToString(authorAttrib) + "*" + Changeset.numToString(deletedAttrib); + const authorAttrib = apool.putAttrib(['author', author || '']); + const deletedAttrib = apool.putAttrib(['removed', true]); + const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; // iteratore over the operators of the changeset - while(iterator.hasNext()) { - var operator = iterator.next(); + while (iterator.hasNext()) { + const operator = iterator.next(); - if (operator.opcode === "-") { + if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; - } else if (operator.opcode === "=" && operator.attribs) { + } else if (operator.opcode === '=' && operator.attribs) { // this is operator changes only attributes, let's mark which author did that - operator.attribs+="*"+Changeset.numToString(authorAttrib); + operator.attribs += `*${Changeset.numToString(authorAttrib)}`; } // append the new operator to our assembler @@ -254,9 +246,9 @@ PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool }; // this method is 80% like Changeset.inverse. I just changed so instead of reverting, it adds deletions and attribute changes to to the atext. -PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { - var lines = Changeset.splitTextLines(startAText.text); - var alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); +PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { + const lines = Changeset.splitTextLines(startAText.text); + const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); // lines and alines are what the exports is meant to apply to. // They may be arrays or objects with .get(i) and .length methods. @@ -278,24 +270,23 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } } - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = Changeset.newOp('+'); - - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); + let curLine = 0; + let curChar = 0; + let curLineOpIter = null; + let curLineOpIterLine; + const curLineNextOp = Changeset.newOp('+'); - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) { + const unpacked = Changeset.unpack(cs); + const csIter = Changeset.opIterator(unpacked.ops); + const builder = Changeset.builder(unpacked.newLen); + function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) { if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { // create curLineOpIter and advance it to curChar curLineOpIter = Changeset.opIterator(alines_get(curLine)); curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; + let indexIntoLine = 0; + let done = false; while (!done) { curLineOpIter.next(curLineNextOp); if (indexIntoLine + curLineNextOp.chars >= curChar) { @@ -320,7 +311,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { curLineOpIter.next(curLineNextOp); } - var charsToUse = Math.min(numChars, curLineNextOp.chars); + const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); numChars -= charsToUse; @@ -338,26 +329,24 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { if (L) { curLine += L; curChar = 0; + } else if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, () => {}); } else { - if (curLineOpIter && curLineOpIterLine == curLine) { - consumeAttribRuns(N, function () {}); - } else { - curChar += N; - } + curChar += N; } } function nextText(numChars) { - var len = 0; - var assem = Changeset.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); + let len = 0; + const assem = Changeset.stringAssembler(); + const firstString = lines_get(curLine).substring(curChar); len += firstString.length; assem.append(firstString); - var lineNum = curLine + 1; + let lineNum = curLine + 1; while (len < numChars) { - var nextString = lines_get(lineNum); + const nextString = lines_get(lineNum); len += nextString.length; assem.append(nextString); lineNum++; @@ -367,7 +356,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { } function cachedStrFunc(func) { - var cache = {}; + const cache = {}; return function (s) { if (!cache[s]) { @@ -377,8 +366,8 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { }; } - var attribKeys = []; - var attribValues = []; + const attribKeys = []; + const attribValues = []; // iterate over all operators of this changeset while (csIter.hasNext()) { @@ -389,27 +378,27 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { // decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set. // If the text this operator applies to is only a star, than this is a false positive and should be ignored - if (csOp.attribs && textBank != "*") { - var deletedAttrib = apool.putAttrib(["removed", true]); - var authorAttrib = apool.putAttrib(["author", ""]); + if (csOp.attribs && textBank != '*') { + const deletedAttrib = apool.putAttrib(['removed', true]); + var authorAttrib = apool.putAttrib(['author', '']); attribKeys.length = 0; attribValues.length = 0; - Changeset.eachAttribNumber(csOp.attribs, function (n) { + Changeset.eachAttribNumber(csOp.attribs, (n) => { attribKeys.push(apool.getAttribKey(n)); attribValues.push(apool.getAttribValue(n)); - if (apool.getAttribKey(n) === "author") { + if (apool.getAttribKey(n) === 'author') { authorAttrib = n; } }); - var undoBackToAttribs = cachedStrFunc(function (attribs) { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool); + var undoBackToAttribs = cachedStrFunc((attribs) => { + const backAttribs = []; + for (let i = 0; i < attribKeys.length; i++) { + const appliedKey = attribKeys[i]; + const appliedValue = attribValues[i]; + const oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, apool); if (appliedValue != oldValue) { backAttribs.push([appliedKey, oldValue]); @@ -419,21 +408,21 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { return Changeset.makeAttribsString('=', backAttribs, apool); }); - var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib); + var oldAttribsAddition = `*${Changeset.numToString(deletedAttrib)}*${Changeset.numToString(authorAttrib)}`; - var textLeftToProcess = textBank; + let textLeftToProcess = textBank; - while(textLeftToProcess.length > 0) { + while (textLeftToProcess.length > 0) { // process till the next line break or process only one line break - var lengthToProcess = textLeftToProcess.indexOf("\n"); - var lineBreak = false; - switch(lengthToProcess) { + let lengthToProcess = textLeftToProcess.indexOf('\n'); + let lineBreak = false; + switch (lengthToProcess) { case -1: - lengthToProcess=textLeftToProcess.length; + lengthToProcess = textLeftToProcess.length; break; case 0: lineBreak = true; - lengthToProcess=1; + lengthToProcess = 1; break; } @@ -446,13 +435,13 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak // consume the attributes of this linebreak - consumeAttribRuns(1, function() {}); + consumeAttribRuns(1, () => {}); } else { // add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it var textBankIndex = 0; - consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) { + consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => { // get the old attributes back - var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition; + var attribs = (undoBackToAttribs(attribs) || '') + oldAttribsAddition; builder.insert(processText.substr(textBankIndex, len), attribs); textBankIndex += len; @@ -471,7 +460,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) { var textBank = nextText(csOp.chars); var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); textBankIndex += len; }); diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.js index c2d43f6c21f..18dc35270c5 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.js @@ -1,15 +1,15 @@ -var fs = require('fs'); +const fs = require('fs'); -var check = function(path) { - var existsSync = fs.statSync || fs.existsSync || path.existsSync; +const check = function (path) { + const existsSync = fs.statSync || fs.existsSync || path.existsSync; - var result; + let result; try { result = existsSync(path); } catch (e) { result = false; } return result; -} +}; module.exports = check; diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index a754823a013..60c1cff2a41 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -13,7 +13,7 @@ exports.firstSatisfies = (promises, predicate) => { // value does not satisfy `predicate`. These transformed Promises will be passed to Promise.race, // yielding the first resolved value that satisfies `predicate`. const newPromises = promises.map( - (p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); + (p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); // If `promises` is an empty array or if none of them resolve to a value that satisfies // `predicate`, then `Promise.race(newPromises)` will never resolve. To handle that, add another @@ -35,27 +35,21 @@ exports.firstSatisfies = (promises, predicate) => { return Promise.race(newPromises); }; -exports.timesLimit = function(ltMax, concurrency, promiseCreator) { - var done = 0 - var current = 0 - - function addAnother () { - function _internalRun () { - done++ - - if (done < ltMax) { - addAnother() - } - } - - promiseCreator(current) - .then(_internalRun) - .catch(_internalRun) - - current++ +// Calls `promiseCreator(i)` a total number of `total` times, where `i` is 0 through `total - 1` (in +// order). The `concurrency` argument specifies the maximum number of Promises returned by +// `promiseCreator` that are allowed to be active (unresolved) simultaneously. (In other words: If +// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, +// and each remaining Promise will be created once one of the earlier Promises resolves.) This async +// function resolves once all `total` Promises have resolved. +exports.timesLimit = async (total, concurrency, promiseCreator) => { + if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); + let next = 0; + const addAnother = () => promiseCreator(next++).finally(() => { + if (next < total) return addAnother(); + }); + const promises = []; + for (let i = 0; i < concurrency && i < total; i++) { + promises.push(addAnother()); } - - for (var i = 0; i < concurrency && i < ltMax; i++) { - addAnother() - } -} + await Promise.all(promises); +}; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js index 3815c66dbfa..622b0082dcb 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.js @@ -1,11 +1,10 @@ /** * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids */ -var crypto = require('crypto'); +const crypto = require('crypto'); -var randomString = function(len) -{ - return crypto.randomBytes(len).toString('hex') +const randomString = function (len) { + return crypto.randomBytes(len).toString('hex'); }; module.exports = randomString; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 80d645dd09a..c688816ade1 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -17,10 +17,12 @@ , "pad_connectionstatus.js" , "chat.js" , "gritter.js" + , "$js-cookie/src/js.cookie.js" , "$tinycon/tinycon.js" , "excanvas.js" , "farbtastic.js" , "skin_variants.js" + , "socketio.js" ] , "timeslider.js": [ "timeslider.js" @@ -44,6 +46,7 @@ , "broadcast.js" , "broadcast_slider.js" , "broadcast_revisions.js" + , "socketio.js" ] , "ace2_inner.js": [ "ace2_inner.js" @@ -70,7 +73,6 @@ , "jquery.js" , "rjquery.js" , "$async.js" - , "$async/lib/async.js" , "underscore.js" , "$underscore.js" , "$underscore/underscore.js" diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js index a2b8f557935..59fe6e30bb8 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.js @@ -1,53 +1,50 @@ /** * The Toolbar Module creates and renders the toolbars and buttons */ -var _ = require("underscore") - , tagAttributes - , tag - , Button - , ButtonsGroup - , Separator - , defaultButtonAttributes - , removeItem; - -removeItem = function(array,what) { - var ax; +const _ = require('underscore'); +let tagAttributes; +let tag; +let Button; +let ButtonsGroup; +let Separator; +let defaultButtonAttributes; +let removeItem; + +removeItem = function (array, what) { + let ax; while ((ax = array.indexOf(what)) !== -1) { array.splice(ax, 1); } - return array; + return array; }; defaultButtonAttributes = function (name, overrides) { return { command: name, - localizationId: "pad.toolbar." + name + ".title", - class: "buttonicon buttonicon-" + name + localizationId: `pad.toolbar.${name}.title`, + class: `buttonicon buttonicon-${name}`, }; }; tag = function (name, attributes, contents) { - var aStr = tagAttributes(attributes); + const aStr = tagAttributes(attributes); if (_.isString(contents) && contents.length > 0) { - return '<' + name + aStr + '>' + contents + ''; - } - else { - return '<' + name + aStr + '>'; + return `<${name}${aStr}>${contents}`; + } else { + return `<${name}${aStr}>`; } }; tagAttributes = function (attributes) { - attributes = _.reduce(attributes || {}, function (o, val, name) { + attributes = _.reduce(attributes || {}, (o, val, name) => { if (!_.isUndefined(val)) { o[name] = val; } return o; }, {}); - return " " + _.map(attributes, function (val, name) { - return "" + name + '="' + _.escape(val) + '"'; - }).join(" "); + return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`; }; ButtonsGroup = function () { @@ -55,8 +52,8 @@ ButtonsGroup = function () { }; ButtonsGroup.fromArray = function (array) { - var btnGroup = new this; - _.each(array, function (btnName) { + const btnGroup = new this(); + _.each(array, (btnName) => { btnGroup.addButton(Button.load(btnName)); }); return btnGroup; @@ -69,19 +66,18 @@ ButtonsGroup.prototype.addButton = function (button) { ButtonsGroup.prototype.render = function () { if (this.buttons && this.buttons.length == 1) { - this.buttons[0].grouping = ""; - } - else { - _.first(this.buttons).grouping = "grouped-left"; - _.last(this.buttons).grouping = "grouped-right"; - _.each(this.buttons.slice(1, -1), function (btn) { - btn.grouping = "grouped-middle" + this.buttons[0].grouping = ''; + } else { + _.first(this.buttons).grouping = 'grouped-left'; + _.last(this.buttons).grouping = 'grouped-right'; + _.each(this.buttons.slice(1, -1), (btn) => { + btn.grouping = 'grouped-middle'; }); } - return _.map(this.buttons, function (btn) { - if(btn) return btn.render(); - }).join("\n"); + return _.map(this.buttons, (btn) => { + if (btn) return btn.render(); + }).join('\n'); }; Button = function (attributes) { @@ -89,165 +85,163 @@ Button = function (attributes) { }; Button.load = function (btnName) { - var button = module.exports.availableButtons[btnName]; - try{ + const button = module.exports.availableButtons[btnName]; + try { if (button.constructor === Button || button.constructor === SelectButton) { return button; - } - else { + } else { return new Button(button); } - }catch(e){ - console.warn("Error loading button", btnName); + } catch (e) { + console.warn('Error loading button', btnName); return false; } }; _.extend(Button.prototype, { - grouping: "", + grouping: '', - render: function () { - var liAttributes = { - "data-type": "button", - "data-key": this.attributes.command, + render() { + const liAttributes = { + 'data-type': 'button', + 'data-key': this.attributes.command, }; - return tag("li", liAttributes, - tag("a", { "class": this.grouping, "data-l10n-id": this.attributes.localizationId }, - tag("button", { "class": " "+ this.attributes.class, "data-l10n-id": this.attributes.localizationId }) - ) + return tag('li', liAttributes, + tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId}, + tag('button', {'class': ` ${this.attributes.class}`, 'data-l10n-id': this.attributes.localizationId}) + ) ); - } + }, }); - var SelectButton = function (attributes) { this.attributes = attributes; this.options = []; }; _.extend(SelectButton.prototype, Button.prototype, { - addOption: function (value, text, attributes) { + addOption(value, text, attributes) { this.options.push({ - value: value, - text: text, - attributes: attributes + value, + text, + attributes, }); return this; }, - select: function (attributes) { - var options = []; + select(attributes) { + const options = []; - _.each(this.options, function (opt) { - var a = _.extend({ - value: opt.value + _.each(this.options, (opt) => { + const a = _.extend({ + value: opt.value, }, opt.attributes); - options.push( tag("option", a, opt.text) ); + options.push(tag('option', a, opt.text)); }); - return tag("select", attributes, options.join("")); + return tag('select', attributes, options.join('')); }, - render: function () { - var attributes = { - id: this.attributes.id, - "data-key": this.attributes.command, - "data-type": "select" + render() { + const attributes = { + 'id': this.attributes.id, + 'data-key': this.attributes.command, + 'data-type': 'select', }; - return tag("li", attributes, - this.select({ id: this.attributes.selectId }) + return tag('li', attributes, + this.select({id: this.attributes.selectId}) ); - } + }, }); Separator = function () {}; Separator.prototype.render = function () { - return tag("li", { "class": "separator" }); + return tag('li', {class: 'separator'}); }; module.exports = { availableButtons: { - bold: defaultButtonAttributes("bold"), - italic: defaultButtonAttributes("italic"), - underline: defaultButtonAttributes("underline"), - strikethrough: defaultButtonAttributes("strikethrough"), + bold: defaultButtonAttributes('bold'), + italic: defaultButtonAttributes('italic'), + underline: defaultButtonAttributes('underline'), + strikethrough: defaultButtonAttributes('strikethrough'), orderedlist: { - command: "insertorderedlist", - localizationId: "pad.toolbar.ol.title", - class: "buttonicon buttonicon-insertorderedlist" + command: 'insertorderedlist', + localizationId: 'pad.toolbar.ol.title', + class: 'buttonicon buttonicon-insertorderedlist', }, unorderedlist: { - command: "insertunorderedlist", - localizationId: "pad.toolbar.ul.title", - class: "buttonicon buttonicon-insertunorderedlist" + command: 'insertunorderedlist', + localizationId: 'pad.toolbar.ul.title', + class: 'buttonicon buttonicon-insertunorderedlist', }, - indent: defaultButtonAttributes("indent"), + indent: defaultButtonAttributes('indent'), outdent: { - command: "outdent", - localizationId: "pad.toolbar.unindent.title", - class: "buttonicon buttonicon-outdent" + command: 'outdent', + localizationId: 'pad.toolbar.unindent.title', + class: 'buttonicon buttonicon-outdent', }, - undo: defaultButtonAttributes("undo"), - redo: defaultButtonAttributes("redo"), + undo: defaultButtonAttributes('undo'), + redo: defaultButtonAttributes('redo'), clearauthorship: { - command: "clearauthorship", - localizationId: "pad.toolbar.clearAuthorship.title", - class: "buttonicon buttonicon-clearauthorship" + command: 'clearauthorship', + localizationId: 'pad.toolbar.clearAuthorship.title', + class: 'buttonicon buttonicon-clearauthorship', }, importexport: { - command: "import_export", - localizationId: "pad.toolbar.import_export.title", - class: "buttonicon buttonicon-import_export" + command: 'import_export', + localizationId: 'pad.toolbar.import_export.title', + class: 'buttonicon buttonicon-import_export', }, timeslider: { - command: "showTimeSlider", - localizationId: "pad.toolbar.timeslider.title", - class: "buttonicon buttonicon-history" + command: 'showTimeSlider', + localizationId: 'pad.toolbar.timeslider.title', + class: 'buttonicon buttonicon-history', }, - savedrevision: defaultButtonAttributes("savedRevision"), - settings: defaultButtonAttributes("settings"), - embed: defaultButtonAttributes("embed"), - showusers: defaultButtonAttributes("showusers"), + savedrevision: defaultButtonAttributes('savedRevision'), + settings: defaultButtonAttributes('settings'), + embed: defaultButtonAttributes('embed'), + showusers: defaultButtonAttributes('showusers'), timeslider_export: { - command: "import_export", - localizationId: "timeslider.toolbar.exportlink.title", - class: "buttonicon buttonicon-import_export" + command: 'import_export', + localizationId: 'timeslider.toolbar.exportlink.title', + class: 'buttonicon buttonicon-import_export', }, timeslider_settings: { - command: "settings", - localizationId: "pad.toolbar.settings.title", - class: "buttonicon buttonicon-settings" + command: 'settings', + localizationId: 'pad.toolbar.settings.title', + class: 'buttonicon buttonicon-settings', }, timeslider_returnToPad: { - command: "timeslider_returnToPad", - localizationId: "timeslider.toolbar.returnbutton", - class: "buttontext" - } + command: 'timeslider_returnToPad', + localizationId: 'timeslider.toolbar.returnbutton', + class: 'buttontext', + }, }, - registerButton: function (buttonName, buttonInfo) { + registerButton(buttonName, buttonInfo) { this.availableButtons[buttonName] = buttonInfo; }, - button: function (attributes) { + button(attributes) { return new Button(attributes); }, - separator: function () { - return (new Separator).render(); + separator() { + return (new Separator()).render(); }, - selectButton: function (attributes) { + selectButton(attributes) { return new SelectButton(attributes); }, @@ -255,15 +249,15 @@ module.exports = { * Valid values for whichMenu: 'left' | 'right' | 'timeslider-right' * Valid values for page: 'pad' | 'timeslider' */ - menu: function (buttons, isReadOnly, whichMenu, page) { + menu(buttons, isReadOnly, whichMenu, page) { if (isReadOnly) { // The best way to detect if it's the left editbar is to check for a bold button - if (buttons[0].indexOf("bold") !== -1) { + if (buttons[0].indexOf('bold') !== -1) { // Clear all formatting buttons buttons = []; } else { // Remove Save Revision from the right menu - removeItem(buttons[0],"savedrevision"); + removeItem(buttons[0], 'savedrevision'); } } else { /* @@ -277,14 +271,12 @@ module.exports = { * sufficient to visit a single read only pad to cause the disappearence * of the star button from all the pads. */ - if ((buttons[0].indexOf("savedrevision") === -1) && (whichMenu === "right") && (page === "pad")) { - buttons[0].push("savedrevision"); + if ((buttons[0].indexOf('savedrevision') === -1) && (whichMenu === 'right') && (page === 'pad')) { + buttons[0].push('savedrevision'); } } - var groups = _.map(buttons, function (group) { - return ButtonsGroup.fromArray(group).render(); - }); + const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render()); return groups.join(this.separator()); - } + }, }; diff --git a/src/package-lock.json b/src/package-lock.json index 3c6a019d813..1725a35483c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,8 +1,12161 @@ { "name": "ep_etherpad-lite", - "version": "1.8.5", - "lockfileVersion": 1, + "version": "1.8.7", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "ep_etherpad-lite", + "version": "1.8.7", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.0", + "async-stacktrace": "0.0.2", + "channels": "0.0.4", + "cheerio": "0.22.0", + "clean-css": "4.2.3", + "cookie-parser": "1.4.5", + "ejs": "2.6.1", + "etherpad-require-kernel": "1.0.9", + "etherpad-yajsml": "0.0.2", + "express": "4.17.1", + "express-rate-limit": "5.1.1", + "express-session": "1.17.1", + "find-root": "1.1.0", + "formidable": "1.2.1", + "graceful-fs": "4.2.4", + "http-errors": "1.8.0", + "js-cookie": "^2.2.1", + "jsonminify": "0.4.1", + "languages4translatewiki": "0.1.3", + "lodash.clonedeep": "4.5.0", + "log4js": "0.6.35", + "measured-core": "1.11.2", + "mime-types": "^2.1.27", + "nodeify": "1.0.1", + "npm": "6.14.8", + "openapi-backend": "2.4.1", + "proxy-addr": "^2.0.6", + "rate-limiter-flexible": "^2.1.4", + "rehype": "^10.0.0", + "rehype-minify-whitespace": "^4.0.5", + "request": "2.88.2", + "resolve": "1.1.7", + "security": "1.0.0", + "semver": "5.6.0", + "slide": "1.1.6", + "socket.io": "^2.3.0", + "terser": "^4.7.0", + "threads": "^1.4.0", + "tiny-worker": "^2.3.0", + "tinycon": "0.0.1", + "ueberdb2": "^0.5.6", + "underscore": "1.8.3", + "unorm": "1.4.1" + }, + "bin": { + "etherpad-lite": "node/server.js" + }, + "devDependencies": { + "eslint": "^7.15.0", + "eslint-config-etherpad": "^1.0.20", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", + "etherpad-cli-client": "0.0.9", + "mocha": "7.1.2", + "mocha-froth": "^0.2.10", + "nyc": "15.0.1", + "set-cookie-parser": "^2.4.6", + "sinon": "^9.2.0", + "superagent": "^3.8.3", + "supertest": "4.0.2", + "wd": "1.12.1" + }, + "engines": { + "node": ">=10.13.0", + "npm": ">=5.5.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", + "dependencies": { + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.3.tgz", + "integrity": "sha512-QoPaxGXfgqgGpK1p21FJ400z56hV681a8DOcZt3J5z0WIHgFeaIZ4+6bX5ATqmOoCpRCsH4ITEwKaOyFMz7wOA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.1.tgz", + "integrity": "sha512-1Vlm18XYW6Yg7uHunroXeunWz5FShPFAdxBbPy8H6niB2Elz9QQsCoYHMbcc11EL1pTxaIr9HXz2An/mHXlX1Q==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^8.0.0", + "@apidevtools/openapi-schemas": "^2.0.2", + "@apidevtools/swagger-methods": "^3.0.0", + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "openapi-types": "^1.3.5", + "z-schema": "^4.2.2" + } + }, + "node_modules/@azure/ms-rest-azure-env": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-1.1.2.tgz", + "integrity": "sha512-l7z0DPCi2Hp88w12JhDTtx5d0Y3+vhfE7JKJb9O7sEz71Cwp053N8piTtTnnk/tUor9oZHgEKi/p3tQQmLPjvA==" + }, + "node_modules/@azure/ms-rest-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.9.0.tgz", + "integrity": "sha512-cB4Z2Mg7eBmet1rfbf0QSO1XbhfknRW7B+mX3IHJq0KGHaGJvCPoVTgdsJdCkazEMK1jtANFNEDDzSQacxyzbA==", + "dependencies": { + "@types/tunnel": "0.0.0", + "axios": "^0.19.0", + "form-data": "^2.3.2", + "tough-cookie": "^2.4.3", + "tslib": "^1.9.2", + "tunnel": "0.0.6", + "uuid": "^3.2.1", + "xml2js": "^0.4.19" + } + }, + "node_modules/@azure/ms-rest-nodeauth": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-2.0.2.tgz", + "integrity": "sha512-KmNNICOxt3EwViAJI3iu2VH8t8BQg5J2rSAyO4IUYLF9ZwlyYsP419pdvl4NBUhluAP2cgN7dfD2V6E6NOMZlQ==", + "dependencies": { + "@azure/ms-rest-azure-env": "^1.1.2", + "@azure/ms-rest-js": "^1.8.7", + "adal-node": "^0.1.28" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.8.3" + } + }, + "node_modules/@babel/core": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz", + "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.6", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.6", + "@babel/parser": "^7.9.6", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/core/node_modules/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 + }, + "node_modules/@babel/core/node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@babel/core/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz", + "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.9.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", + "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.9.5" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", + "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.6", + "@babel/types": "^7.9.0", + "lodash": "^4.17.13" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz", + "integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, + "node_modules/@babel/helpers": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz", + "integrity": "sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6" + } + }, + "node_modules/@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz", + "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==", + "dev": true, + "bin": { + "parser": "./bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "node_modules/@babel/traverse": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz", + "integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.6", + "@babel/helper-function-name": "^7.9.5", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/traverse/node_modules/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 + }, + "node_modules/@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "funding": "https://github.com/sponsors/sindresorhus", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@eslint/eslintrc/node_modules/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 + }, + "node_modules/@eslint/eslintrc/node_modules/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, + "funding": "https://github.com/sponsors/sindresorhus", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", + "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ==" + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@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==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", + "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@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==" + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "node_modules/@types/node": { + "version": "14.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz", + "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==" + }, + "node_modules/@types/readable-stream": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz", + "integrity": "sha512-sqsgQqFT7HmQz/V5jH1O0fvQQnXAJO46Gg9LRO/JPfjmVmGUlcx831TZZO3Y3HtWhIkzf3kTsNT0Z0kzIhIvZw==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "*" + } + }, + "node_modules/@types/request": { + "version": "2.48.5", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", + "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@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==" + }, + "node_modules/@types/tunnel": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.0.tgz", + "integrity": "sha512-FGDp0iBRiBdPjOgjJmn1NH0KDLN+Z8fRmo+9J7XGBhubq1DPrGrbmG4UTlGzrpbCpesMqD0sWkzi27EYkOMHyg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", + "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/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==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adal-node": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", + "integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=", + "dependencies": { + "@types/node": "^8.0.47", + "async": ">=0.6.0", + "date-utils": "*", + "jws": "3.x.x", + "request": ">= 2.52.0", + "underscore": ">= 1.3.1", + "uuid": "^3.1.0", + "xmldom": ">= 0.1.x", + "xpath.js": "~1.1.0" + }, + "engines": { + "node": ">= 0.6.15" + } + }, + "node_modules/adal-node/node_modules/@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "node_modules/agentkeepalive": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archiver": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", + "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^2.6.3", + "buffer-crc32": "^0.2.1", + "glob": "^7.1.4", + "readable-stream": "^3.4.0", + "tar-stream": "^2.1.0", + "zip-stream": "^2.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/archiver-utils/node_modules/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 + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/archiver/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "node_modules/async-stacktrace": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/async-stacktrace/-/async-stacktrace-0.0.2.tgz", + "integrity": "sha1-i7uXh+OzjINscpp+nXwIYw210e8=" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "node_modules/axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "1.5.10" + } + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bath-es5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", + "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dependencies": { + "callsite": "1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" + }, + "node_modules/bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, + "node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-request": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", + "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=", + "engines": [ + "node" + ] + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/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==" + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/cassandra-driver": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.1.tgz", + "integrity": "sha512-Vk0kUHlMV4vFXRPwRpKnCZEEMZkp9/RucBDB7gpaUmn9sCusKzzUzVkXeusTxKSoGuIgLJJ7YBiFJdXOctUS7A==", + "dependencies": { + "@types/long": "^4.0.0", + "@types/node": ">=8", + "adm-zip": "^0.4.13", + "long": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ccount": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", + "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/channels": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/channels/-/channels-0.0.4.tgz", + "integrity": "sha1-G+4yPt6hUrue8E9BvG5rD1lIqUE=" + }, + "node_modules/character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "engines": { + "node": "*" + } + }, + "node_modules/cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.1.1" + } + }, + "node_modules/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==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-table": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.4.tgz", + "integrity": "sha512-1vinpnX/ZERcmE443i3SZTmU5DF0rPO9DrL4I2iVAllhxzCM9SzPlHnz19fsZB78htkKZvYBvj6SZ6vXnaxmTA==", + "dependencies": { + "chalk": "^2.4.1", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/cliui/node_modules/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=", + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui/node_modules/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==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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==", + "dependencies": { + "browser-request": "~0.3.0", + "debug": "^4.0.1", + "request": "^2.88.0" + }, + "bin": { + "follow": "./cli.js" + }, + "engines": { + "node": ">=6.13.0" + } + }, + "node_modules/cloudant-follow/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/cloudant-follow/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "node_modules/compress-commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", + "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^3.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^2.3.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/compress-commons/node_modules/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 + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/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==" + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/convert-source-map/node_modules/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 + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc32-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", + "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", + "dev": true, + "dependencies": { + "crc": "^3.4.4", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "./bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dependencies": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "node_modules/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==", + "engines": { + "node": "*" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-utils": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", + "integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q=", + "engines": { + "node": ">0.4.0" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/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 + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dirty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz", + "integrity": "sha1-cO3SuZlUHcmXT9Ooy9DGcP4jYHg=" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dependencies": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/ejs": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticsearch": { + "version": "16.7.2", + "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-16.7.2.tgz", + "integrity": "sha512-1ZLKZlG2ABfYVBX2d7/JgxOsKJrM5Yu62GvshWu7ZSvhxPomCN4Gas90DS51yYI56JolY0XGhyiRlUhLhIL05Q==", + "dependencies": { + "agentkeepalive": "^3.4.1", + "chalk": "^1.0.0", + "lodash": "^4.17.10" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/elasticsearch/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticsearch/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticsearch/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticsearch/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticsearch/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "0.3.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/engine.io-client": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", + "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", + "dependencies": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client/node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "node_modules/errs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", + "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/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==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz", + "integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==", + "dev": true, + "funding": "https://opencollective.com/eslint", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-config-etherpad": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.20.tgz", + "integrity": "sha512-dDEmWphxOmYe7XC0Uevzb0lK7o1jDBGwYMMCdNeZlgo2EfJljnijPgodlimM4R+4OsnfegEMY6rdWoXjzdd5Rw==", + "dev": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "eslint": "^7.15.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "funding": "https://github.com/sponsors/mysticatea", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "funding": "https://github.com/sponsors/mysticatea", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", + "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.1.0", + "ramda": "^0.27.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-plugin-node/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "./bin/semver.js" + } + }, + "node_modules/eslint-plugin-prefer-arrow": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz", + "integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-plugin-you-dont-need-lodash-underscore": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.10.0.tgz", + "integrity": "sha512-Zu1KbHiWKf+alVvT+kFX2M5HW1gmtnkfF1l2cjmFozMnG0gbGgXo8oqK7lwk+ygeOXDmVfOyijqBd7SUub9AEQ==", + "dev": true, + "dependencies": { + "kebab-case": "^1.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "funding": "https://github.com/sponsors/mysticatea", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/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, + "funding": "https://github.com/chalk/ansi-styles?sponsor=1", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "funding": "https://github.com/chalk/chalk?sponsor=1", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/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 + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "funding": "https://github.com/sponsors/sindresorhus", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/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 + }, + "node_modules/eslint/node_modules/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/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, + "funding": "https://github.com/sponsors/sindresorhus", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "./bin/esparse.js", + "esvalidate": "./bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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=", + "dev": true, + "dependencies": { + "async": "*", + "socket.io-client": "*" + }, + "bin": { + "etherpad": "cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/etherpad-require-kernel": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz", + "integrity": "sha1-7Y8E6f0szsOgBVu20t/p2ZkS5+I=" + }, + "node_modules/etherpad-yajsml": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/etherpad-yajsml/-/etherpad-yajsml-0.0.2.tgz", + "integrity": "sha1-HCTSaLCUduY30EnN2xxt+McptG4=" + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-rate-limit": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.1.1.tgz", + "integrity": "sha512-puA1zcCx/quwWUOU6pT6daCt6t7SweD9wKChKhb+KSgFMKRwS81C224hiSAUANw/gnSHiwEhgozM/2ezEBZPeA==" + }, + "node_modules/express-session": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", + "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.0", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/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==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "funding": "https://github.com/avajs/find-cache-dir?sponsor=1", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", + "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", + "dependencies": { + "is-buffer": "~2.0.3" + }, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/follow-redirects/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/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==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "deprecated": "Please upgrade to the upcoming v2, currently (until end of February) install using formidable@canary!" + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "deprecated": "Please update to v 2.2.x", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dependencies": { + "isarray": "2.0.1" + } + }, + "node_modules/has-binary2/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-embedded": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-1.0.6.tgz", + "integrity": "sha512-JQMW+TJe0UAIXZMjCJ4Wf6ayDV9Yv3PBDPsHD4ExBpAspJ6MOcCX+nzVF+UJVv7OqPcg852WEMSHQPoRA+FVSw==", + "dependencies": { + "hast-util-is-element": "^1.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-embedded/node_modules/hast-util-is-element": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz", + "integrity": "sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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==", + "dependencies": { + "ccount": "^1.0.3", + "hastscript": "^5.0.0", + "property-information": "^5.0.0", + "web-namespaces": "^1.1.2", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.4.tgz", + "integrity": "sha512-NFR6ljJRvDcyPP5SbV7MyPBgF47X3BsskLnmw1U34yL+X6YC0MoBx9EyMg8Jtx4FzGH95jw8+c1VPLHaRA0wDQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz", + "integrity": "sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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==", + "dependencies": { + "ccount": "^1.0.0", + "comma-separated-tokens": "^1.0.1", + "hast-util-is-element": "^1.0.0", + "hast-util-whitespace": "^1.0.0", + "html-void-elements": "^1.0.0", + "property-information": "^5.2.0", + "space-separated-tokens": "^1.0.0", + "stringify-entities": "^2.0.0", + "unist-util-is": "^3.0.0", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz", + "integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", + "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", + "dependencies": { + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/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==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dependencies": { + "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" + } + }, + "node_modules/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dependencies": { + "symbol-observable": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", + "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" + }, + "node_modules/is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "./bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/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 + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbi": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", + "integrity": "sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg==" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/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==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonminify": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", + "integrity": "sha1-gF2vuzk5UYjO6atYLIHvlZ1+cQw=", + "engines": { + "node": ">=0.8.0", + "npm": ">=1.1.0" + } + }, + "node_modules/jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==", + "engines": { + "node": "*" + } + }, + "node_modules/jsonschema-draft4": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", + "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=" + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kebab-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.0.tgz", + "integrity": "sha1-P55JkK3K0MaGwOcB92RYaPdfkes=", + "dev": true + }, + "node_modules/languages4translatewiki": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", + "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" + }, + "node_modules/lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/lazystream/node_modules/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 + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "node_modules/lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + }, + "node_modules/lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "node_modules/lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "dev": true + }, + "node_modules/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==", + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "0.6.35", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.35.tgz", + "integrity": "sha1-OrHafLFII7dO04ZcSFk6zfEfG1k=", + "dependencies": { + "readable-stream": "~1.0.2", + "semver": "~4.3.3" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/log4js/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/log4js/node_modules/semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "bin": { + "semver": "./bin/semver" + } + }, + "node_modules/log4js/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/long": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", + "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/lru-cache": { + "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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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==", + "dev": true, + "funding": "https://github.com/sponsors/sindresorhus", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "./bin/semver.js" + } + }, + "node_modules/measured-core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.11.2.tgz", + "integrity": "sha1-nb6m0gdBtW9hq9hm5Jbri4Xmk0k=", + "dependencies": { + "binary-search": "^1.3.3", + "optional-js": "^2.0.0" + }, + "engines": { + "node": ">= 5.12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", + "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", + "dev": true, + "dependencies": { + "ansi-colors": "3.2.3", + "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", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.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", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha-froth": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/mocha-froth/-/mocha-froth-0.2.10.tgz", + "integrity": "sha512-xyJqAYtm2zjrkG870hjeSVvGgS4Dc9tRokmN6R7XLgBKhdtAJ1ytU6zL045djblfHaPyTkSerQU4wqcjsv7Aew==", + "dev": true + }, + "node_modules/mocha/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/mocha/node_modules/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==", + "dev": true + }, + "node_modules/mocha/node_modules/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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mock-json-schema": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/mock-json-schema/-/mock-json-schema-1.0.8.tgz", + "integrity": "sha512-22yL+WggSo8HXqw0HkXgXXJjJMSBCfv54htfwN4BabaFdJ3808jL0CzE+VaBRlj8Nr0+pnSVE9YvsDG5Quu6hQ==", + "dependencies": { + "lodash": "^4.17.11", + "openapi-types": "^1.3.2" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/mssql": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.3.0.tgz", + "integrity": "sha512-6/BK/3J8Oe4t6BYnmdCCORHhyBtBI/Fh0Sh6l1hPzb/hKtxDrsaSDGIpck1u8bzkLzev39TH5W2nz+ffeRz7gg==", + "dependencies": { + "debug": "^4.3.1", + "tarn": "^1.1.5", + "tedious": "^6.7.0" + }, + "bin": { + "mssql": "./bin/mssql" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mssql/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mssql/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/mysql/node_modules/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==", + "dependencies": { + "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" + } + }, + "node_modules/mysql/node_modules/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==" + }, + "node_modules/mysql/node_modules/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==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/nano": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.3.tgz", + "integrity": "sha512-nubyTQeZ/p+xf3ZFFMd7WrZwpcy9tUDrbaXw9HFBsM6zBY5gXspvOjvG2Zz3emT6nfJtP/h7F2/ESfsVVXnuMw==", + "dependencies": { + "@types/request": "^2.48.4", + "cloudant-follow": "^0.18.2", + "debug": "^4.1.1", + "errs": "^0.3.2", + "request": "^2.88.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nano/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nano/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/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==", + "dependencies": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node_modules/node-environment-flags/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "./bin/semver" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nodeify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", + "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", + "dependencies": { + "is-promise": "~1.0.0", + "promise": "~1.3.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm": { + "version": "6.14.8", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz", + "integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==", + "bundleDependencies": [ + "abbrev", + "ansicolors", + "ansistyles", + "aproba", + "archy", + "bin-links", + "bluebird", + "byte-size", + "cacache", + "call-limit", + "chownr", + "ci-info", + "cli-columns", + "cli-table3", + "cmd-shim", + "columnify", + "config-chain", + "debuglog", + "detect-indent", + "detect-newline", + "dezalgo", + "editor", + "figgy-pudding", + "find-npm-prefix", + "fs-vacuum", + "fs-write-stream-atomic", + "gentle-fs", + "glob", + "graceful-fs", + "has-unicode", + "hosted-git-info", + "iferr", + "imurmurhash", + "infer-owner", + "inflight", + "inherits", + "ini", + "init-package-json", + "is-cidr", + "json-parse-better-errors", + "JSONStream", + "lazy-property", + "libcipm", + "libnpm", + "libnpmaccess", + "libnpmhook", + "libnpmorg", + "libnpmsearch", + "libnpmteam", + "libnpx", + "lock-verify", + "lockfile", + "lodash._baseindexof", + "lodash._baseuniq", + "lodash._bindcallback", + "lodash._cacheindexof", + "lodash._createcache", + "lodash._getnative", + "lodash.clonedeep", + "lodash.restparam", + "lodash.union", + "lodash.uniq", + "lodash.without", + "lru-cache", + "meant", + "mississippi", + "mkdirp", + "move-concurrently", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-cache-filename", + "npm-install-checks", + "npm-lifecycle", + "npm-package-arg", + "npm-packlist", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "once", + "opener", + "osenv", + "pacote", + "path-is-inside", + "promise-inflight", + "qrcode-terminal", + "query-string", + "qw", + "read-cmd-shim", + "read-installed", + "read-package-json", + "read-package-tree", + "read", + "readable-stream", + "readdir-scoped-modules", + "request", + "retry", + "rimraf", + "safe-buffer", + "semver", + "sha", + "slide", + "sorted-object", + "sorted-union-stream", + "ssri", + "stringify-package", + "tar", + "text-table", + "tiny-relative-date", + "uid-number", + "umask", + "unique-filename", + "unpipe", + "update-notifier", + "uuid", + "validate-npm-package-license", + "validate-npm-package-name", + "which", + "worker-farm", + "write-file-atomic" + ], + "dependencies": { + "abbrev": "~1.1.1", + "ansicolors": "~0.3.2", + "ansistyles": "~0.1.3", + "aproba": "^2.0.0", + "archy": "~1.0.0", + "bin-links": "^1.1.8", + "bluebird": "^3.5.5", + "byte-size": "^5.0.1", + "cacache": "^12.0.3", + "call-limit": "^1.1.1", + "chownr": "^1.1.4", + "ci-info": "^2.0.0", + "cli-columns": "^3.1.2", + "cli-table3": "^0.5.1", + "cmd-shim": "^3.0.3", + "columnify": "~1.5.4", + "config-chain": "^1.1.12", + "debuglog": "*", + "detect-indent": "~5.0.0", + "detect-newline": "^2.1.0", + "dezalgo": "~1.0.3", + "editor": "~1.0.0", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "fs-vacuum": "~1.2.10", + "fs-write-stream-atomic": "~1.0.10", + "gentle-fs": "^2.3.1", + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", + "has-unicode": "~2.0.1", + "hosted-git-info": "^2.8.8", + "iferr": "^1.0.2", + "imurmurhash": "*", + "infer-owner": "^1.0.4", + "inflight": "~1.0.6", + "inherits": "^2.0.4", + "ini": "^1.3.5", + "init-package-json": "^1.10.3", + "is-cidr": "^3.0.0", + "json-parse-better-errors": "^1.0.2", + "JSONStream": "^1.3.5", + "lazy-property": "~1.0.0", + "libcipm": "^4.0.8", + "libnpm": "^3.0.1", + "libnpmaccess": "^3.0.2", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "libnpx": "^10.2.4", + "lock-verify": "^2.1.0", + "lockfile": "^1.0.4", + "lodash._baseindexof": "*", + "lodash._baseuniq": "~4.6.0", + "lodash._bindcallback": "*", + "lodash._cacheindexof": "*", + "lodash._createcache": "*", + "lodash._getnative": "*", + "lodash.clonedeep": "~4.5.0", + "lodash.restparam": "*", + "lodash.union": "~4.6.0", + "lodash.uniq": "~4.5.0", + "lodash.without": "~4.4.0", + "lru-cache": "^5.1.1", + "meant": "^1.0.2", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.5", + "move-concurrently": "^1.0.1", + "node-gyp": "^5.1.0", + "nopt": "^4.0.3", + "normalize-package-data": "^2.5.0", + "npm-audit-report": "^1.3.3", + "npm-cache-filename": "~1.0.2", + "npm-install-checks": "^3.0.2", + "npm-lifecycle": "^3.1.5", + "npm-package-arg": "^6.1.1", + "npm-packlist": "^1.4.8", + "npm-pick-manifest": "^3.0.2", + "npm-profile": "^4.0.4", + "npm-registry-fetch": "^4.0.7", + "npm-user-validate": "~1.0.0", + "npmlog": "~4.1.2", + "once": "~1.4.0", + "opener": "^1.5.1", + "osenv": "^0.1.5", + "pacote": "^9.5.12", + "path-is-inside": "~1.0.2", + "promise-inflight": "~1.0.1", + "qrcode-terminal": "^0.12.0", + "query-string": "^6.8.2", + "qw": "~1.0.1", + "read": "~1.0.7", + "read-cmd-shim": "^1.0.5", + "read-installed": "~4.0.3", + "read-package-json": "^2.1.1", + "read-package-tree": "^5.3.1", + "readable-stream": "^3.6.0", + "readdir-scoped-modules": "^1.1.0", + "request": "^2.88.0", + "retry": "^0.12.0", + "rimraf": "^2.7.1", + "safe-buffer": "^5.1.2", + "semver": "^5.7.1", + "sha": "^3.0.0", + "slide": "~1.1.6", + "sorted-object": "~2.0.1", + "sorted-union-stream": "~2.1.3", + "ssri": "^6.0.1", + "stringify-package": "^1.0.1", + "tar": "^4.4.13", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "uid-number": "0.0.6", + "umask": "~1.1.0", + "unique-filename": "^1.1.1", + "unpipe": "~1.0.0", + "update-notifier": "^2.5.0", + "uuid": "^3.3.3", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "~3.0.0", + "which": "^1.3.1", + "worker-farm": "^1.7.0", + "write-file-atomic": "^2.4.3" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "6 >=6.2.0 || 8 || >=9.3.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "1.1.1", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "inBundle": true + }, + "node_modules/npm/node_modules/agent-base": { + "version": "4.3.0", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "inBundle": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "3.5.2", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "inBundle": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/npm/node_modules/ajv": { + "version": "5.5.2", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "inBundle": true, + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/npm/node_modules/ansi-align": { + "version": "2.0.0", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "inBundle": true, + "dependencies": { + "string-width": "^2.0.0" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "2.1.1", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "3.2.1", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "inBundle": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/ansicolors": { + "version": "0.3.2", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "inBundle": true + }, + "node_modules/npm/node_modules/ansistyles": { + "version": "0.1.3", + "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=", + "inBundle": true + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "inBundle": true + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "1.1.4", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "inBundle": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/npm/node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/asap": { + "version": "2.0.6", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "inBundle": true + }, + "node_modules/npm/node_modules/asn1": { + "version": "0.2.4", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "inBundle": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/npm/node_modules/assert-plus": { + "version": "1.0.0", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "inBundle": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/asynckit": { + "version": "0.4.0", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "inBundle": true + }, + "node_modules/npm/node_modules/aws-sign2": { + "version": "0.7.0", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/aws4": { + "version": "1.8.0", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.0", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "inBundle": true + }, + "node_modules/npm/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "inBundle": true, + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/npm/node_modules/bin-links": { + "version": "1.1.8", + "integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==", + "inBundle": true, + "dependencies": { + "bluebird": "^3.5.3", + "cmd-shim": "^3.0.0", + "gentle-fs": "^2.3.0", + "graceful-fs": "^4.1.15", + "npm-normalize-package-bin": "^1.0.0", + "write-file-atomic": "^2.3.0" + } + }, + "node_modules/npm/node_modules/bluebird": { + "version": "3.5.5", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "inBundle": true + }, + "node_modules/npm/node_modules/boxen": { + "version": "1.3.0", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "inBundle": true, + "dependencies": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "1.1.11", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "inBundle": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/buffer-from": { + "version": "1.0.0", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "inBundle": true + }, + "node_modules/npm/node_modules/builtins": { + "version": "1.0.3", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "inBundle": true + }, + "node_modules/npm/node_modules/byline": { + "version": "5.0.0", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/byte-size": { + "version": "5.0.1", + "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==", + "inBundle": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "12.0.3", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "inBundle": true, + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/npm/node_modules/call-limit": { + "version": "1.1.1", + "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/camelcase": { + "version": "4.1.0", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/capture-stack-trace": { + "version": "1.0.0", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/caseless": { + "version": "0.12.0", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "inBundle": true + }, + "node_modules/npm/node_modules/chalk": { + "version": "2.4.1", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "inBundle": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "1.1.4", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "inBundle": true + }, + "node_modules/npm/node_modules/ci-info": { + "version": "2.0.0", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "2.0.10", + "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==", + "inBundle": true, + "dependencies": { + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/cli-boxes": { + "version": "1.0.0", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "3.1.2", + "integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=", + "inBundle": true, + "dependencies": { + "string-width": "^2.0.0", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.5.1", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "inBundle": true, + "dependencies": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/npm/node_modules/cliui": { + "version": "5.0.0", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "inBundle": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/npm/node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/cliui/node_modules/string-width": { + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "inBundle": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "inBundle": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "inBundle": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "3.0.3", + "integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "mkdirp": "~0.5.0" + } + }, + "node_modules/npm/node_modules/co": { + "version": "4.6.0", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "inBundle": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/npm/node_modules/code-point-at": { + "version": "1.1.0", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "1.9.1", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "inBundle": true, + "dependencies": { + "color-name": "^1.1.1" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.3", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "inBundle": true + }, + "node_modules/npm/node_modules/colors": { + "version": "1.3.3", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "inBundle": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.5.4", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "inBundle": true, + "dependencies": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "node_modules/npm/node_modules/combined-stream": { + "version": "1.0.6", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "inBundle": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "inBundle": true + }, + "node_modules/npm/node_modules/concat-stream": { + "version": "1.6.2", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "inBundle": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/npm/node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/config-chain": { + "version": "1.1.12", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "inBundle": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/npm/node_modules/configstore": { + "version": "3.1.5", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", + "inBundle": true, + "dependencies": { + "dot-prop": "^4.2.1", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "inBundle": true + }, + "node_modules/npm/node_modules/copy-concurrently": { + "version": "1.0.5", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "inBundle": true, + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/npm/node_modules/copy-concurrently/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "inBundle": true + }, + "node_modules/npm/node_modules/copy-concurrently/node_modules/iferr": { + "version": "0.1.5", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "inBundle": true + }, + "node_modules/npm/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "inBundle": true + }, + "node_modules/npm/node_modules/create-error-class": { + "version": "3.0.2", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "inBundle": true, + "dependencies": { + "capture-stack-trace": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "5.1.0", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "inBundle": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/lru-cache": { + "version": "4.1.5", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "inBundle": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/yallist": { + "version": "2.1.2", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "inBundle": true + }, + "node_modules/npm/node_modules/crypto-random-string": { + "version": "1.0.0", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/cyclist": { + "version": "0.2.2", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "inBundle": true + }, + "node_modules/npm/node_modules/dashdash": { + "version": "1.14.1", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "inBundle": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "3.1.0", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "inBundle": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "inBundle": true + }, + "node_modules/npm/node_modules/debuglog": { + "version": "1.0.1", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/decamelize": { + "version": "1.2.0", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/decode-uri-component": { + "version": "0.2.0", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "inBundle": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/npm/node_modules/deep-extend": { + "version": "0.6.0", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "inBundle": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.3", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "inBundle": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/npm/node_modules/define-properties": { + "version": "1.1.3", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "inBundle": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/delayed-stream": { + "version": "1.0.0", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "inBundle": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "inBundle": true + }, + "node_modules/npm/node_modules/detect-indent": { + "version": "5.0.0", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/detect-newline": { + "version": "2.1.0", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/dezalgo": { + "version": "1.0.3", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "inBundle": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/dot-prop": { + "version": "4.2.1", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "inBundle": true, + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/dotenv": { + "version": "5.0.1", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==", + "inBundle": true, + "engines": { + "node": ">=4.6.0" + } + }, + "node_modules/npm/node_modules/duplexer3": { + "version": "0.1.4", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "inBundle": true + }, + "node_modules/npm/node_modules/duplexify": { + "version": "3.6.0", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "inBundle": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/npm/node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/ecc-jsbn": { + "version": "0.1.2", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "inBundle": true, + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/npm/node_modules/editor": { + "version": "1.0.0", + "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=", + "inBundle": true + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "7.0.3", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "inBundle": true + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.12", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "inBundle": true, + "dependencies": { + "iconv-lite": "~0.4.13" + } + }, + "node_modules/npm/node_modules/end-of-stream": { + "version": "1.4.1", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "inBundle": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.0", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "1.1.2", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", + "inBundle": true + }, + "node_modules/npm/node_modules/errno": { + "version": "0.1.7", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "inBundle": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "./cli.js" + } + }, + "node_modules/npm/node_modules/es-abstract": { + "version": "1.12.0", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "inBundle": true, + "dependencies": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/es-to-primitive": { + "version": "1.2.0", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "inBundle": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/es6-promise": { + "version": "4.2.8", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "inBundle": true + }, + "node_modules/npm/node_modules/es6-promisify": { + "version": "5.0.0", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "inBundle": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/npm/node_modules/escape-string-regexp": { + "version": "1.0.5", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "inBundle": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm/node_modules/execa": { + "version": "0.7.0", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "inBundle": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/execa/node_modules/get-stream": { + "version": "3.0.0", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/extend": { + "version": "3.0.2", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "inBundle": true + }, + "node_modules/npm/node_modules/extsprintf": { + "version": "1.3.0", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true + }, + "node_modules/npm/node_modules/fast-deep-equal": { + "version": "1.1.0", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "inBundle": true + }, + "node_modules/npm/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "inBundle": true + }, + "node_modules/npm/node_modules/figgy-pudding": { + "version": "3.5.1", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "inBundle": true + }, + "node_modules/npm/node_modules/find-npm-prefix": { + "version": "1.0.2", + "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==", + "inBundle": true + }, + "node_modules/npm/node_modules/flush-write-stream": { + "version": "1.0.3", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "inBundle": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + } + }, + "node_modules/npm/node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/forever-agent": { + "version": "0.6.1", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/form-data": { + "version": "2.3.2", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "inBundle": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/npm/node_modules/from2": { + "version": "2.3.0", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "inBundle": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/npm/node_modules/from2/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "1.2.7", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "inBundle": true, + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/npm/node_modules/fs-minipass/node_modules/minipass": { + "version": "2.9.0", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/npm/node_modules/fs-vacuum": { + "version": "1.2.10", + "integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "path-is-inside": "^1.0.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/npm/node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/npm/node_modules/fs-write-stream-atomic/node_modules/iferr": { + "version": "0.1.5", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "inBundle": true + }, + "node_modules/npm/node_modules/fs-write-stream-atomic/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/fs-write-stream-atomic/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "inBundle": true + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.1", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "inBundle": true + }, + "node_modules/npm/node_modules/gauge": { + "version": "2.7.4", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "inBundle": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/npm/node_modules/gauge/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "inBundle": true + }, + "node_modules/npm/node_modules/gauge/node_modules/string-width": { + "version": "1.0.2", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "inBundle": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/genfun": { + "version": "5.0.0", + "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", + "inBundle": true + }, + "node_modules/npm/node_modules/gentle-fs": { + "version": "2.3.1", + "integrity": "sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==", + "inBundle": true, + "dependencies": { + "aproba": "^1.1.2", + "chownr": "^1.1.2", + "cmd-shim": "^3.0.3", + "fs-vacuum": "^1.2.10", + "graceful-fs": "^4.1.11", + "iferr": "^0.1.5", + "infer-owner": "^1.0.4", + "mkdirp": "^0.5.1", + "path-is-inside": "^1.0.2", + "read-cmd-shim": "^1.0.1", + "slide": "^1.1.6" + } + }, + "node_modules/npm/node_modules/gentle-fs/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "inBundle": true + }, + "node_modules/npm/node_modules/gentle-fs/node_modules/iferr": { + "version": "0.1.5", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "inBundle": true + }, + "node_modules/npm/node_modules/get-caller-file": { + "version": "2.0.5", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "inBundle": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/npm/node_modules/get-stream": { + "version": "4.1.0", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "inBundle": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/getpass": { + "version": "0.1.7", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "inBundle": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "7.1.6", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "inBundle": true, + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/global-dirs": { + "version": "0.1.1", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "inBundle": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/got": { + "version": "6.7.1", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "inBundle": true, + "dependencies": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.4", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "inBundle": true + }, + "node_modules/npm/node_modules/har-schema": { + "version": "2.0.0", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/har-validator": { + "version": "5.1.0", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "deprecated": "this library is no longer supported", + "inBundle": true, + "dependencies": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/has": { + "version": "1.0.3", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "inBundle": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm/node_modules/has-flag": { + "version": "3.0.0", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/has-symbols": { + "version": "1.0.0", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "inBundle": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "inBundle": true + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "2.8.8", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "inBundle": true + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "3.8.1", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "inBundle": true + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "2.1.0", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "inBundle": true, + "dependencies": { + "agent-base": "4", + "debug": "3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/npm/node_modules/http-signature": { + "version": "1.2.0", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "inBundle": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "2.2.4", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "inBundle": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "inBundle": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.4.23", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "inBundle": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/iferr": { + "version": "1.0.2", + "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==", + "inBundle": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "3.0.3", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "inBundle": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/npm/node_modules/import-lazy": { + "version": "2.1.0", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "inBundle": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "inBundle": true + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "inBundle": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/ini": { + "version": "1.3.5", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "deprecated": "Please update to ini >=1.3.6 to avoid a prototype pollution issue", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "1.10.3", + "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", + "inBundle": true, + "dependencies": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "1.1.5", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "inBundle": true + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "2.1.0", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/is-callable": { + "version": "1.1.4", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "inBundle": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/is-ci": { + "version": "1.2.1", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "inBundle": true, + "dependencies": { + "ci-info": "^1.5.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/npm/node_modules/is-ci/node_modules/ci-info": { + "version": "1.6.0", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "inBundle": true + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "3.0.0", + "integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==", + "inBundle": true, + "dependencies": { + "cidr-regex": "^2.0.10" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/is-date-object": { + "version": "1.0.1", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "inBundle": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "inBundle": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-installed-globally": { + "version": "0.1.0", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "inBundle": true, + "dependencies": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/is-npm": { + "version": "1.0.0", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-obj": { + "version": "1.0.1", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-path-inside": { + "version": "1.0.1", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "inBundle": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-redirect": { + "version": "1.0.0", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-regex": { + "version": "1.0.4", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "inBundle": true, + "dependencies": { + "has": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/is-retry-allowed": { + "version": "1.2.0", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-stream": { + "version": "1.1.0", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/is-symbol": { + "version": "1.0.2", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "inBundle": true, + "dependencies": { + "has-symbols": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/is-typedarray": { + "version": "1.0.0", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "inBundle": true + }, + "node_modules/npm/node_modules/isarray": { + "version": "1.0.0", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "inBundle": true + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "inBundle": true + }, + "node_modules/npm/node_modules/isstream": { + "version": "0.1.2", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "inBundle": true + }, + "node_modules/npm/node_modules/jsbn": { + "version": "0.1.1", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "inBundle": true, + "optional": true + }, + "node_modules/npm/node_modules/json-parse-better-errors": { + "version": "1.0.2", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "inBundle": true + }, + "node_modules/npm/node_modules/json-schema": { + "version": "0.2.3", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "inBundle": true + }, + "node_modules/npm/node_modules/json-schema-traverse": { + "version": "0.3.1", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "inBundle": true + }, + "node_modules/npm/node_modules/json-stringify-safe": { + "version": "5.0.1", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "inBundle": true + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true + }, + "node_modules/npm/node_modules/JSONStream": { + "version": "1.3.5", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "inBundle": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "./bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/jsprim": { + "version": "1.4.1", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/npm/node_modules/latest-version": { + "version": "3.1.0", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "inBundle": true, + "dependencies": { + "package-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/lazy-property": { + "version": "1.0.0", + "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=", + "inBundle": true + }, + "node_modules/npm/node_modules/libcipm": { + "version": "4.0.8", + "integrity": "sha512-IN3hh2yDJQtZZ5paSV4fbvJg4aHxCCg5tcZID/dSVlTuUiWktsgaldVljJv6Z5OUlYspx6xQkbR0efNodnIrOA==", + "inBundle": true, + "dependencies": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "graceful-fs": "^4.1.11", + "ini": "^1.3.5", + "lock-verify": "^2.1.0", + "mkdirp": "^0.5.1", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "pacote": "^9.1.0", + "read-package-json": "^2.0.13", + "rimraf": "^2.6.2", + "worker-farm": "^1.6.0" + } + }, + "node_modules/npm/node_modules/libnpm": { + "version": "3.0.1", + "integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==", + "inBundle": true, + "dependencies": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.3", + "find-npm-prefix": "^1.0.2", + "libnpmaccess": "^3.0.2", + "libnpmconfig": "^1.2.1", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmpublish": "^1.1.2", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "lock-verify": "^2.0.2", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "npm-profile": "^4.0.2", + "npm-registry-fetch": "^4.0.0", + "npmlog": "^4.1.2", + "pacote": "^9.5.3", + "read-package-json": "^2.0.13", + "stringify-package": "^1.0.0" + } + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "3.0.2", + "integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==", + "inBundle": true, + "dependencies": { + "aproba": "^2.0.0", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/libnpmconfig": { + "version": "1.2.1", + "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", + "inBundle": true, + "dependencies": { + "figgy-pudding": "^3.5.1", + "find-up": "^3.0.0", + "ini": "^1.3.5" + } + }, + "node_modules/npm/node_modules/libnpmconfig/node_modules/find-up": { + "version": "3.0.0", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "inBundle": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/libnpmconfig/node_modules/locate-path": { + "version": "3.0.0", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "inBundle": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/libnpmconfig/node_modules/p-limit": { + "version": "2.2.0", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "inBundle": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/libnpmconfig/node_modules/p-locate": { + "version": "3.0.0", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "inBundle": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/libnpmconfig/node_modules/p-try": { + "version": "2.2.0", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "5.0.3", + "integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==", + "inBundle": true, + "dependencies": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "1.0.1", + "integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==", + "inBundle": true, + "dependencies": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "1.1.2", + "integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==", + "inBundle": true, + "dependencies": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0", + "semver": "^5.5.1", + "ssri": "^6.0.1" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "2.0.2", + "integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==", + "inBundle": true, + "dependencies": { + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "1.0.2", + "integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==", + "inBundle": true, + "dependencies": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/libnpx": { + "version": "10.2.4", + "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", + "inBundle": true, + "dependencies": { + "dotenv": "^5.0.1", + "npm-package-arg": "^6.0.0", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.0", + "update-notifier": "^2.3.0", + "which": "^1.3.0", + "y18n": "^4.0.0", + "yargs": "^14.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/lock-verify": { + "version": "2.1.0", + "integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==", + "inBundle": true, + "dependencies": { + "npm-package-arg": "^6.1.0", + "semver": "^5.4.1" + } + }, + "node_modules/npm/node_modules/lockfile": { + "version": "1.0.4", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "inBundle": true, + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/npm/node_modules/lodash._baseindexof": { + "version": "3.1.0", + "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash._baseuniq": { + "version": "4.6.0", + "integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=", + "inBundle": true, + "dependencies": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "node_modules/npm/node_modules/lodash._bindcallback": { + "version": "3.0.1", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash._cacheindexof": { + "version": "3.0.2", + "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash._createcache": { + "version": "3.1.2", + "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", + "inBundle": true, + "dependencies": { + "lodash._getnative": "^3.0.0" + } + }, + "node_modules/npm/node_modules/lodash._createset": { + "version": "4.0.3", + "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash._getnative": { + "version": "3.9.1", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash._root": { + "version": "3.0.1", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash.clonedeep": { + "version": "4.5.0", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash.restparam": { + "version": "3.6.1", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash.union": { + "version": "4.6.0", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash.uniq": { + "version": "4.5.0", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "inBundle": true + }, + "node_modules/npm/node_modules/lodash.without": { + "version": "4.4.0", + "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=", + "inBundle": true + }, + "node_modules/npm/node_modules/lowercase-keys": { + "version": "1.0.1", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "5.1.1", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "inBundle": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/npm/node_modules/make-dir": { + "version": "1.3.0", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "inBundle": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "5.0.2", + "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "inBundle": true, + "dependencies": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "node_modules/npm/node_modules/meant": { + "version": "1.0.2", + "integrity": "sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==", + "inBundle": true + }, + "node_modules/npm/node_modules/mime-db": { + "version": "1.35.0", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "inBundle": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/mime-types": { + "version": "2.1.19", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "inBundle": true, + "dependencies": { + "mime-db": "~1.35.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "3.0.4", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "inBundle": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/minimist": { + "version": "1.2.5", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "inBundle": true + }, + "node_modules/npm/node_modules/minizlib": { + "version": "1.3.3", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "inBundle": true, + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "2.9.0", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/npm/node_modules/mississippi": { + "version": "3.0.0", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "inBundle": true, + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "0.5.5", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "inBundle": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/npm/node_modules/mkdirp/node_modules/minimist": { + "version": "1.2.5", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "inBundle": true + }, + "node_modules/npm/node_modules/move-concurrently": { + "version": "1.0.1", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "inBundle": true, + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/npm/node_modules/move-concurrently/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "inBundle": true + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.1", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "inBundle": true + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "0.0.7", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "inBundle": true + }, + "node_modules/npm/node_modules/node-fetch-npm": { + "version": "2.0.2", + "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", + "inBundle": true, + "dependencies": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "5.1.0", + "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", + "inBundle": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", + "tar": "^4.4.12", + "which": "^1.3.1" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "4.0.3", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "inBundle": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "2.5.0", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "inBundle": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm/node_modules/normalize-package-data/node_modules/resolve": { + "version": "1.10.0", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "inBundle": true, + "dependencies": { + "path-parse": "^1.0.6" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "1.3.3", + "integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==", + "inBundle": true, + "dependencies": { + "cli-table3": "^0.5.0", + "console-control-strings": "^1.1.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "1.1.1", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "inBundle": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm/node_modules/npm-cache-filename": { + "version": "1.0.2", + "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=", + "inBundle": true + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "3.0.2", + "integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==", + "inBundle": true, + "dependencies": { + "semver": "^2.3.0 || 3.x || 4 || 5" + } + }, + "node_modules/npm/node_modules/npm-lifecycle": { + "version": "3.1.5", + "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", + "inBundle": true, + "dependencies": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.15", + "node-gyp": "^5.0.2", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + } + }, + "node_modules/npm/node_modules/npm-logical-tree": { + "version": "1.2.1", + "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==", + "inBundle": true + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "inBundle": true + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "6.1.1", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "inBundle": true, + "dependencies": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "1.4.8", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "inBundle": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "3.0.2", + "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", + "inBundle": true, + "dependencies": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "4.0.4", + "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", + "inBundle": true, + "dependencies": { + "aproba": "^1.1.2 || 2", + "figgy-pudding": "^3.4.1", + "npm-registry-fetch": "^4.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "4.0.7", + "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", + "inBundle": true, + "dependencies": { + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "JSONStream": "^1.3.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/safe-buffer": { + "version": "5.2.1", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true + }, + "node_modules/npm/node_modules/npm-run-path": { + "version": "2.0.2", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "inBundle": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "1.0.0", + "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=", + "inBundle": true + }, + "node_modules/npm/node_modules/npmlog": { + "version": "4.1.2", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "inBundle": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/npm/node_modules/number-is-nan": { + "version": "1.0.1", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/oauth-sign": { + "version": "0.9.0", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/object-assign": { + "version": "4.1.1", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/object-keys": { + "version": "1.0.12", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "inBundle": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/object.getownpropertydescriptors": { + "version": "2.0.3", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "inBundle": true, + "dependencies": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "inBundle": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/opener": { + "version": "1.5.1", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "inBundle": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/npm/node_modules/os-homedir": { + "version": "1.0.2", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/os-tmpdir": { + "version": "1.0.2", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/osenv": { + "version": "0.1.5", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "inBundle": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/npm/node_modules/p-finally": { + "version": "1.0.0", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/package-json": { + "version": "4.0.1", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "inBundle": true, + "dependencies": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "9.5.12", + "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", + "inBundle": true, + "dependencies": { + "bluebird": "^3.5.3", + "cacache": "^12.0.2", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.3", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.12", + "npm-pick-manifest": "^3.0.0", + "npm-registry-fetch": "^4.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.6.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + } + }, + "node_modules/npm/node_modules/pacote/node_modules/minipass": { + "version": "2.9.0", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/npm/node_modules/parallel-transform": { + "version": "1.1.0", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "inBundle": true, + "dependencies": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/npm/node_modules/parallel-transform/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/parallel-transform/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/path-exists": { + "version": "3.0.0", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/path-is-inside": { + "version": "1.0.2", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "inBundle": true + }, + "node_modules/npm/node_modules/path-key": { + "version": "2.0.1", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/path-parse": { + "version": "1.0.6", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "inBundle": true + }, + "node_modules/npm/node_modules/performance-now": { + "version": "2.1.0", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "inBundle": true + }, + "node_modules/npm/node_modules/pify": { + "version": "3.0.0", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/prepend-http": { + "version": "1.0.4", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/process-nextick-args": { + "version": "2.0.0", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "inBundle": true + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "inBundle": true + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "1.1.1", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "inBundle": true, + "dependencies": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/npm/node_modules/promise-retry/node_modules/retry": { + "version": "0.10.1", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "0.3.0", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "inBundle": true, + "dependencies": { + "read": "1" + } + }, + "node_modules/npm/node_modules/proto-list": { + "version": "1.2.4", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "inBundle": true + }, + "node_modules/npm/node_modules/protoduck": { + "version": "5.0.1", + "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "inBundle": true, + "dependencies": { + "genfun": "^5.0.0" + } + }, + "node_modules/npm/node_modules/prr": { + "version": "1.0.1", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "inBundle": true + }, + "node_modules/npm/node_modules/pseudomap": { + "version": "1.0.2", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "inBundle": true + }, + "node_modules/npm/node_modules/psl": { + "version": "1.1.29", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "inBundle": true + }, + "node_modules/npm/node_modules/pump": { + "version": "3.0.0", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "inBundle": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/npm/node_modules/pumpify": { + "version": "1.5.1", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "inBundle": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/npm/node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "inBundle": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/npm/node_modules/punycode": { + "version": "1.4.1", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "inBundle": true + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "inBundle": true, + "bin": { + "qrcode-terminal": "./bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/qs": { + "version": "6.5.2", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "inBundle": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/npm/node_modules/query-string": { + "version": "6.8.2", + "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", + "inBundle": true, + "dependencies": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/qw": { + "version": "1.0.1", + "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=", + "inBundle": true + }, + "node_modules/npm/node_modules/rc": { + "version": "1.2.8", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "inBundle": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "./cli.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "1.0.7", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "inBundle": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "1.0.5", + "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/npm/node_modules/read-installed": { + "version": "4.0.3", + "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", + "inBundle": true, + "dependencies": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "2.1.1", + "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", + "inBundle": true, + "dependencies": { + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "json-parse-better-errors": "^1.0.1", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/npm/node_modules/read-package-tree": { + "version": "5.3.1", + "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", + "inBundle": true, + "dependencies": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.0", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "inBundle": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "inBundle": true, + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/npm/node_modules/registry-auth-token": { + "version": "3.4.0", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "inBundle": true, + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/npm/node_modules/registry-url": { + "version": "3.1.0", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "inBundle": true, + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/request": { + "version": "2.88.0", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "inBundle": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/require-directory": { + "version": "2.1.1", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/require-main-filename": { + "version": "2.0.0", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "inBundle": true + }, + "node_modules/npm/node_modules/resolve-from": { + "version": "4.0.0", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "inBundle": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "2.7.1", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "inBundle": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "./bin.js" + } + }, + "node_modules/npm/node_modules/run-queue": { + "version": "1.0.3", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "inBundle": true, + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/npm/node_modules/run-queue/node_modules/aproba": { + "version": "1.2.0", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "inBundle": true + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.1.2", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "inBundle": true + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "inBundle": true + }, + "node_modules/npm/node_modules/semver": { + "version": "5.7.1", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "inBundle": true, + "bin": { + "semver": "./bin/semver" + } + }, + "node_modules/npm/node_modules/semver-diff": { + "version": "2.1.0", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "inBundle": true, + "dependencies": { + "semver": "^5.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "inBundle": true + }, + "node_modules/npm/node_modules/sha": { + "version": "3.0.0", + "integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "1.2.0", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "inBundle": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "1.0.0", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "3.0.2", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "inBundle": true + }, + "node_modules/npm/node_modules/slide": { + "version": "1.1.6", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.1.0", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "inBundle": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.3.3", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "inBundle": true, + "dependencies": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "4.0.2", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "inBundle": true, + "dependencies": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "4.2.1", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "inBundle": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/npm/node_modules/sorted-object": { + "version": "2.0.1", + "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=", + "inBundle": true + }, + "node_modules/npm/node_modules/sorted-union-stream": { + "version": "2.1.3", + "integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=", + "inBundle": true, + "dependencies": { + "from2": "^1.3.0", + "stream-iterate": "^1.1.0" + } + }, + "node_modules/npm/node_modules/sorted-union-stream/node_modules/from2": { + "version": "1.3.0", + "integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=", + "inBundle": true, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.10" + } + }, + "node_modules/npm/node_modules/sorted-union-stream/node_modules/isarray": { + "version": "0.0.1", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "inBundle": true + }, + "node_modules/npm/node_modules/sorted-union-stream/node_modules/readable-stream": { + "version": "1.1.14", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "inBundle": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/npm/node_modules/sorted-union-stream/node_modules/string_decoder": { + "version": "0.10.31", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "inBundle": true + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.0.0", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "inBundle": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.1.0", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "inBundle": true + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.0", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "inBundle": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.5", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "inBundle": true + }, + "node_modules/npm/node_modules/split-on-first": { + "version": "1.1.0", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/sshpk": { + "version": "1.14.2", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "inBundle": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + }, + "optionalDependencies": { + "bcrypt-pbkdf": "^1.0.0", + "ecc-jsbn": "~0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "node_modules/npm/node_modules/ssri": { + "version": "6.0.1", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "inBundle": true, + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/npm/node_modules/stream-each": { + "version": "1.2.2", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "inBundle": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/npm/node_modules/stream-iterate": { + "version": "1.2.0", + "integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=", + "inBundle": true, + "dependencies": { + "readable-stream": "^2.1.5", + "stream-shift": "^1.0.0" + } + }, + "node_modules/npm/node_modules/stream-iterate/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/stream-iterate/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/stream-shift": { + "version": "1.0.0", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "inBundle": true + }, + "node_modules/npm/node_modules/strict-uri-encode": { + "version": "2.0.0", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.0", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "inBundle": true + }, + "node_modules/npm/node_modules/string-width": { + "version": "2.1.1", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "inBundle": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.0", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "inBundle": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/stringify-package": { + "version": "1.0.1", + "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==", + "inBundle": true + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "3.0.1", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "inBundle": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/strip-eof": { + "version": "1.0.0", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "5.4.0", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "inBundle": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "4.4.13", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "inBundle": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "2.9.0", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/npm/node_modules/term-size": { + "version": "1.2.0", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "inBundle": true, + "dependencies": { + "execa": "^0.7.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "inBundle": true + }, + "node_modules/npm/node_modules/through": { + "version": "2.3.8", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "inBundle": true + }, + "node_modules/npm/node_modules/through2": { + "version": "2.0.3", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "inBundle": true, + "dependencies": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "node_modules/npm/node_modules/through2/node_modules/readable-stream": { + "version": "2.3.6", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "inBundle": true, + "dependencies": { + "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" + } + }, + "node_modules/npm/node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "inBundle": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/npm/node_modules/timed-out": { + "version": "4.0.1", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==", + "inBundle": true + }, + "node_modules/npm/node_modules/tough-cookie": { + "version": "2.4.3", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "inBundle": true, + "dependencies": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/tunnel-agent": { + "version": "0.6.0", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "inBundle": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/tweetnacl": { + "version": "0.14.5", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "inBundle": true, + "optional": true + }, + "node_modules/npm/node_modules/typedarray": { + "version": "0.0.6", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "inBundle": true + }, + "node_modules/npm/node_modules/uid-number": { + "version": "0.0.6", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "inBundle": true, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/umask": { + "version": "1.1.0", + "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", + "inBundle": true + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "1.1.1", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "inBundle": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "2.0.0", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "inBundle": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/npm/node_modules/unique-string": { + "version": "1.0.0", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "inBundle": true, + "dependencies": { + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/unpipe": { + "version": "1.0.0", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "inBundle": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/npm/node_modules/unzip-response": { + "version": "2.0.1", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/update-notifier": { + "version": "2.5.0", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "inBundle": true, + "dependencies": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/url-parse-lax": { + "version": "1.0.0", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "inBundle": true, + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "inBundle": true + }, + "node_modules/npm/node_modules/util-extend": { + "version": "1.0.3", + "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=", + "inBundle": true + }, + "node_modules/npm/node_modules/util-promisify": { + "version": "2.1.0", + "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "inBundle": true, + "dependencies": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node_modules/npm/node_modules/uuid": { + "version": "3.3.3", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "inBundle": true, + "bin": { + "uuid": "./bin/uuid" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "inBundle": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "3.0.0", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "inBundle": true, + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/npm/node_modules/verror": { + "version": "1.10.0", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "inBundle": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "1.3.1", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "inBundle": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "./bin/which" + } + }, + "node_modules/npm/node_modules/which-module": { + "version": "2.0.0", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "inBundle": true + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.2", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "inBundle": true, + "dependencies": { + "string-width": "^1.0.2" + } + }, + "node_modules/npm/node_modules/wide-align/node_modules/string-width": { + "version": "1.0.2", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "inBundle": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/widest-line": { + "version": "2.0.1", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "inBundle": true, + "dependencies": { + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/worker-farm": { + "version": "1.7.0", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "inBundle": true, + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "5.1.0", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "inBundle": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "inBundle": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "inBundle": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "inBundle": true + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "2.4.3", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "inBundle": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/npm/node_modules/xdg-basedir": { + "version": "3.0.0", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/xtend": { + "version": "4.0.1", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "inBundle": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/npm/node_modules/y18n": { + "version": "4.0.0", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "inBundle": true + }, + "node_modules/npm/node_modules/yallist": { + "version": "3.0.3", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "inBundle": true + }, + "node_modules/npm/node_modules/yargs": { + "version": "14.2.3", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "inBundle": true, + "dependencies": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "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": "^15.0.1" + } + }, + "node_modules/npm/node_modules/yargs-parser": { + "version": "15.0.1", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "inBundle": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/npm/node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "inBundle": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "inBundle": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/locate-path": { + "version": "3.0.0", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "inBundle": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "funding": "https://github.com/sponsors/sindresorhus", + "inBundle": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/p-locate": { + "version": "3.0.0", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "inBundle": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/p-try": { + "version": "2.2.0", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "inBundle": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "inBundle": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "inBundle": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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==", + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/nyc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.1.tgz", + "integrity": "sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "funding": "https://github.com/chalk/ansi-styles?sponsor=1", + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/node_modules/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 + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "node_modules/object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/observable-fns": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.5.1.tgz", + "integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==" + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-backend": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-2.4.1.tgz", + "integrity": "sha512-48j8QhDD9sfV6t7Zgn9JrfJtCpJ53bmoT2bzXYYig1HhG/Xn0Aa5fJhM0cQSZq9nq78/XbU7RDEa3e+IADNkmA==", + "dependencies": { + "ajv": "^6.10.0", + "bath-es5": "^3.0.3", + "cookie": "^0.4.0", + "lodash": "^4.17.15", + "mock-json-schema": "^1.0.5", + "openapi-schema-validation": "^0.4.2", + "openapi-types": "^1.3.4", + "qs": "^6.6.0", + "swagger-parser": "^9.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/openapi-backend/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openapi-schema-validation": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", + "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", + "dependencies": { + "jsonschema": "1.2.4", + "jsonschema-draft4": "^1.0.0", + "swagger-schema-official": "2.0.0-bab6bed" + } + }, + "node_modules/openapi-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", + "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" + }, + "node_modules/optional-js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/optional-js/-/optional-js-2.1.1.tgz", + "integrity": "sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA==" + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "funding": "https://github.com/sponsors/sindresorhus", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/pg": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", + "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.4.0", + "pg-pool": "^3.2.2", + "pg-protocol": "^1.4.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "pg-native": ">=2.0.0" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-connection-string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", + "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", + "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", + "dependencies": { + "split2": "^3.1.1" + } + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "funding": "https://github.com/sponsors/jonschlinkert", + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", + "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", + "dependencies": { + "is-promise": "~1" + } + }, + "node_modules/property-information": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.5.0.tgz", + "integrity": "sha512-RgEbCx2HLa1chNgvChcx+rrCWD0ctBmGSE0M7lVm1yyv4UbvbrWoXp/BkVLZefzjrRBGW8/Js6uh/BnlHXFyjA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rate-limiter-flexible": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.1.4.tgz", + "integrity": "sha512-wtbWcqZbCqyAO1k63moagJlCZuPCEqbJJ6il1y2JVoiUyxlE36+cM7ETta9K6tTom9O5pNK+CxwHMgyyyJ31Gg==" + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/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==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dependencies": { + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", + "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "dependencies": { + "denque": "^1.4.1", + "redis-commands": "^1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-redis" + } + }, + "node_modules/redis-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true, + "funding": "https://github.com/sponsors/mysticatea", + "engines": { + "node": ">=8" + } + }, + "node_modules/rehype": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-10.0.0.tgz", + "integrity": "sha512-0W8M4Y91b2QuzDSTjkZgBOJo79bP089YbSQNPMqebuUVrp6iveoi+Ra6/H7fJwUxq8FCHGCGzkLaq3fvO9XnVg==", + "dependencies": { + "rehype-parse": "^6.0.0", + "rehype-stringify": "^6.0.0", + "unified": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-4.0.5.tgz", + "integrity": "sha512-QC3Z+bZ5wbv+jGYQewpAAYhXhzuH/TVRx7z08rurBmh9AbG8Nu8oJnvs9LWj43Fd/C7UIhXoQ7Wddgt+ThWK5g==", + "dependencies": { + "hast-util-embedded": "^1.0.0", + "hast-util-is-element": "^1.0.0", + "hast-util-whitespace": "^1.0.4", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-minify-whitespace/node_modules/unist-util-is": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", + "integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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==", + "dependencies": { + "hast-util-from-parse5": "^5.0.0", + "parse5": "^5.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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==", + "dependencies": { + "hast-util-to-html": "^6.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==" + }, + "node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/rethinkdb": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", + "integrity": "sha512-6DzwqEpFc8cqesAdo07a845oBRxLiHvWzopTKBo/uY2ypGWIsJQFJk3wjRDtSEhczxJqLS0jnf37rwgzYAw8NQ==", + "dependencies": { + "bluebird": ">= 2.3.2 < 3" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/security": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/security/-/security-1.0.0.tgz", + "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=", + "engines": { + "node": "*" + } + }, + "node_modules/semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "bin": { + "semver": "./bin/semver" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/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==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/set-cookie-parser": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz", + "integrity": "sha512-mNCnTUF0OYPwYzSHbdRdCfNNHqrne+HS5tS5xNb6yJbdP9wInV0q5xPLE0EyfV/Q3tImo3y/OXpD8Jn0Jtnjrg==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/simple-git": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.27.0.tgz", + "integrity": "sha512-/Q4aolzErYrIx6SgyH421jmtv5l1DaAw+KYWMWy229+isW6yld/nHGxJ2xUR/aeX3SuYJnbucyUigERwaw4Xow==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.1" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sinon": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.0.tgz", + "integrity": "sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.2.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/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=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "engines": { + "node": "*" + } + }, + "node_modules/socket.io": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "dependencies": { + "debug": "~4.1.0", + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + }, + "node_modules/socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "dependencies": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/socket.io-client/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-client/node_modules/socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "dependencies": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + } + }, + "node_modules/socket.io-client/node_modules/socket.io-parser/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/socket.io-client/node_modules/socket.io-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/socket.io-parser": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "dependencies": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/socket.io-parser/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "./bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "dependencies": { + "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" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/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=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/superagent/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/superagent/node_modules/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 + }, + "node_modules/superagent/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/superagent/node_modules/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 + }, + "node_modules/superagent/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/supertest/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/supertest/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/supertest/node_modules/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 + }, + "node_modules/supertest/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "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" + } + }, + "node_modules/supertest/node_modules/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 + }, + "node_modules/supertest/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/supertest/node_modules/superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==", + "dependencies": { + "@apidevtools/swagger-parser": "9.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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=" + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/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, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/table/node_modules/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=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "dev": true, + "dependencies": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "node_modules/tarn": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.5.tgz", + "integrity": "sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/tedious": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-6.7.0.tgz", + "integrity": "sha512-8qr7+sB0h4SZVQBRWUgHmYuOEflAOl2eihvxk0fVNvpvGJV4V5UC/YmSvebyfgyfwWcPO22/AnSbYVZZqf9wuQ==", + "dependencies": { + "@azure/ms-rest-nodeauth": "2.0.2", + "@types/node": "^12.12.17", + "@types/readable-stream": "^2.3.5", + "bl": "^3.0.0", + "depd": "^2.0.0", + "iconv-lite": "^0.5.0", + "jsbi": "^3.1.1", + "native-duplexpair": "^1.0.0", + "punycode": "^2.1.0", + "readable-stream": "^3.4.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tedious/node_modules/@types/node": { + "version": "12.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz", + "integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==" + }, + "node_modules/tedious/node_modules/bl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.1.tgz", + "integrity": "sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ==", + "dependencies": { + "readable-stream": "^3.0.1" + } + }, + "node_modules/tedious/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tedious/node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tedious/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "node_modules/terser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/threads": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.4.1.tgz", + "integrity": "sha512-LSgGCu2lwdrfqjYWmeqO+7fgxAbUtjlsa7UA5J6r4x8fCoMd015h19rMwXqz4/q8l3svdloE36Of41rpZWiYFg==", + "dependencies": { + "callsites": "^3.1.0", + "debug": "^4.1.1", + "is-observable": "^1.1.0", + "observable-fns": "^0.5.1", + "tiny-worker": ">= 2" + }, + "optionalDependencies": { + "tiny-worker": ">= 2" + } + }, + "node_modules/threads/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/threads/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "dependencies": { + "esm": "^3.2.25" + } + }, + "node_modules/tinycon": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.0.1.tgz", + "integrity": "sha1-beEM1SGaHxIdmgokssEbP7JN/+0=", + "engines": { + "node": "*" + } + }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/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==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/ueberdb2": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.6.tgz", + "integrity": "sha512-stLhNkWlxUMAO33JjEh8JCRuZvHYeDQjbo6K1C3I7R37AlMKNu9GWXSZm1wQDnAqpXAXeMVh3owBsAdj0YvOrg==", + "dependencies": { + "async": "^3.2.0", + "cassandra-driver": "^4.5.1", + "chai": "^4.2.0", + "channels": "0.0.4", + "cli-table": "^0.3.1", + "dirty": "^1.1.0", + "elasticsearch": "^16.7.1", + "mocha": "^7.1.2", + "mssql": "^6.2.3", + "mysql": "2.18.1", + "nano": "^8.2.2", + "pg": "^8.0.3", + "randexp": "^0.5.3", + "redis": "^3.0.2", + "rethinkdb": "^2.4.2", + "rimraf": "^3.0.2", + "simple-git": "^2.4.0" + } + }, + "node_modules/ueberdb2/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/ueberdb2/node_modules/mocha": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", + "dependencies": { + "ansi-colors": "3.2.3", + "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", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.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", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ueberdb2/node_modules/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==" + }, + "node_modules/ueberdb2/node_modules/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==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "node_modules/unified": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.0.0.tgz", + "integrity": "sha512-ssFo33gljU3PdlWLjNp15Inqb77d6JnJSfyplGJPT/a+fNRNyCBeveBAYJdO5khKdF6WVHa/yYCC7Xl6BDwZUQ==", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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==" + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unorm": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", + "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "bin": { + "uuid": "./bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "node_modules/validator": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", + "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vargs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", + "integrity": "sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=", + "dev": true, + "engines": { + "node": ">=0.1.93" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vfile": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.1.tgz", + "integrity": "sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/wd": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/wd/-/wd-1.12.1.tgz", + "integrity": "sha512-O99X8OnOgkqfmsPyLIRzG9LmZ+rjmdGFBCyhGpnsSL4MB4xzHoeWmSVcumDiQ5QqPZcwGkszTgeJvjk2VjtiNw==", + "dev": true, + "engines": [ + "node" + ], + "hasInstallScript": true, + "dependencies": { + "archiver": "^3.0.0", + "async": "^2.0.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.1", + "q": "^1.5.1", + "request": "2.88.0", + "vargs": "^0.1.0" + }, + "bin": { + "wd": "lib/bin.js" + } + }, + "node_modules/wd/node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/wd/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/wd/node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/wd/node_modules/request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/wd/node_modules/tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "dependencies": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/web-namespaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", + "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "./bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/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=", + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/wrap-ansi/node_modules/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=", + "engines": { + "node": ">=4" + } + }, + "node_modules/wrap-ansi/node_modules/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==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmldom": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz", + "integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xpath.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", + "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "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" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dependencies": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/yargs/node_modules/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=", + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs/node_modules/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==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, + "node_modules/z-schema": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz", + "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==", + "dependencies": { + "commander": "^2.7.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^12.0.0" + }, + "bin": { + "z-schema": "./bin/z-schema" + }, + "engines": { + "node": ">=6.0.0" + }, + "optionalDependencies": { + "commander": "^2.7.1" + } + }, + "node_modules/zip-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", + "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^2.1.1", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6" + } + } + }, "dependencies": { "@apidevtools/json-schema-ref-parser": { "version": "8.0.0", @@ -44,9 +12197,9 @@ "integrity": "sha512-l7z0DPCi2Hp88w12JhDTtx5d0Y3+vhfE7JKJb9O7sEz71Cwp053N8piTtTnnk/tUor9oZHgEKi/p3tQQmLPjvA==" }, "@azure/ms-rest-js": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.8.15.tgz", - "integrity": "sha512-kIB71V3DcrA4iysBbOsYcxd4WWlOE7OFtCUYNfflPODM0lbIR23A236QeTn5iAeYwcHmMjR/TAKp5KQQh/WqoQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.9.0.tgz", + "integrity": "sha512-cB4Z2Mg7eBmet1rfbf0QSO1XbhfknRW7B+mX3IHJq0KGHaGJvCPoVTgdsJdCkazEMK1jtANFNEDDzSQacxyzbA==", "requires": { "@types/tunnel": "0.0.0", "axios": "^0.19.0", @@ -367,6 +12520,68 @@ "to-fast-properties": "^2.0.0" } }, + "@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "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" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "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 + }, + "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 + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -435,11 +12650,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -454,6 +12669,51 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@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==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", + "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -471,9 +12731,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "14.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.10.1.tgz", - "integrity": "sha512-aYNbO+FZ/3KGeQCEkNhHFRIzBOUgc7QvcVNKXbfnhDkSfwUv91JsQQa10rDgKSTSLkXZ1UIyPe4FJJNVgw1xWQ==" + "version": "14.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz", + "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==" }, "@types/readable-stream": { "version": "2.3.9", @@ -534,6 +12794,19 @@ "negotiator": "0.6.2" } }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "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==", + "dev": true, + "requires": {} + }, "adal-node": { "version": "0.1.28", "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", @@ -551,9 +12824,9 @@ }, "dependencies": { "@types/node": { - "version": "8.10.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.63.tgz", - "integrity": "sha512-g+nSkeHFDd2WOQChfmy9SAXLywT47WZBrGS/NC5ym5PJ8c8RC6l4pbGaUW/X0+eZJnXw6/AVNEouXWhV4iz72Q==" + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" } } }, @@ -602,14 +12875,17 @@ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, "anymatch": { "version": "3.1.1", @@ -758,11 +13034,6 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "array-iterate": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-1.1.4.tgz", - "integrity": "sha512-sNRaPGh9nnmdC8Zf+pT3UqP8rnWj5Hf9wiFGsX3wUQ2yVSIhO2ShFwCoceIPpB41QF6i2OEmrHmCo36xronCVA==" - }, "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", @@ -786,6 +13057,12 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", @@ -882,9 +13159,9 @@ "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" }, "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" }, "binary-search": { "version": "1.3.6", @@ -892,9 +13169,9 @@ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -1028,6 +13305,15 @@ "write-file-atomic": "^3.0.0" } }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -1054,9 +13340,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "cassandra-driver": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.0.tgz", - "integrity": "sha512-OCYJ3Zuy2La0qf+7dfeYlG3X4C0ns1cbPu0hhpC3xyUWqTy1Ai9a4mlQjSBqbcHi51gdFB5UbS0fWJzZIY6NXw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.1.tgz", + "integrity": "sha512-Vk0kUHlMV4vFXRPwRpKnCZEEMZkp9/RucBDB7gpaUmn9sCusKzzUzVkXeusTxKSoGuIgLJJ7YBiFJdXOctUS7A==", "requires": { "@types/long": "^4.0.0", "@types/node": ">=8", @@ -1083,15 +13369,13 @@ } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "channels": { @@ -1167,11 +13451,12 @@ "dev": true }, "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.4.tgz", + "integrity": "sha512-1vinpnX/ZERcmE443i3SZTmU5DF0rPO9DrL4I2iVAllhxzCM9SzPlHnz19fsZB78htkKZvYBvj6SZ6vXnaxmTA==", "requires": { - "colors": "1.0.3" + "chalk": "^2.4.1", + "string-width": "^4.2.0" } }, "cliui": { @@ -1189,6 +13474,16 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -1220,11 +13515,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -1234,11 +13529,6 @@ } } }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==" - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1252,11 +13542,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1516,6 +13801,12 @@ "type-detect": "^4.0.0" } }, + "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 + }, "default-require-extensions": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", @@ -1563,6 +13854,15 @@ "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz", "integrity": "sha1-cO3SuZlUHcmXT9Ooy9DGcP4jYHg=" }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", @@ -1627,19 +13927,56 @@ "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" }, "elasticsearch": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-16.7.1.tgz", - "integrity": "sha512-PL/BxB03VGbbghJwISYvVcrR9KbSSkuQ7OM//jHJg/End/uC2fvXg4QI7RXLvCGbhBuNQ8dPue7DOOPra73PCw==", + "version": "16.7.2", + "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-16.7.2.tgz", + "integrity": "sha512-1ZLKZlG2ABfYVBX2d7/JgxOsKJrM5Yu62GvshWu7ZSvhxPomCN4Gas90DS51yYI56JolY0XGhyiRlUhLhIL05Q==", "requires": { "agentkeepalive": "^3.4.1", "chalk": "^1.0.0", "lodash": "^4.17.10" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "encodeurl": { "version": "1.0.2", @@ -1741,6 +14078,23 @@ "has-binary2": "~1.0.2" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + } + } + }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -1752,21 +14106,35 @@ "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" }, "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^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==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "es-to-primitive": { @@ -1795,21 +14163,369 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz", + "integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "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.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "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.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.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "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": "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" + } + } + } + }, + "eslint-config-etherpad": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.20.tgz", + "integrity": "sha512-dDEmWphxOmYe7XC0Uevzb0lK7o1jDBGwYMMCdNeZlgo2EfJljnijPgodlimM4R+4OsnfegEMY6rdWoXjzdd5Rw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, + "eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, + "eslint-plugin-mocha": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz", + "integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.1.0", + "ramda": "^0.27.1" + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-prefer-arrow": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz", + "integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==", + "dev": true, + "requires": {} + }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, + "eslint-plugin-you-dont-need-lodash-underscore": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.10.0.tgz", + "integrity": "sha512-Zu1KbHiWKf+alVvT+kFX2M5HW1gmtnkfF1l2cjmFozMnG0gbGgXo8oqK7lwk+ygeOXDmVfOyijqBd7SUub9AEQ==", + "dev": true, + "requires": { + "kebab-case": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "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": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "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 + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "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=", + "dev": true, + "requires": { + "async": "*", + "socket.io-client": "*" + } + }, "etherpad-require-kernel": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz", @@ -1921,6 +14637,21 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1968,13 +14699,29 @@ } }, "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", + "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", "requires": { "is-buffer": "~2.0.3" } }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", @@ -2061,6 +14808,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -2077,6 +14830,16 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2150,6 +14913,13 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + } } }, "has-binary2": { @@ -2193,11 +14963,18 @@ } }, "hast-util-embedded": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-1.0.5.tgz", - "integrity": "sha512-0FfLHmfArWOizbdwjL+Rc9QIBzqP80juicNl4S4NEPq5OYWBCgYrtYDPUDoSyQQ9IQlBn9W7++fpYQNzZSq/wQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-1.0.6.tgz", + "integrity": "sha512-JQMW+TJe0UAIXZMjCJ4Wf6ayDV9Yv3PBDPsHD4ExBpAspJ6MOcCX+nzVF+UJVv7OqPcg852WEMSHQPoRA+FVSw==", "requires": { - "hast-util-is-element": "^1.0.0" + "hast-util-is-element": "^1.1.0" + }, + "dependencies": { + "hast-util-is-element": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz", + "integrity": "sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==" + } } }, "hast-util-from-parse5": { @@ -2212,20 +14989,6 @@ "xtend": "^4.0.1" } }, - "hast-util-has-property": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-1.0.4.tgz", - "integrity": "sha512-ghHup2voGfgFoHMGnaLHOjbYFACKrRh9KFttdCzMCbFoBMJXiNi2+XTrPP8+q6cDJM/RSqlCfVWrjp1H201rZg==" - }, - "hast-util-is-body-ok-link": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-1.0.3.tgz", - "integrity": "sha512-NB8jW4iqT+iVld2oCjSk0T2S2FyR86rDZ7nKHx3WNf/WX16fjjdfoog6T+YeJFsPzszVKsNlVJL+k5c4asAHog==", - "requires": { - "hast-util-has-property": "^1.0.0", - "hast-util-is-element": "^1.0.0" - } - }, "hast-util-is-element": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.4.tgz", @@ -2236,17 +14999,6 @@ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz", "integrity": "sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==" }, - "hast-util-phrasing": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-1.0.5.tgz", - "integrity": "sha512-P3uxm+8bnwcfAS/XpGie9wMmQXAQqsYhgQQKRwmWH/V6chiq0lmTy8KjQRJmYjusdMtNKGCUksdILSZy1suSpQ==", - "requires": { - "hast-util-embedded": "^1.0.0", - "hast-util-has-property": "^1.0.0", - "hast-util-is-body-ok-link": "^1.0.0", - "hast-util-is-element": "^1.0.0" - } - }, "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", @@ -2296,11 +15048,6 @@ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==" }, - "html-whitespace-sensitive-tag-names": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-1.0.2.tgz", - "integrity": "sha512-9jCcAq9ZsjUkZjNFDvxalDPhktOijpfzLyzBcqMLOFSbtcDNrPlKDvZeH7KdEbP7C6OjPpIdDMMPm0oq2Dpk0A==" - }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -2315,15 +15062,22 @@ } }, "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", "requires": { "depd": "~1.1.2", "inherits": "2.0.4", - "setprototypeof": "1.1.1", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" + }, + "dependencies": { + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + } } }, "http-signature": { @@ -2358,6 +15112,30 @@ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2422,9 +15200,18 @@ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } }, "is-date-object": { "version": "1.0.2", @@ -2442,9 +15229,9 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "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==" }, "is-glob": { "version": "4.0.1", @@ -2459,6 +15246,11 @@ "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==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2483,11 +15275,11 @@ "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "requires": { - "has": "^1.0.3" + "has-symbols": "^1.0.1" } }, "is-stream": { @@ -2646,6 +15438,11 @@ "istanbul-lib-report": "^3.0.0" } }, + "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==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2687,6 +15484,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2727,6 +15530,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2746,6 +15555,12 @@ "safe-buffer": "^5.0.1" } }, + "kebab-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.0.tgz", + "integrity": "sha1-P55JkK3K0MaGwOcB92RYaPdfkes=", + "dev": true + }, "languages4translatewiki": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/languages4translatewiki/-/languages4translatewiki-0.1.3.tgz", @@ -2798,6 +15613,16 @@ } } }, + "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" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -2808,9 +15633,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.assignin": { "version": "4.2.0", @@ -2917,34 +15742,6 @@ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "requires": { "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "log4js": { @@ -2984,6 +15781,15 @@ "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" }, + "lru-cache": { + "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", @@ -3060,7 +15866,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -3069,6 +15874,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", + "dev": true, "requires": { "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", @@ -3100,27 +15906,22 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, "requires": { "ms": "^2.1.1" } }, - "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" - } - }, "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==" + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "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" } @@ -3148,21 +15949,21 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mssql": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.2.1.tgz", - "integrity": "sha512-erINJ9EUPvPuWXifZfhum0CVEVrdvnFYlpgU6WKkQW69W4W7DWqJS2FHdedHnuJWlJ8x1WW1NcD8GFfF15O2aA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-6.3.0.tgz", + "integrity": "sha512-6/BK/3J8Oe4t6BYnmdCCORHhyBtBI/Fh0Sh6l1hPzb/hKtxDrsaSDGIpck1u8bzkLzev39TH5W2nz+ffeRz7gg==", "requires": { - "debug": "^4", + "debug": "^4.3.1", "tarn": "^1.1.5", - "tedious": "^6.6.2" + "tedious": "^6.7.0" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -3218,9 +16019,9 @@ } }, "nano": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.2.tgz", - "integrity": "sha512-1/rAvpd1J0Os0SazgutWQBx2buAq3KwJpmdIylPDqOwy73iQeAhTSCq3uzbGzvcNNW16Vv/BLXkk+DYcdcH+aw==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.3.tgz", + "integrity": "sha512-nubyTQeZ/p+xf3ZFFMd7WrZwpcy9tUDrbaXw9HFBsM6zBY5gXspvOjvG2Zz3emT6nfJtP/h7F2/ESfsVVXnuMw==", "requires": { "@types/request": "^2.48.4", "cloudant-follow": "^0.18.2", @@ -3230,11 +16031,11 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -3249,11 +16050,41 @@ "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", "integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=" }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": 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.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-environment-flags": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", @@ -3294,17 +16125,16 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm": { - "version": "6.14.5", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.5.tgz", - "integrity": "sha512-CDwa3FJd0XJpKDbWCST484H+mCNjF26dPrU+xnREW+upR0UODjMEfXPl3bxWuAwZIX6c2ASg1plLO7jP8ehWeA==", + "version": "6.14.8", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz", + "integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==", "requires": { - "JSONStream": "^1.3.5", "abbrev": "~1.1.1", "ansicolors": "~0.3.2", "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.7", + "bin-links": "^1.1.8", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", @@ -3325,7 +16155,7 @@ "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.3.0", + "gentle-fs": "^2.3.1", "glob": "^7.1.6", "graceful-fs": "^4.2.4", "has-unicode": "~2.0.1", @@ -3339,15 +16169,16 @@ "init-package-json": "^1.10.3", "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", + "JSONStream": "^1.3.5", "lazy-property": "~1.0.0", - "libcipm": "^4.0.7", + "libcipm": "^4.0.8", "libnpm": "^3.0.1", "libnpmaccess": "^3.0.2", "libnpmhook": "^5.0.3", "libnpmorg": "^1.0.1", "libnpmsearch": "^2.0.2", "libnpmteam": "^1.0.2", - "libnpx": "^10.2.2", + "libnpx": "^10.2.4", "lock-verify": "^2.1.0", "lockfile": "^1.0.4", "lodash._baseindexof": "*", @@ -3362,22 +16193,22 @@ "lodash.uniq": "~4.5.0", "lodash.without": "~4.4.0", "lru-cache": "^5.1.1", - "meant": "~1.0.1", + "meant": "^1.0.2", "mississippi": "^3.0.0", "mkdirp": "^0.5.5", "move-concurrently": "^1.0.1", "node-gyp": "^5.1.0", "nopt": "^4.0.3", "normalize-package-data": "^2.5.0", - "npm-audit-report": "^1.3.2", + "npm-audit-report": "^1.3.3", "npm-cache-filename": "~1.0.2", "npm-install-checks": "^3.0.2", - "npm-lifecycle": "^3.1.4", + "npm-lifecycle": "^3.1.5", "npm-package-arg": "^6.1.1", "npm-packlist": "^1.4.8", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.4", - "npm-registry-fetch": "^4.0.4", + "npm-registry-fetch": "^4.0.7", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", @@ -3423,40 +16254,31 @@ "write-file-atomic": "^2.4.3" }, "dependencies": { - "JSONStream": { - "version": "1.3.5", - "resolved": "", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "abbrev": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "bundled": true }, "agent-base": { "version": "4.3.0", - "resolved": "", "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "bundled": true, "requires": { "es6-promisify": "^5.0.0" } }, "agentkeepalive": { "version": "3.5.2", - "resolved": "", "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "bundled": true, "requires": { "humanize-ms": "^1.2.1" } }, "ajv": { "version": "5.5.2", - "resolved": "", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "bundled": true, "requires": { "co": "^4.6.0", "fast-deep-equal": "^1.0.0", @@ -3466,49 +16288,49 @@ }, "ansi-align": { "version": "2.0.0", - "resolved": "", "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "bundled": true, "requires": { "string-width": "^2.0.0" } }, "ansi-regex": { "version": "2.1.1", - "resolved": "", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "bundled": true }, "ansi-styles": { "version": "3.2.1", - "resolved": "", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "bundled": true, "requires": { "color-convert": "^1.9.0" } }, "ansicolors": { "version": "0.3.2", - "resolved": "", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "bundled": true }, "ansistyles": { "version": "0.1.3", - "resolved": "", - "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=" + "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=", + "bundled": true }, "aproba": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "bundled": true }, "archy": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "bundled": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": "", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "bundled": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -3516,8 +16338,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3530,8 +16352,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3540,55 +16362,55 @@ }, "asap": { "version": "2.0.6", - "resolved": "", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "bundled": true }, "asn1": { "version": "0.2.4", - "resolved": "", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "bundled": true, "requires": { "safer-buffer": "~2.1.0" } }, "assert-plus": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "bundled": true }, "asynckit": { "version": "0.4.0", - "resolved": "", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "bundled": true }, "aws-sign2": { "version": "0.7.0", - "resolved": "", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "bundled": true }, "aws4": { "version": "1.8.0", - "resolved": "", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "bundled": true }, "balanced-match": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "bundled": true }, "bcrypt-pbkdf": { "version": "1.0.2", - "resolved": "", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "bundled": true, "optional": true, "requires": { "tweetnacl": "^0.14.3" } }, "bin-links": { - "version": "1.1.7", - "resolved": "", - "integrity": "sha512-/eaLaTu7G7/o7PV04QPy1HRT65zf+1tFkPGv0sPTV0tRwufooYBQO3zrcyGgm+ja+ZtBf2GEuKjDRJ2pPG+yqA==", + "version": "1.1.8", + "integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==", + "bundled": true, "requires": { "bluebird": "^3.5.3", "cmd-shim": "^3.0.0", @@ -3600,13 +16422,13 @@ }, "bluebird": { "version": "3.5.5", - "resolved": "", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "bundled": true }, "boxen": { "version": "1.3.0", - "resolved": "", "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "bundled": true, "requires": { "ansi-align": "^2.0.0", "camelcase": "^4.0.0", @@ -3619,8 +16441,8 @@ }, "brace-expansion": { "version": "1.1.11", - "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "bundled": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3628,28 +16450,28 @@ }, "buffer-from": { "version": "1.0.0", - "resolved": "", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "bundled": true }, "builtins": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "bundled": true }, "byline": { "version": "5.0.0", - "resolved": "", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "bundled": true }, "byte-size": { "version": "5.0.1", - "resolved": "", - "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==" + "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==", + "bundled": true }, "cacache": { "version": "12.0.3", - "resolved": "", "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "bundled": true, "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", @@ -3670,28 +16492,28 @@ }, "call-limit": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==" + "integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==", + "bundled": true }, "camelcase": { "version": "4.1.0", - "resolved": "", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "bundled": true }, "capture-stack-trace": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "bundled": true }, "caseless": { "version": "0.12.0", - "resolved": "", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "bundled": true }, "chalk": { "version": "2.4.1", - "resolved": "", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "bundled": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3700,31 +16522,31 @@ }, "chownr": { "version": "1.1.4", - "resolved": "", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "bundled": true }, "ci-info": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "bundled": true }, "cidr-regex": { "version": "2.0.10", - "resolved": "", "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==", + "bundled": true, "requires": { "ip-regex": "^2.1.0" } }, "cli-boxes": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "bundled": true }, "cli-columns": { "version": "3.1.2", - "resolved": "", "integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=", + "bundled": true, "requires": { "string-width": "^2.0.0", "strip-ansi": "^3.0.1" @@ -3732,8 +16554,8 @@ }, "cli-table3": { "version": "0.5.1", - "resolved": "", "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "bundled": true, "requires": { "colors": "^1.1.2", "object-assign": "^4.1.0", @@ -3741,39 +16563,54 @@ } }, "cliui": { - "version": "4.1.0", - "resolved": "", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "bundled": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "bundled": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, "clone": { "version": "1.0.4", - "resolved": "", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "bundled": true }, "cmd-shim": { "version": "3.0.3", - "resolved": "", "integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "mkdirp": "~0.5.0" @@ -3781,37 +16618,37 @@ }, "co": { "version": "4.6.0", - "resolved": "", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "bundled": true }, "code-point-at": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "bundled": true }, "color-convert": { "version": "1.9.1", - "resolved": "", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "bundled": true, "requires": { "color-name": "^1.1.1" } }, "color-name": { "version": "1.1.3", - "resolved": "", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "bundled": true }, "colors": { "version": "1.3.3", - "resolved": "", "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "bundled": true, "optional": true }, "columnify": { "version": "1.5.4", - "resolved": "", "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "bundled": true, "requires": { "strip-ansi": "^3.0.0", "wcwidth": "^1.0.0" @@ -3819,21 +16656,21 @@ }, "combined-stream": { "version": "1.0.6", - "resolved": "", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "bundled": true, "requires": { "delayed-stream": "~1.0.0" } }, "concat-map": { "version": "0.0.1", - "resolved": "", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "bundled": true }, "concat-stream": { "version": "1.6.2", - "resolved": "", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "bundled": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -3843,8 +16680,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3857,8 +16694,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3867,19 +16704,19 @@ }, "config-chain": { "version": "1.1.12", - "resolved": "", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "bundled": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "configstore": { - "version": "3.1.2", - "resolved": "", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "3.1.5", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", + "bundled": true, "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^4.2.1", "graceful-fs": "^4.1.2", "make-dir": "^1.0.0", "unique-string": "^1.0.0", @@ -3889,13 +16726,13 @@ }, "console-control-strings": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "bundled": true }, "copy-concurrently": { "version": "1.0.5", - "resolved": "", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "bundled": true, "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -3907,33 +16744,33 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true }, "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "bundled": true } } }, "core-util-is": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "bundled": true }, "create-error-class": { "version": "3.0.2", - "resolved": "", "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "bundled": true, "requires": { "capture-stack-trace": "^1.0.0" } }, "cross-spawn": { "version": "5.1.0", - "resolved": "", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "bundled": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -3942,8 +16779,8 @@ "dependencies": { "lru-cache": { "version": "4.1.5", - "resolved": "", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "bundled": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -3951,131 +16788,131 @@ }, "yallist": { "version": "2.1.2", - "resolved": "", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "bundled": true } } }, "crypto-random-string": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "bundled": true }, "cyclist": { "version": "0.2.2", - "resolved": "", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "bundled": true }, "dashdash": { "version": "1.14.1", - "resolved": "", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "bundled": true, "requires": { "assert-plus": "^1.0.0" } }, "debug": { "version": "3.1.0", - "resolved": "", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "bundled": true, "requires": { "ms": "2.0.0" }, "dependencies": { "ms": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "bundled": true } } }, "debuglog": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", + "bundled": true }, "decamelize": { "version": "1.2.0", - "resolved": "", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "bundled": true }, "decode-uri-component": { "version": "0.2.0", - "resolved": "", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "bundled": true }, "deep-extend": { "version": "0.6.0", - "resolved": "", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "bundled": true }, "defaults": { "version": "1.0.3", - "resolved": "", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "bundled": true, "requires": { "clone": "^1.0.2" } }, "define-properties": { "version": "1.1.3", - "resolved": "", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "bundled": true, "requires": { "object-keys": "^1.0.12" } }, "delayed-stream": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "bundled": true }, "delegates": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "bundled": true }, "detect-indent": { "version": "5.0.0", - "resolved": "", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "bundled": true }, "detect-newline": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "bundled": true }, "dezalgo": { "version": "1.0.3", - "resolved": "", "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "bundled": true, "requires": { "asap": "^2.0.0", "wrappy": "1" } }, "dot-prop": { - "version": "4.2.0", - "resolved": "", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "4.2.1", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "bundled": true, "requires": { "is-obj": "^1.0.0" } }, "dotenv": { "version": "5.0.1", - "resolved": "", - "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==", + "bundled": true }, "duplexer3": { "version": "0.1.4", - "resolved": "", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "bundled": true }, "duplexify": { "version": "3.6.0", - "resolved": "", "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "bundled": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -4085,8 +16922,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4099,8 +16936,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4109,8 +16946,8 @@ }, "ecc-jsbn": { "version": "0.1.2", - "resolved": "", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "bundled": true, "optional": true, "requires": { "jsbn": "~0.1.0", @@ -4119,47 +16956,52 @@ }, "editor": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=" + "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=", + "bundled": true + }, + "emoji-regex": { + "version": "7.0.3", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "bundled": true }, "encoding": { "version": "0.1.12", - "resolved": "", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "bundled": true, "requires": { "iconv-lite": "~0.4.13" } }, "end-of-stream": { "version": "1.4.1", - "resolved": "", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "bundled": true, "requires": { "once": "^1.4.0" } }, "env-paths": { "version": "2.2.0", - "resolved": "", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "bundled": true }, "err-code": { "version": "1.1.2", - "resolved": "", - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", + "bundled": true }, "errno": { "version": "0.1.7", - "resolved": "", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "bundled": true, "requires": { "prr": "~1.0.1" } }, "es-abstract": { "version": "1.12.0", - "resolved": "", "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "bundled": true, "requires": { "es-to-primitive": "^1.1.1", "function-bind": "^1.1.1", @@ -4170,8 +17012,8 @@ }, "es-to-primitive": { "version": "1.2.0", - "resolved": "", "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "bundled": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -4180,26 +17022,26 @@ }, "es6-promise": { "version": "4.2.8", - "resolved": "", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "bundled": true }, "es6-promisify": { "version": "5.0.0", - "resolved": "", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "bundled": true, "requires": { "es6-promise": "^4.0.3" } }, "escape-string-regexp": { "version": "1.0.5", - "resolved": "", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "bundled": true }, "execa": { "version": "0.7.0", - "resolved": "", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "bundled": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4212,53 +17054,45 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "bundled": true } } }, "extend": { "version": "3.0.2", - "resolved": "", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "bundled": true }, "extsprintf": { "version": "1.3.0", - "resolved": "", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "bundled": true }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "bundled": true }, "fast-json-stable-stringify": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "bundled": true }, "figgy-pudding": { "version": "3.5.1", - "resolved": "", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "bundled": true }, "find-npm-prefix": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==" - }, - "find-up": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } + "integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==", + "bundled": true }, "flush-write-stream": { "version": "1.0.3", - "resolved": "", "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "bundled": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.4" @@ -4266,8 +17100,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4280,8 +17114,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4290,13 +17124,13 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": "", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "bundled": true }, "form-data": { "version": "2.3.2", - "resolved": "", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "bundled": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "1.0.6", @@ -4305,8 +17139,8 @@ }, "from2": { "version": "2.3.0", - "resolved": "", "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "bundled": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -4314,8 +17148,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4328,8 +17162,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4338,16 +17172,16 @@ }, "fs-minipass": { "version": "1.2.7", - "resolved": "", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "bundled": true, "requires": { "minipass": "^2.6.0" }, "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4357,8 +17191,8 @@ }, "fs-vacuum": { "version": "1.2.10", - "resolved": "", "integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "path-is-inside": "^1.0.1", @@ -4367,8 +17201,8 @@ }, "fs-write-stream-atomic": { "version": "1.0.10", - "resolved": "", "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "bundled": true, "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -4378,13 +17212,13 @@ "dependencies": { "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "bundled": true }, "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4397,8 +17231,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -4407,18 +17241,18 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "bundled": true }, "function-bind": { "version": "1.1.1", - "resolved": "", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "bundled": true }, "gauge": { "version": "2.7.4", - "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "bundled": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4432,13 +17266,13 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true }, "string-width": { "version": "1.0.2", - "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4449,13 +17283,13 @@ }, "genfun": { "version": "5.0.0", - "resolved": "", - "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==" + "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", + "bundled": true }, "gentle-fs": { - "version": "2.3.0", - "resolved": "", - "integrity": "sha512-3k2CgAmPxuz7S6nKK+AqFE2AdM1QuwqKLPKzIET3VRwK++3q96MsNFobScDjlCrq97ZJ8y5R725MOlm6ffUCjg==", + "version": "2.3.1", + "integrity": "sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==", + "bundled": true, "requires": { "aproba": "^1.1.2", "chownr": "^1.1.2", @@ -4472,41 +17306,41 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true }, "iferr": { "version": "0.1.5", - "resolved": "", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "bundled": true } } }, "get-caller-file": { - "version": "1.0.3", - "resolved": "", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + "version": "2.0.5", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "bundled": true }, "get-stream": { "version": "4.1.0", - "resolved": "", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "bundled": true, "requires": { "pump": "^3.0.0" } }, "getpass": { "version": "0.1.7", - "resolved": "", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "bundled": true, "requires": { "assert-plus": "^1.0.0" } }, "glob": { "version": "7.1.6", - "resolved": "", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "bundled": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4518,16 +17352,16 @@ }, "global-dirs": { "version": "0.1.1", - "resolved": "", "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "bundled": true, "requires": { "ini": "^1.3.4" } }, "got": { "version": "6.7.1", - "resolved": "", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "bundled": true, "requires": { "create-error-class": "^3.0.0", "duplexer3": "^0.1.4", @@ -4544,25 +17378,25 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "bundled": true } } }, "graceful-fs": { "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "bundled": true }, "har-schema": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "bundled": true }, "har-validator": { "version": "5.1.0", - "resolved": "", "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "bundled": true, "requires": { "ajv": "^5.3.0", "har-schema": "^2.0.0" @@ -4570,41 +17404,41 @@ }, "has": { "version": "1.0.3", - "resolved": "", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "bundled": true, "requires": { "function-bind": "^1.1.1" } }, "has-flag": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "bundled": true }, "has-symbols": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "bundled": true }, "has-unicode": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "bundled": true }, "hosted-git-info": { "version": "2.8.8", - "resolved": "", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "bundled": true }, "http-cache-semantics": { "version": "3.8.1", - "resolved": "", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "bundled": true }, "http-proxy-agent": { "version": "2.1.0", - "resolved": "", "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "bundled": true, "requires": { "agent-base": "4", "debug": "3.1.0" @@ -4612,8 +17446,8 @@ }, "http-signature": { "version": "1.2.0", - "resolved": "", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "bundled": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -4622,8 +17456,8 @@ }, "https-proxy-agent": { "version": "2.2.4", - "resolved": "", "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "bundled": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -4631,52 +17465,52 @@ }, "humanize-ms": { "version": "1.2.1", - "resolved": "", "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "bundled": true, "requires": { "ms": "^2.0.0" } }, "iconv-lite": { "version": "0.4.23", - "resolved": "", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "bundled": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "iferr": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==" + "integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==", + "bundled": true }, "ignore-walk": { "version": "3.0.3", - "resolved": "", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "bundled": true, "requires": { "minimatch": "^3.0.4" } }, "import-lazy": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "bundled": true }, "imurmurhash": { "version": "0.1.4", - "resolved": "", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "bundled": true }, "infer-owner": { "version": "1.0.4", - "resolved": "", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "bundled": true }, "inflight": { "version": "1.0.6", - "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4684,18 +17518,18 @@ }, "inherits": { "version": "2.0.4", - "resolved": "", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "bundled": true }, "ini": { "version": "1.3.5", - "resolved": "", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "bundled": true }, "init-package-json": { "version": "1.10.3", - "resolved": "", "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", + "bundled": true, "requires": { "glob": "^7.1.1", "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", @@ -4707,66 +17541,61 @@ "validate-npm-package-name": "^3.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, "ip": { "version": "1.1.5", - "resolved": "", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "bundled": true }, "ip-regex": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "bundled": true }, "is-callable": { "version": "1.1.4", - "resolved": "", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "bundled": true }, "is-ci": { "version": "1.2.1", - "resolved": "", "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "bundled": true, "requires": { "ci-info": "^1.5.0" }, "dependencies": { "ci-info": { "version": "1.6.0", - "resolved": "", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==" + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "bundled": true } } }, "is-cidr": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==", + "bundled": true, "requires": { "cidr-regex": "^2.0.10" } }, "is-date-object": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "bundled": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, "requires": { "number-is-nan": "^1.0.0" } }, "is-installed-globally": { "version": "0.1.0", - "resolved": "", "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "bundled": true, "requires": { "global-dirs": "^0.1.0", "is-path-inside": "^1.0.0" @@ -4774,108 +17603,117 @@ }, "is-npm": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "bundled": true }, "is-obj": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "bundled": true }, "is-path-inside": { "version": "1.0.1", - "resolved": "", "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "bundled": true, "requires": { "path-is-inside": "^1.0.1" } }, "is-redirect": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "bundled": true }, "is-regex": { "version": "1.0.4", - "resolved": "", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "bundled": true, "requires": { "has": "^1.0.1" } }, "is-retry-allowed": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "bundled": true }, "is-stream": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "bundled": true }, "is-symbol": { "version": "1.0.2", - "resolved": "", "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "bundled": true, "requires": { "has-symbols": "^1.0.0" } }, "is-typedarray": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "bundled": true }, "isarray": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "bundled": true }, "isexe": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "bundled": true }, "isstream": { "version": "0.1.2", - "resolved": "", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "bundled": true }, "jsbn": { "version": "0.1.1", - "resolved": "", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "bundled": true, "optional": true }, "json-parse-better-errors": { "version": "1.0.2", - "resolved": "", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "bundled": true }, "json-schema": { "version": "0.2.3", - "resolved": "", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "bundled": true }, "json-schema-traverse": { "version": "0.3.1", - "resolved": "", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "bundled": true }, "json-stringify-safe": { "version": "5.0.1", - "resolved": "", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "bundled": true }, "jsonparse": { "version": "1.3.1", - "resolved": "", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "bundled": true + }, + "JSONStream": { + "version": "1.3.5", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "bundled": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } }, "jsprim": { "version": "1.4.1", - "resolved": "", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "bundled": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -4885,29 +17723,21 @@ }, "latest-version": { "version": "3.1.0", - "resolved": "", "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "bundled": true, "requires": { "package-json": "^4.0.0" } }, "lazy-property": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=" - }, - "lcid": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } + "integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=", + "bundled": true }, "libcipm": { - "version": "4.0.7", - "resolved": "", - "integrity": "sha512-fTq33otU3PNXxxCTCYCYe7V96o59v/o7bvtspmbORXpgFk+wcWrGf5x6tBgui5gCed/45/wtPomBsZBYm5KbIw==", + "version": "4.0.8", + "integrity": "sha512-IN3hh2yDJQtZZ5paSV4fbvJg4aHxCCg5tcZID/dSVlTuUiWktsgaldVljJv6Z5OUlYspx6xQkbR0efNodnIrOA==", + "bundled": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.1", @@ -4915,7 +17745,7 @@ "find-npm-prefix": "^1.0.2", "graceful-fs": "^4.1.11", "ini": "^1.3.5", - "lock-verify": "^2.0.2", + "lock-verify": "^2.1.0", "mkdirp": "^0.5.1", "npm-lifecycle": "^3.0.0", "npm-logical-tree": "^1.2.1", @@ -4928,8 +17758,8 @@ }, "libnpm": { "version": "3.0.1", - "resolved": "", "integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==", + "bundled": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.3", @@ -4955,8 +17785,8 @@ }, "libnpmaccess": { "version": "3.0.2", - "resolved": "", "integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==", + "bundled": true, "requires": { "aproba": "^2.0.0", "get-stream": "^4.0.0", @@ -4966,8 +17796,8 @@ }, "libnpmconfig": { "version": "1.2.1", - "resolved": "", "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "find-up": "^3.0.0", @@ -4976,16 +17806,16 @@ "dependencies": { "find-up": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "bundled": true, "requires": { "locate-path": "^3.0.0" } }, "locate-path": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "bundled": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -4993,31 +17823,31 @@ }, "p-limit": { "version": "2.2.0", - "resolved": "", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "bundled": true, "requires": { "p-try": "^2.0.0" } }, "p-locate": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "bundled": true, "requires": { "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", - "resolved": "", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "bundled": true } } }, "libnpmhook": { "version": "5.0.3", - "resolved": "", "integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5027,8 +17857,8 @@ }, "libnpmorg": { "version": "1.0.1", - "resolved": "", "integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5038,8 +17868,8 @@ }, "libnpmpublish": { "version": "1.1.2", - "resolved": "", "integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.5.1", @@ -5054,8 +17884,8 @@ }, "libnpmsearch": { "version": "2.0.2", - "resolved": "", "integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "get-stream": "^4.0.0", @@ -5064,8 +17894,8 @@ }, "libnpmteam": { "version": "1.0.2", - "resolved": "", "integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==", + "bundled": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -5074,9 +17904,9 @@ } }, "libnpx": { - "version": "10.2.2", - "resolved": "", - "integrity": "sha512-ujaYToga1SAX5r7FU5ShMFi88CWpY75meNZtr6RtEyv4l2ZK3+Wgvxq2IqlwWBiDZOqhumdeiocPS1aKrCMe3A==", + "version": "10.2.4", + "integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==", + "bundled": true, "requires": { "dotenv": "^5.0.1", "npm-package-arg": "^6.0.0", @@ -5085,22 +17915,13 @@ "update-notifier": "^2.3.0", "which": "^1.3.0", "y18n": "^4.0.0", - "yargs": "^11.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "yargs": "^14.2.3" } }, "lock-verify": { "version": "2.1.0", - "resolved": "", "integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==", + "bundled": true, "requires": { "npm-package-arg": "^6.1.0", "semver": "^5.4.1" @@ -5108,21 +17929,21 @@ }, "lockfile": { "version": "1.0.4", - "resolved": "", "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "bundled": true, "requires": { "signal-exit": "^3.0.2" } }, "lodash._baseindexof": { "version": "3.1.0", - "resolved": "", - "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=" + "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=", + "bundled": true }, "lodash._baseuniq": { "version": "4.6.0", - "resolved": "", "integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=", + "bundled": true, "requires": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" @@ -5130,87 +17951,87 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "resolved": "", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "bundled": true }, "lodash._cacheindexof": { "version": "3.0.2", - "resolved": "", - "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=" + "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=", + "bundled": true }, "lodash._createcache": { "version": "3.1.2", - "resolved": "", "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", + "bundled": true, "requires": { "lodash._getnative": "^3.0.0" } }, "lodash._createset": { "version": "4.0.3", - "resolved": "", - "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=" + "integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=", + "bundled": true }, "lodash._getnative": { "version": "3.9.1", - "resolved": "", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "bundled": true }, "lodash._root": { "version": "3.0.1", - "resolved": "", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "bundled": true }, "lodash.clonedeep": { "version": "4.5.0", - "resolved": "", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "bundled": true }, "lodash.restparam": { "version": "3.6.1", - "resolved": "", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "bundled": true }, "lodash.union": { "version": "4.6.0", - "resolved": "", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "bundled": true }, "lodash.uniq": { "version": "4.5.0", - "resolved": "", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "bundled": true }, "lodash.without": { "version": "4.4.0", - "resolved": "", - "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=" + "integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=", + "bundled": true }, "lowercase-keys": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "bundled": true }, "lru-cache": { "version": "5.1.1", - "resolved": "", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "bundled": true, "requires": { "yallist": "^3.0.2" } }, "make-dir": { "version": "1.3.0", - "resolved": "", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "bundled": true, "requires": { "pify": "^3.0.0" } }, "make-fetch-happen": { "version": "5.0.2", - "resolved": "", "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "bundled": true, "requires": { "agentkeepalive": "^3.4.1", "cacache": "^12.0.0", @@ -5225,69 +18046,49 @@ "ssri": "^6.0.0" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, "meant": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha512-UakVLFjKkbbUwNWJ2frVLnnAtbb7D7DsloxRd3s/gDpI8rdv8W5Hp3NaDb+POBI1fQdeussER6NB8vpcRURvlg==" - }, - "mem": { - "version": "4.3.0", - "resolved": "", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - } - } + "version": "1.0.2", + "integrity": "sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==", + "bundled": true }, "mime-db": { "version": "1.35.0", - "resolved": "", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "bundled": true }, "mime-types": { "version": "2.1.19", - "resolved": "", "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "bundled": true, "requires": { "mime-db": "~1.35.0" } }, "minimatch": { "version": "3.0.4", - "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, "requires": { "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "bundled": true + }, "minizlib": { "version": "1.3.3", - "resolved": "", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "bundled": true, "requires": { "minipass": "^2.9.0" }, "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5297,8 +18098,8 @@ }, "mississippi": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "bundled": true, "requires": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", @@ -5314,23 +18115,23 @@ }, "mkdirp": { "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "bundled": true, "requires": { "minimist": "^1.2.5" }, "dependencies": { "minimist": { "version": "1.2.5", - "resolved": "", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "bundled": true } } }, "move-concurrently": { "version": "1.0.1", - "resolved": "", "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "bundled": true, "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -5342,30 +18143,25 @@ "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true } } }, "ms": { "version": "2.1.1", - "resolved": "", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "bundled": true }, "mute-stream": { "version": "0.0.7", - "resolved": "", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "bundled": true }, "node-fetch-npm": { "version": "2.0.2", - "resolved": "", "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", + "bundled": true, "requires": { "encoding": "^0.1.11", "json-parse-better-errors": "^1.0.0", @@ -5374,8 +18170,8 @@ }, "node-gyp": { "version": "5.1.0", - "resolved": "", "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", + "bundled": true, "requires": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -5392,8 +18188,8 @@ }, "nopt": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "bundled": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -5401,8 +18197,8 @@ }, "normalize-package-data": { "version": "2.5.0", - "resolved": "", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "bundled": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -5412,8 +18208,8 @@ "dependencies": { "resolve": { "version": "1.10.0", - "resolved": "", "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "bundled": true, "requires": { "path-parse": "^1.0.6" } @@ -5421,9 +18217,9 @@ } }, "npm-audit-report": { - "version": "1.3.2", - "resolved": "", - "integrity": "sha512-abeqS5ONyXNaZJPGAf6TOUMNdSe1Y6cpc9MLBRn+CuUoYbfdca6AxOyXVlfIv9OgKX+cacblbG5w7A6ccwoTPw==", + "version": "1.3.3", + "integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==", + "bundled": true, "requires": { "cli-table3": "^0.5.0", "console-control-strings": "^1.1.0" @@ -5431,29 +18227,29 @@ }, "npm-bundled": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "bundled": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-cache-filename": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=" + "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=", + "bundled": true }, "npm-install-checks": { "version": "3.0.2", - "resolved": "", "integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==", + "bundled": true, "requires": { "semver": "^2.3.0 || 3.x || 4 || 5" } }, "npm-lifecycle": { - "version": "3.1.4", - "resolved": "", - "integrity": "sha512-tgs1PaucZwkxECGKhC/stbEgFyc3TGh2TJcg2CDr6jbvQRdteHNhmMeljRzpe4wgFAXQADoy1cSqqi7mtiAa5A==", + "version": "3.1.5", + "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", + "bundled": true, "requires": { "byline": "^5.0.0", "graceful-fs": "^4.1.15", @@ -5467,18 +18263,18 @@ }, "npm-logical-tree": { "version": "1.2.1", - "resolved": "", - "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==" + "integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==", + "bundled": true }, "npm-normalize-package-bin": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "bundled": true }, "npm-package-arg": { "version": "6.1.1", - "resolved": "", "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "bundled": true, "requires": { "hosted-git-info": "^2.7.1", "osenv": "^0.1.5", @@ -5488,8 +18284,8 @@ }, "npm-packlist": { "version": "1.4.8", - "resolved": "", "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "bundled": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -5498,8 +18294,8 @@ }, "npm-pick-manifest": { "version": "3.0.2", - "resolved": "", "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1", "npm-package-arg": "^6.0.0", @@ -5508,8 +18304,8 @@ }, "npm-profile": { "version": "4.0.4", - "resolved": "", "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", + "bundled": true, "requires": { "aproba": "^1.1.2 || 2", "figgy-pudding": "^3.4.1", @@ -5517,13 +18313,13 @@ } }, "npm-registry-fetch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.4.tgz", - "integrity": "sha512-6jb34hX/iYNQebqWUHtU8YF6Cjb1H6ouTFPClYsyiW6lpFkljTpdeftm53rRojtja1rKAvKNIIiTS5Sjpw4wsA==", + "version": "4.0.7", + "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", + "bundled": true, "requires": { - "JSONStream": "^1.3.4", "bluebird": "^3.5.1", "figgy-pudding": "^3.4.1", + "JSONStream": "^1.3.4", "lru-cache": "^5.1.1", "make-fetch-happen": "^5.0.0", "npm-package-arg": "^6.1.0", @@ -5531,29 +18327,29 @@ }, "dependencies": { "safe-buffer": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "version": "5.2.1", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "bundled": true } } }, "npm-run-path": { "version": "2.0.2", - "resolved": "", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "bundled": true, "requires": { "path-key": "^2.0.0" } }, "npm-user-validate": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=" + "integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=", + "bundled": true }, "npmlog": { "version": "4.1.2", - "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "bundled": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -5563,28 +18359,28 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "bundled": true }, "oauth-sign": { "version": "0.9.0", - "resolved": "", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "bundled": true }, "object-assign": { "version": "4.1.1", - "resolved": "", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "bundled": true }, "object-keys": { "version": "1.0.12", - "resolved": "", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "bundled": true }, "object.getownpropertydescriptors": { "version": "2.0.3", - "resolved": "", "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "bundled": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.5.1" @@ -5592,114 +18388,45 @@ }, "once": { "version": "1.4.0", - "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "requires": { "wrappy": "1" } }, "opener": { "version": "1.5.1", - "resolved": "", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "bundled": true }, "os-homedir": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "3.1.0", - "resolved": "", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "bundled": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "bundled": true }, "osenv": { "version": "0.1.5", - "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "bundled": true, "requires": { "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, - "p-limit": { - "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" + "os-tmpdir": "^1.0.0" } }, - "p-try": { + "p-finally": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "bundled": true }, "package-json": { "version": "4.0.1", - "resolved": "", "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "bundled": true, "requires": { "got": "^6.7.1", "registry-auth-token": "^3.0.1", @@ -5709,8 +18436,8 @@ }, "pacote": { "version": "9.5.12", - "resolved": "", "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", + "bundled": true, "requires": { "bluebird": "^3.5.3", "cacache": "^12.0.2", @@ -5746,8 +18473,8 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5757,8 +18484,8 @@ }, "parallel-transform": { "version": "1.1.0", - "resolved": "", "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "bundled": true, "requires": { "cyclist": "~0.2.2", "inherits": "^2.0.3", @@ -5767,8 +18494,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5781,8 +18508,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -5791,58 +18518,58 @@ }, "path-exists": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "bundled": true }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "bundled": true }, "path-is-inside": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "bundled": true }, "path-key": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "bundled": true }, "path-parse": { "version": "1.0.6", - "resolved": "", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "bundled": true }, "performance-now": { "version": "2.1.0", - "resolved": "", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "bundled": true }, "pify": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "bundled": true }, "prepend-http": { "version": "1.0.4", - "resolved": "", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "bundled": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "bundled": true }, "promise-inflight": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "bundled": true }, "promise-retry": { "version": "1.1.1", - "resolved": "", "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "bundled": true, "requires": { "err-code": "^1.0.0", "retry": "^0.10.0" @@ -5850,51 +18577,51 @@ "dependencies": { "retry": { "version": "0.10.1", - "resolved": "", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "bundled": true } } }, "promzard": { "version": "0.3.0", - "resolved": "", "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "bundled": true, "requires": { "read": "1" } }, "proto-list": { "version": "1.2.4", - "resolved": "", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "bundled": true }, "protoduck": { "version": "5.0.1", - "resolved": "", "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "bundled": true, "requires": { "genfun": "^5.0.0" } }, "prr": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "bundled": true }, "pseudomap": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "bundled": true }, "psl": { "version": "1.1.29", - "resolved": "", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "bundled": true }, "pump": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5902,8 +18629,8 @@ }, "pumpify": { "version": "1.5.1", - "resolved": "", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "bundled": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -5912,8 +18639,8 @@ "dependencies": { "pump": { "version": "2.0.1", - "resolved": "", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5923,23 +18650,23 @@ }, "punycode": { "version": "1.4.1", - "resolved": "", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "bundled": true }, "qrcode-terminal": { "version": "0.12.0", - "resolved": "", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==" + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bundled": true }, "qs": { "version": "6.5.2", - "resolved": "", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "bundled": true }, "query-string": { "version": "6.8.2", - "resolved": "", "integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==", + "bundled": true, "requires": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", @@ -5948,47 +18675,40 @@ }, "qw": { "version": "1.0.1", - "resolved": "", - "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=" + "integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=", + "bundled": true }, "rc": { "version": "1.2.8", - "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "bundled": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - } } }, "read": { "version": "1.0.7", - "resolved": "", "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "bundled": true, "requires": { "mute-stream": "~0.0.4" } }, "read-cmd-shim": { "version": "1.0.5", - "resolved": "", "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2" } }, "read-installed": { "version": "4.0.3", - "resolved": "", "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", + "bundled": true, "requires": { "debuglog": "^1.0.1", "graceful-fs": "^4.1.2", @@ -6001,8 +18721,8 @@ }, "read-package-json": { "version": "2.1.1", - "resolved": "", "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", + "bundled": true, "requires": { "glob": "^7.1.1", "graceful-fs": "^4.1.2", @@ -6013,8 +18733,8 @@ }, "read-package-tree": { "version": "5.3.1", - "resolved": "", "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", + "bundled": true, "requires": { "read-package-json": "^2.0.0", "readdir-scoped-modules": "^1.0.0", @@ -6023,8 +18743,8 @@ }, "readable-stream": { "version": "3.6.0", - "resolved": "", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "bundled": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6033,8 +18753,8 @@ }, "readdir-scoped-modules": { "version": "1.1.0", - "resolved": "", "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "bundled": true, "requires": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", @@ -6044,8 +18764,8 @@ }, "registry-auth-token": { "version": "3.4.0", - "resolved": "", "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "bundled": true, "requires": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" @@ -6053,16 +18773,16 @@ }, "registry-url": { "version": "3.1.0", - "resolved": "", "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "bundled": true, "requires": { "rc": "^1.0.1" } }, "request": { "version": "2.88.0", - "resolved": "", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "bundled": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -6088,115 +18808,115 @@ }, "require-directory": { "version": "2.1.1", - "resolved": "", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "bundled": true }, "require-main-filename": { - "version": "1.0.1", - "resolved": "", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + "version": "2.0.0", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "bundled": true }, "resolve-from": { "version": "4.0.0", - "resolved": "", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "bundled": true }, "retry": { "version": "0.12.0", - "resolved": "", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "bundled": true }, "rimraf": { "version": "2.7.1", - "resolved": "", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "bundled": true, "requires": { "glob": "^7.1.3" } }, "run-queue": { "version": "1.0.3", - "resolved": "", "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "bundled": true, "requires": { "aproba": "^1.1.1" }, "dependencies": { "aproba": { "version": "1.2.0", - "resolved": "", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "bundled": true } } }, "safe-buffer": { "version": "5.1.2", - "resolved": "", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "bundled": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "bundled": true }, "semver": { "version": "5.7.1", - "resolved": "", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bundled": true }, "semver-diff": { "version": "2.1.0", - "resolved": "", "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "bundled": true, "requires": { "semver": "^5.0.3" } }, "set-blocking": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true }, "sha": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==", + "bundled": true, "requires": { "graceful-fs": "^4.1.2" } }, "shebang-command": { "version": "1.2.0", - "resolved": "", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "bundled": true, "requires": { "shebang-regex": "^1.0.0" } }, "shebang-regex": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "bundled": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true }, "slide": { "version": "1.1.6", - "resolved": "", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "bundled": true }, "smart-buffer": { "version": "4.1.0", - "resolved": "", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "bundled": true }, "socks": { "version": "2.3.3", - "resolved": "", "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "bundled": true, "requires": { "ip": "1.1.5", "smart-buffer": "^4.1.0" @@ -6204,8 +18924,8 @@ }, "socks-proxy-agent": { "version": "4.0.2", - "resolved": "", "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "bundled": true, "requires": { "agent-base": "~4.2.1", "socks": "~2.3.2" @@ -6213,8 +18933,8 @@ "dependencies": { "agent-base": { "version": "4.2.1", - "resolved": "", "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "bundled": true, "requires": { "es6-promisify": "^5.0.0" } @@ -6223,13 +18943,13 @@ }, "sorted-object": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=" + "integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=", + "bundled": true }, "sorted-union-stream": { "version": "2.1.3", - "resolved": "", "integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=", + "bundled": true, "requires": { "from2": "^1.3.0", "stream-iterate": "^1.1.0" @@ -6237,8 +18957,8 @@ "dependencies": { "from2": { "version": "1.3.0", - "resolved": "", "integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=", + "bundled": true, "requires": { "inherits": "~2.0.1", "readable-stream": "~1.1.10" @@ -6246,13 +18966,13 @@ }, "isarray": { "version": "0.0.1", - "resolved": "", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "bundled": true }, "readable-stream": { "version": "1.1.14", - "resolved": "", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -6262,15 +18982,15 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "bundled": true } } }, "spdx-correct": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "bundled": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -6278,32 +18998,32 @@ }, "spdx-exceptions": { "version": "2.1.0", - "resolved": "", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "bundled": true }, "spdx-expression-parse": { "version": "3.0.0", - "resolved": "", "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "bundled": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { - "version": "3.0.3", - "resolved": "", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==" + "version": "3.0.5", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "bundled": true }, "split-on-first": { "version": "1.1.0", - "resolved": "", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "bundled": true }, "sshpk": { "version": "1.14.2", - "resolved": "", "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "bundled": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6318,16 +19038,16 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "bundled": true, "requires": { "figgy-pudding": "^3.5.1" } }, "stream-each": { "version": "1.2.2", - "resolved": "", "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "bundled": true, "requires": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" @@ -6335,8 +19055,8 @@ }, "stream-iterate": { "version": "1.2.0", - "resolved": "", "integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=", + "bundled": true, "requires": { "readable-stream": "^2.1.5", "stream-shift": "^1.0.0" @@ -6344,8 +19064,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6358,8 +19078,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6368,18 +19088,33 @@ }, "stream-shift": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "bundled": true }, "strict-uri-encode": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "bundled": true + }, + "string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "bundled": true + } + } }, "string-width": { "version": "2.1.1", - "resolved": "", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "bundled": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -6387,74 +19122,59 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "bundled": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "bundled": true }, "strip-ansi": { "version": "4.0.0", - "resolved": "", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "bundled": true, "requires": { "ansi-regex": "^3.0.0" } } } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.0", - "resolved": "", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - } - } - }, "stringify-package": { "version": "1.0.1", - "resolved": "", - "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==" + "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==", + "bundled": true }, "strip-ansi": { "version": "3.0.1", - "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-eof": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "bundled": true }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "bundled": true }, "supports-color": { "version": "5.4.0", - "resolved": "", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "bundled": true, "requires": { "has-flag": "^3.0.0" } }, "tar": { "version": "4.4.13", - "resolved": "", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "bundled": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -6467,8 +19187,8 @@ "dependencies": { "minipass": { "version": "2.9.0", - "resolved": "", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "bundled": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6478,26 +19198,26 @@ }, "term-size": { "version": "1.2.0", - "resolved": "", "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "bundled": true, "requires": { "execa": "^0.7.0" } }, "text-table": { "version": "0.2.0", - "resolved": "", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "bundled": true }, "through": { "version": "2.3.8", - "resolved": "", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "bundled": true }, "through2": { "version": "2.0.3", - "resolved": "", "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "bundled": true, "requires": { "readable-stream": "^2.1.5", "xtend": "~4.0.1" @@ -6505,8 +19225,8 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "bundled": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6519,8 +19239,8 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6529,18 +19249,18 @@ }, "timed-out": { "version": "4.0.1", - "resolved": "", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "bundled": true }, "tiny-relative-date": { "version": "1.3.0", - "resolved": "", - "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==" + "integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==", + "bundled": true }, "tough-cookie": { "version": "2.4.3", - "resolved": "", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "bundled": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -6548,71 +19268,71 @@ }, "tunnel-agent": { "version": "0.6.0", - "resolved": "", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "bundled": true, "requires": { "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", - "resolved": "", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "bundled": true, "optional": true }, "typedarray": { "version": "0.0.6", - "resolved": "", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "bundled": true }, "uid-number": { "version": "0.0.6", - "resolved": "", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "bundled": true }, "umask": { "version": "1.1.0", - "resolved": "", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" + "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", + "bundled": true }, "unique-filename": { "version": "1.1.1", - "resolved": "", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "bundled": true, "requires": { "unique-slug": "^2.0.0" } }, "unique-slug": { "version": "2.0.0", - "resolved": "", "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "bundled": true, "requires": { "imurmurhash": "^0.1.4" } }, "unique-string": { "version": "1.0.0", - "resolved": "", "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "bundled": true, "requires": { "crypto-random-string": "^1.0.0" } }, "unpipe": { "version": "1.0.0", - "resolved": "", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "bundled": true }, "unzip-response": { "version": "2.0.1", - "resolved": "", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "bundled": true }, "update-notifier": { "version": "2.5.0", - "resolved": "", "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "bundled": true, "requires": { "boxen": "^1.2.1", "chalk": "^2.0.1", @@ -6628,39 +19348,39 @@ }, "url-parse-lax": { "version": "1.0.0", - "resolved": "", "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "bundled": true, "requires": { "prepend-http": "^1.0.1" } }, "util-deprecate": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "bundled": true }, "util-extend": { "version": "1.0.3", - "resolved": "", - "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=" + "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=", + "bundled": true }, "util-promisify": { "version": "2.1.0", - "resolved": "", "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "bundled": true, "requires": { "object.getownpropertydescriptors": "^2.0.3" } }, "uuid": { "version": "3.3.3", - "resolved": "", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "bundled": true }, "validate-npm-package-license": { "version": "3.0.4", - "resolved": "", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "bundled": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -6668,16 +19388,16 @@ }, "validate-npm-package-name": { "version": "3.0.0", - "resolved": "", "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "bundled": true, "requires": { "builtins": "^1.0.3" } }, "verror": { "version": "1.10.0", - "resolved": "", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "bundled": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -6686,37 +19406,37 @@ }, "wcwidth": { "version": "1.0.1", - "resolved": "", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "bundled": true, "requires": { "defaults": "^1.0.3" } }, "which": { "version": "1.3.1", - "resolved": "", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "bundled": true, "requires": { "isexe": "^2.0.0" } }, "which-module": { "version": "2.0.0", - "resolved": "", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "bundled": true }, "wide-align": { "version": "1.1.2", - "resolved": "", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "bundled": true, "requires": { "string-width": "^1.0.2" }, "dependencies": { "string-width": { "version": "1.0.2", - "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6727,50 +19447,69 @@ }, "widest-line": { "version": "2.0.1", - "resolved": "", "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "bundled": true, "requires": { "string-width": "^2.1.1" } }, "worker-farm": { "version": "1.7.0", - "resolved": "", "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "bundled": true, "requires": { "errno": "~0.1.7" } }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "bundled": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "bundled": true + }, "string-width": { - "version": "1.0.2", - "resolved": "", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "bundled": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" } } } }, "wrappy": { "version": "1.0.2", - "resolved": "", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "bundled": true }, "write-file-atomic": { "version": "2.4.3", - "resolved": "", "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "bundled": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -6779,56 +19518,124 @@ }, "xdg-basedir": { "version": "3.0.0", - "resolved": "", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "bundled": true }, "xtend": { "version": "4.0.1", - "resolved": "", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "bundled": true }, "y18n": { "version": "4.0.0", - "resolved": "", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "bundled": true }, "yallist": { "version": "3.0.3", - "resolved": "", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "bundled": true }, "yargs": { - "version": "11.1.1", - "resolved": "", - "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", + "version": "14.2.3", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "bundled": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" }, "dependencies": { - "y18n": { - "version": "3.2.1", - "resolved": "", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "ansi-regex": { + "version": "4.1.0", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "bundled": true + }, + "find-up": { + "version": "3.0.0", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "bundled": true + }, + "locate-path": { + "version": "3.0.0", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, "yargs-parser": { - "version": "9.0.2", - "resolved": "", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "version": "15.0.1", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "bundled": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "bundled": true + } } } } @@ -7050,9 +19857,9 @@ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" }, "object-keys": { "version": "1.1.1", @@ -7071,12 +19878,13 @@ } }, "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "es-abstract": "^1.18.0-next.1" } }, "observable-fns": { @@ -7148,6 +19956,20 @@ "resolved": "https://registry.npmjs.org/optional-js/-/optional-js-2.1.1.tgz", "integrity": "sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA==" }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -7195,6 +20017,15 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -7259,31 +20090,23 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.3.3.tgz", - "integrity": "sha512-wmUyoQM/Xzmo62wgOdQAn5tl7u+IA1ZYK7qbuppi+3E+Gj4hlUxVHjInulieWrd0SfHi/ADriTb5ILJ/lsJrSg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", + "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.3.0", - "pg-pool": "^3.2.1", - "pg-protocol": "^1.2.5", + "pg-connection-string": "^2.4.0", + "pg-pool": "^3.2.2", + "pg-protocol": "^1.4.0", "pg-types": "^2.1.0", - "pgpass": "1.x", - "semver": "4.3.2" - }, - "dependencies": { - "semver": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", - "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" - } + "pgpass": "1.x" } }, "pg-connection-string": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz", - "integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", + "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" }, "pg-int8": { "version": "1.0.1", @@ -7291,14 +20114,15 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.1.tgz", - "integrity": "sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", + "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==", + "requires": {} }, "pg-protocol": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.2.5.tgz", - "integrity": "sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", + "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" }, "pg-types": { "version": "2.2.0", @@ -7313,11 +20137,11 @@ } }, "pgpass": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", - "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", "requires": { - "split": "^1.0.0" + "split2": "^3.1.1" } }, "picomatch": { @@ -7393,6 +20217,12 @@ "xtend": "^4.0.0" } }, + "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 + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7407,6 +20237,12 @@ "fromentries": "^1.2.0" } }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", @@ -7453,6 +20289,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, "randexp": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", @@ -7554,6 +20396,12 @@ "redis-errors": "^1.0.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "rehype": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/rehype/-/rehype-10.0.0.tgz", @@ -7564,38 +20412,21 @@ "unified": "^9.0.0" } }, - "rehype-format": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-3.0.1.tgz", - "integrity": "sha512-rBIoIt63o+aKqIXyWV/lKE5dRoqXZkNjADvdVxHeSWqv862Y34FfcQJlXpLglhMIGrSW3GODEtdHyN+XTmdAHw==", - "requires": { - "hast-util-embedded": "^1.0.1", - "hast-util-phrasing": "^1.0.0", - "html-whitespace-sensitive-tag-names": "^1.0.0", - "rehype-minify-whitespace": "^3.0.0", - "repeat-string": "^1.5.4", - "unist-util-visit-parents": "^3.0.0" - } - }, "rehype-minify-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-3.0.0.tgz", - "integrity": "sha512-Z5NIG9FxTeK2Ta+eTWCnTVPXu1qC58eCXZA3m/Z7PPinKw82KSR+12c2l1sLLSg27QZOmZrrd9piS8dsAVfliQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-4.0.5.tgz", + "integrity": "sha512-QC3Z+bZ5wbv+jGYQewpAAYhXhzuH/TVRx7z08rurBmh9AbG8Nu8oJnvs9LWj43Fd/C7UIhXoQ7Wddgt+ThWK5g==", "requires": { - "collapse-white-space": "^1.0.0", "hast-util-embedded": "^1.0.0", - "hast-util-has-property": "^1.0.0", - "hast-util-is-body-ok-link": "^1.0.0", "hast-util-is-element": "^1.0.0", - "html-whitespace-sensitive-tag-names": "^1.0.0", - "unist-util-is": "^4.0.0", - "unist-util-modify-children": "^1.0.0" + "hast-util-whitespace": "^1.0.4", + "unist-util-is": "^4.0.0" }, "dependencies": { "unist-util-is": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz", - "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", + "integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==" } } }, @@ -7627,11 +20458,6 @@ "es6-error": "^4.0.1" } }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, "replace-ext": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", @@ -7758,6 +20584,18 @@ "statuses": "~1.5.0" }, "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -7814,21 +20652,21 @@ "dev": true }, "simple-git": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.20.1.tgz", - "integrity": "sha512-aa9s2ZLjXlHCVGbDXQLInMLvLkxKEclqMU9X5HMXi3tLWLxbWObz1UgtyZha6ocHarQtFp0OjQW9KHVR1g6wbA==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.27.0.tgz", + "integrity": "sha512-/Q4aolzErYrIx6SgyH421jmtv5l1DaAw+KYWMWy229+isW6yld/nHGxJ2xUR/aeX3SuYJnbucyUigERwaw4Xow==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.1.1" + "debug": "^4.3.1" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -7838,6 +20676,63 @@ } } }, + "sinon": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.0.tgz", + "integrity": "sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.2.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.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", + "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" + } + } + } + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "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=", + "dev": true + } + } + }, "slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", @@ -8016,12 +20911,12 @@ } } }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "requires": { - "through": "2" + "readable-stream": "^3.0.0" } }, "sprintf-js": { @@ -8031,98 +20926,64 @@ }, "sqlstring": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, - "string.prototype.trimleft": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", - "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "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": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimstart": "^1.0.0" + "safe-buffer": "~5.2.0" } }, - "string.prototype.trimright": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", - "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimend": "^1.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "call-bind": "^1.0.0", + "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==", + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", "requires": { - "safe-buffer": "~5.2.0" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "stringify-entities": { @@ -8138,11 +20999,11 @@ } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.0" } }, "strip-bom": { @@ -8235,12 +21096,86 @@ "requires": { "methods": "^1.1.2", "superagent": "^3.8.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + } } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } }, "swagger-parser": { "version": "9.0.1", @@ -8260,6 +21195,58 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.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==", + "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=", + "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==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.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==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "tar-stream": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", @@ -8297,9 +21284,9 @@ }, "dependencies": { "@types/node": { - "version": "12.12.58", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz", - "integrity": "sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA==" + "version": "12.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz", + "integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==" }, "bl": { "version": "3.0.1", @@ -8366,6 +21353,12 @@ } } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "threads": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/threads/-/threads-1.4.1.tgz", @@ -8393,11 +21386,6 @@ } } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, "tiny-worker": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", @@ -8450,9 +21438,9 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tunnel": { "version": "0.0.6", @@ -8472,6 +21460,15 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "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, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8502,9 +21499,9 @@ } }, "ueberdb2": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.3.tgz", - "integrity": "sha512-812rKRIkJD+2XX7boh7vsexzemj9/oT1KnAR7O/bAbIZIZBs2zZegEJr/pdJvzF+AIem+CVNHp1ankOpqKCKkA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-0.5.6.tgz", + "integrity": "sha512-stLhNkWlxUMAO33JjEh8JCRuZvHYeDQjbo6K1C3I7R37AlMKNu9GWXSZm1wQDnAqpXAXeMVh3owBsAdj0YvOrg==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", @@ -8514,7 +21511,7 @@ "dirty": "^1.1.0", "elasticsearch": "^16.7.1", "mocha": "^7.1.2", - "mssql": "^6.2.1", + "mssql": "^6.2.3", "mysql": "2.18.1", "nano": "^8.2.2", "pg": "^8.0.3", @@ -8523,6 +21520,60 @@ "rethinkdb": "^2.4.2", "rimraf": "^3.0.2", "simple-git": "^2.4.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "mocha": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", + "requires": { + "ansi-colors": "3.2.3", + "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", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.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", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + } + }, + "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==" + }, + "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==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "uid-safe": { @@ -8556,14 +21607,6 @@ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" }, - "unist-util-modify-children": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-1.1.6.tgz", - "integrity": "sha512-TOA6W9QLil+BrHqIZNR4o6IA5QwGOveMbnQxnWYq+7EFORx9vz/CHrtzF36zWrW61E2UKw7sM1KPtIgeceVwXw==", - "requires": { - "array-iterate": "^1.0.0" - } - }, "unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -8572,22 +21615,6 @@ "@types/unist": "^2.0.2" } }, - "unist-util-visit-parents": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz", - "integrity": "sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g==", - "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "dependencies": { - "unist-util-is": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz", - "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==" - } - } - }, "unorm": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", @@ -8621,6 +21648,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, "validator": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", @@ -8768,8 +21801,43 @@ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "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=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } } }, + "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==", + "dev": true + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", @@ -8785,13 +21853,15 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" }, "string-width": { "version": "3.1.0", @@ -8833,13 +21903,8 @@ "ws": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", - "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" - }, - "wtfnode": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.3.tgz", - "integrity": "sha512-Ll7iH8MbRQTE+QTw20Xax/0PM5VeSVSOhsmoR3+knWuJkEWTV5d9yPO6Sb+IDbt9I4UCrKpvHuF9T9zteRNOuA==", - "dev": true + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", + "requires": {} }, "xml2js": { "version": "0.4.23", @@ -8856,9 +21921,9 @@ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "xmldom": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz", - "integrity": "sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g==" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz", + "integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA==" }, "xmlhttprequest-ssl": { "version": "1.5.5", @@ -8876,9 +21941,15 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yargs": { "version": "13.3.2", @@ -8902,6 +21973,16 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "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=" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", diff --git a/src/package.json b/src/package.json index b01e07a02af..c98c233891b 100644 --- a/src/package.json +++ b/src/package.json @@ -45,18 +45,21 @@ "find-root": "1.1.0", "formidable": "1.2.1", "graceful-fs": "4.2.4", - "http-errors": "1.7.3", + "http-errors": "1.8.0", + "js-cookie": "^2.2.1", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", "log4js": "0.6.35", "measured-core": "1.11.2", + "mime-types": "^2.1.27", "nodeify": "1.0.1", - "npm": "6.14.5", + "npm": "6.14.8", "openapi-backend": "2.4.1", + "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.1.4", "rehype": "^10.0.0", - "rehype-format": "^3.0.1", + "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", "resolve": "1.1.7", "security": "1.0.0", @@ -67,21 +70,74 @@ "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.0.1", - "ueberdb2": "^0.5.3", + "ueberdb2": "^0.5.6", "underscore": "1.8.3", "unorm": "1.4.1" }, "bin": { - "etherpad-lite": "./node/server.js" + "etherpad-lite": "node/server.js" }, "devDependencies": { + "eslint": "^7.15.0", + "eslint-config-etherpad": "^1.0.20", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", + "etherpad-cli-client": "0.0.9", "mocha": "7.1.2", "mocha-froth": "^0.2.10", "nyc": "15.0.1", "set-cookie-parser": "^2.4.6", + "sinon": "^9.2.0", + "superagent": "^3.8.3", "supertest": "4.0.2", - "wd": "1.12.1", - "wtfnode": "^0.8.3" + "wd": "1.12.1" + }, + "eslintConfig": { + "ignorePatterns": [ + "/static/js/admin/jquery.autosize.js", + "/static/js/admin/minify.json.js", + "/static/js/browser.js", + "/static/js/excanvas.js", + "/static/js/farbtastic.js", + "/static/js/gritter.js", + "/static/js/html10n.js", + "/static/js/jquery.js", + "/static/js/vendors/nice-select.js" + ], + "overrides": [ + { + "files": [ + "**/.eslintrc.js" + ], + "extends": "etherpad/node" + }, + { + "files": [ + "**/*" + ], + "excludedFiles": [ + "**/.eslintrc.js" + ], + "extends": "etherpad/node" + }, + { + "files": [ + "static/**/*" + ], + "excludedFiles": [ + "**/.eslintrc.js" + ], + "extends": "etherpad/browser", + "env": { + "shared-node-browser": true + } + } + ], + "root": true }, "engines": { "node": ">=10.13.0", @@ -92,9 +148,10 @@ "url": "https://github.com/ether/etherpad-lite.git" }, "scripts": { - "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "lint": "eslint .", + "test": "nyc mocha --timeout 30000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api" }, - "version": "1.8.6", + "version": "1.8.7", "license": "Apache-2.0" } diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 2043c3fffa2..6edd08d48a2 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -15,7 +15,6 @@ html { font-size: 15px; - line-height: 20px; color: #3e3e3e; } @@ -71,7 +70,3 @@ input { @media (max-width: 800px) { .hide-for-mobile { display: none; } } - -#importmessagepermission { - display: none; -} diff --git a/src/static/css/pad/form.css b/src/static/css/pad/form.css index 92f9a853014..5475d7d5c6a 100644 --- a/src/static/css/pad/form.css +++ b/src/static/css/pad/form.css @@ -16,8 +16,8 @@ select, .nice-select { padding-right: 24px; position: relative; text-align: left !important; - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -26,6 +26,9 @@ select, .nice-select { min-width: 100px; text-transform: capitalize; } +.nice-select:not(.open):not(:hover):focus { + border-color: #a5c8ec; +} .popup .nice-select { padding: 4px 24px 4px 8px; } diff --git a/src/static/css/pad/gritter.css b/src/static/css/pad/gritter.css index e17367efa7b..62a1e3de970 100644 --- a/src/static/css/pad/gritter.css +++ b/src/static/css/pad/gritter.css @@ -31,6 +31,8 @@ .gritter-item .gritter-content { flex: 1 auto; text-align: center; + width: 95%; + overflow-wrap: break-word; } .gritter-item .gritter-close { @@ -48,4 +50,4 @@ right: 1rem; transform: none; } -} \ No newline at end of file +} diff --git a/src/static/css/pad/layout.css b/src/static/css/pad/layout.css index 7b64936d74f..e5b79c2688c 100644 --- a/src/static/css/pad/layout.css +++ b/src/static/css/pad/layout.css @@ -47,8 +47,6 @@ body { width: 0; /* hide when the container is empty */ } -@media only screen and (max-width: 800px) { - #editorcontainerbox { - margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ - } +.mobile-layout #editorcontainerbox { + margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */ } diff --git a/src/static/css/pad/loadingbox.css b/src/static/css/pad/loadingbox.css index 5e990cd7255..8f8d9363224 100644 --- a/src/static/css/pad/loadingbox.css +++ b/src/static/css/pad/loadingbox.css @@ -1,17 +1,14 @@ #editorloadingbox { - padding-top: 100px; - padding-bottom: 100px; - font-size: 2.5em; - color: #aaa; - text-align: center; - position: absolute; width: 100%; - height: 30px; z-index: 100; + position: absolute; } -#editorloadingbox .passForm{ - padding:10px; +.editorloadingbox-message { + padding-top: 100px; + font-size: 2.5em; + color: #aaa; + text-align: center; } #editorloadingbox input{ @@ -22,6 +19,6 @@ padding:10px; } -#passwordRequired, #permissionDenied, #wrongPassword, #noCookie { +#permissionDenied, #noCookie { display:none; -} \ No newline at end of file +} diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 00fc8ca5171..0eb00099673 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -78,9 +78,11 @@ .popup#users .popup-content { overflow: visible; } - /* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ - .popup:not(.toolbar-popup) { - top: auto; - bottom: 1rem; - } -} \ No newline at end of file +} +/* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */ +.mobile-layout .popup:not(.toolbar-popup) { + top: auto; + left: 1rem; + right: auto; + bottom: 1rem; +} diff --git a/src/static/css/pad/popup_users.css b/src/static/css/pad/popup_users.css index 8b6ba82bc7c..c36da0ef77f 100644 --- a/src/static/css/pad/popup_users.css +++ b/src/static/css/pad/popup_users.css @@ -48,6 +48,7 @@ border: 1px solid #ccc; background: transparent; cursor: pointer; + flex-shrink: 0; } #myswatch { width: 100%; @@ -98,13 +99,15 @@ input#myusernameedit:not(.editable) { right: calc(100% + 15px); z-index: 101; } -@media (max-width: 800px) { - #mycolorpicker.popup { - top: auto; - bottom: 0; - left: auto !important; - right: 0 !important; - } +.mobile-layout #users.popup { + right: 1rem; + left: auto; +} +.mobile-layout #mycolorpicker.popup { + top: auto; + bottom: 0; + left: auto !important; + right: 0 !important; } #mycolorpicker.popup .btn-container { margin-top: 10px; diff --git a/src/static/css/pad/toolbar.css b/src/static/css/pad/toolbar.css index bc258510a60..13f7440f5e1 100644 --- a/src/static/css/pad/toolbar.css +++ b/src/static/css/pad/toolbar.css @@ -45,7 +45,7 @@ text-decoration: none; transition: background-color .1s; } -.toolbar ul li button:active, .toolbar ul li button:focus { +.toolbar ul li a.pressed button:active, .toolbar ul li a.pressed button:focus { outline: 0; border: none; } @@ -139,37 +139,40 @@ .toolbar ul li.separator { width: 5px; } - /* menu_right act like a new toolbar on the bottom of the screen */ - .toolbar .menu_right { - position: fixed; - bottom: 0; - right: 0; - left: 0; - border-top: 1px solid #ccc; - background-color: #f4f4f4; - padding: 0 5px 5px 5px; - } - .toolbar ul.menu_right > li { - margin-right: 8px; - } - .toolbar ul.menu_right > li.separator { - display: none; - } - .toolbar ul.menu_right > li a { - border: none; - background-color: transparent; - margin-left: 5px; - } - .toolbar ul.menu_right > li[data-key="showusers"] { - position: absolute; - right: 0; - top: 0; - bottom: 0; - margin: 0; - } - .toolbar ul.menu_right > li[data-key="showusers"] a { - height: 100%; - width: 40px; - border-radius: 0; - } -} \ No newline at end of file +} + +/* menu_right act like a new toolbar on the bottom of the screen */ +.mobile-layout .toolbar .menu_right { + position: fixed; + bottom: 0; + right: 0; + left: 0; + border-top: 1px solid #ccc; + background-color: #f4f4f4; + padding: 0 5px 5px 5px; +} +.mobile-layout .toolbar ul.menu_right > li { + margin-right: 8px; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] { + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: 0; +} +.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] a { + height: 100%; + width: 40px; + border-radius: 0; +} +.mobile-layout .toolbar ul.menu_right > li.separator { + display: none; +} +.mobile-layout .toolbar ul.menu_right > li a { + border: none; + margin-left: 5px; +} +.mobile-layout .toolbar ul.menu_right > li a:not(.selected) { + background-color: transparent; +} diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 17b21662487..c1257a9bc20 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -1,18 +1,18 @@ -var Changeset = require('./Changeset'); -var ChangesetUtils = require('./ChangesetUtils'); -var _ = require('./underscore'); +const Changeset = require('./Changeset'); +const ChangesetUtils = require('./ChangesetUtils'); +const _ = require('./underscore'); -var lineMarkerAttribute = 'lmkr'; +const lineMarkerAttribute = 'lmkr'; // Some of these attributes are kept for compatibility purposes. // Not sure if we need all of them -var DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; +const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; // If one of these attributes are set to the first character of a // line it is considered as a line attribute marker i.e. attributes // set on this marker are applied to the whole line. // The list attribute is only maintained for compatibility reasons -var lineAttributes = [lineMarkerAttribute,'list']; +const lineAttributes = [lineMarkerAttribute, 'list']; /* The Attribute manager builds changesets based on a document @@ -29,8 +29,7 @@ var lineAttributes = [lineMarkerAttribute,'list']; - a SkipList `lines` containing the text lines of the document. */ -var AttributeManager = function(rep, applyChangesetCallback) -{ +const AttributeManager = function (rep, applyChangesetCallback) { this.rep = rep; this.applyChangesetCallback = applyChangesetCallback; this.author = ''; @@ -44,12 +43,11 @@ AttributeManager.lineAttributes = lineAttributes; AttributeManager.prototype = _(AttributeManager.prototype).extend({ - applyChangeset: function(changeset){ - if(!this.applyChangesetCallback) return changeset; + applyChangeset(changeset) { + if (!this.applyChangesetCallback) return changeset; - var cs = changeset.toString(); - if (!Changeset.isIdentity(cs)) - { + const cs = changeset.toString(); + if (!Changeset.isIdentity(cs)) { this.applyChangesetCallback(cs); } @@ -62,18 +60,17 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param end [row, col] tuple pointing to the end of the range @param attribs: an array of attributes */ - setAttributesOnRange: function(start, end, attribs) - { + setAttributesOnRange(start, end, attribs) { // instead of applying the attributes to the whole range at once, we need to apply them // line by line, to be able to disregard the "*" used as line marker. For more details, // see https://github.com/ether/etherpad-lite/issues/2772 - var allChangesets; - for(var row = start[0]; row <= end[0]; row++) { - var rowRange = this._findRowRange(row, start, end); - var startCol = rowRange[0]; - var endCol = rowRange[1]; + let allChangesets; + for (let row = start[0]; row <= end[0]; row++) { + const rowRange = this._findRowRange(row, start, end); + const startCol = rowRange[0]; + const endCol = rowRange[1]; - var rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); + const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); // compose changesets of all rows into a single changeset, as the range might not be continuous // due to the presence of line markers on the rows @@ -87,13 +84,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ return this.applyChangeset(allChangesets); }, - _findRowRange: function(row, start, end) - { - var startCol, endCol; + _findRowRange(row, start, end) { + let startCol, endCol; - var startLineOffset = this.rep.lines.offsetOfIndex(row); - var endLineOffset = this.rep.lines.offsetOfIndex(row+1); - var lineLength = endLineOffset - startLineOffset; + const startLineOffset = this.rep.lines.offsetOfIndex(row); + const endLineOffset = this.rep.lines.offsetOfIndex(row + 1); + const lineLength = endLineOffset - startLineOffset; // find column where range on this row starts if (row === start[0]) { // are we on the first row of range? @@ -119,9 +115,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param endCol column where range ends @param attribs: an array of attributes */ - _setAttributesOnRangeByLine: function(row, startCol, endCol, attribs) - { - var builder = Changeset.builder(this.rep.lines.totalWidth()); + _setAttributesOnRangeByLine(row, startCol, endCol, attribs) { + const builder = Changeset.builder(this.rep.lines.totalWidth()); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool); return builder; @@ -131,12 +126,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ Returns if the line already has a line marker @param lineNum: the number of the line */ - lineHasMarker: function(lineNum){ - var that = this; + lineHasMarker(lineNum) { + const that = this; - return _.find(lineAttributes, function(attribute){ - return that.getAttributeOnLine(lineNum, attribute) != ''; - }) !== undefined; + return _.find(lineAttributes, (attribute) => that.getAttributeOnLine(lineNum, attribute) != '') !== undefined; }, /* @@ -144,14 +137,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param lineNum: the number of the line to set the attribute for @param attributeKey: the name of the attribute to get, e.g. list */ - getAttributeOnLine: function(lineNum, attributeName){ + getAttributeOnLine(lineNum, attributeName) { // get `attributeName` attribute of first char of line - var aline = this.rep.alines[lineNum]; - if (aline) - { - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) - { + const aline = this.rep.alines[lineNum]; + if (aline) { + const opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) { return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || ''; } } @@ -162,97 +153,94 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ Gets all attributes on a line @param lineNum: the number of the line to get the attribute for */ - getAttributesOnLine: function(lineNum){ + getAttributesOnLine(lineNum) { // get attributes of first char of line - var aline = this.rep.alines[lineNum]; - var attributes = [] - if (aline) - { - var opIter = Changeset.opIterator(aline) - , op - if (opIter.hasNext()) - { - op = opIter.next() - if(!op.attribs) return [] - - Changeset.eachAttribNumber(op.attribs, function(n) { - attributes.push([this.rep.apool.getAttribKey(n), this.rep.apool.getAttribValue(n)]) - }.bind(this)) + const aline = this.rep.alines[lineNum]; + const attributes = []; + if (aline) { + const opIter = Changeset.opIterator(aline); + let op; + if (opIter.hasNext()) { + op = opIter.next(); + if (!op.attribs) return []; + + Changeset.eachAttribNumber(op.attribs, (n) => { + attributes.push([this.rep.apool.getAttribKey(n), this.rep.apool.getAttribValue(n)]); + }); return attributes; } } return []; }, - /* + /* Gets a given attribute on a selection @param attributeName @param prevChar returns true or false if an attribute is visible in range */ - getAttributeOnSelection: function(attributeName, prevChar){ - var rep = this.rep; - if (!(rep.selStart && rep.selEnd)) return + getAttributeOnSelection(attributeName, prevChar) { + const rep = this.rep; + if (!(rep.selStart && rep.selEnd)) return; // If we're looking for the caret attribute not the selection // has the user already got a selection or is this purely a caret location? - var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if(isNotSelection){ - if(prevChar){ + const isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); + if (isNotSelection) { + if (prevChar) { // If it's not the start of the line - if(rep.selStart[1] !== 0){ + if (rep.selStart[1] !== 0) { rep.selStart[1]--; } } } - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); - function hasIt(attribs) - { + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); + function hasIt(attribs) { return withItRegex.test(attribs); } - return rangeHasAttrib(rep.selStart, rep.selEnd) + return rangeHasAttrib(rep.selStart, rep.selEnd); function rangeHasAttrib(selStart, selEnd) { // if range is collapsed -> no attribs in range - if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false + if (selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false; - if(selStart[0] != selEnd[0]) { // -> More than one line selected - var hasAttrib = true + if (selStart[0] != selEnd[0]) { // -> More than one line selected + var hasAttrib = true; // from selStart to the end of the first line - hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]) + hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); // for all lines in between - for(var n=selStart[0]+1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]) + for (let n = selStart[0] + 1; n < selEnd[0]; n++) { + hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); } // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]) + hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - return hasAttrib + return hasAttrib; } // Logic tells us we now have a range on a single line - var lineNum = selStart[0] - , start = selStart[1] - , end = selEnd[1] - , hasAttrib = true + const lineNum = selStart[0]; + const start = selStart[1]; + const end = selEnd[1]; + var hasAttrib = true; // Iterate over attribs on this line - var opIter = Changeset.opIterator(rep.alines[lineNum]) - , indexIntoLine = 0 + const opIter = Changeset.opIterator(rep.alines[lineNum]); + let indexIntoLine = 0; while (opIter.hasNext()) { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { // does op overlap selection? if (!(opEndInLine <= start || opStartInLine >= end)) { @@ -263,7 +251,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ indexIntoLine = opEndInLine; } - return hasAttrib + return hasAttrib; } }, @@ -274,40 +262,39 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ returns a list of attributes in the format [ ["key","value"], ["key","value"], ... ] */ - getAttributesOnPosition: function(lineNumber, column){ + getAttributesOnPosition(lineNumber, column) { // get all attributes of the line - var aline = this.rep.alines[lineNumber]; + const aline = this.rep.alines[lineNumber]; if (!aline) { - return []; + return []; } // iterate through all operations of a line - var opIter = Changeset.opIterator(aline); + const opIter = Changeset.opIterator(aline); // we need to sum up how much characters each operations take until the wanted position - var currentPointer = 0; - var attributes = []; - var currentOperation; + let currentPointer = 0; + const attributes = []; + let currentOperation; while (opIter.hasNext()) { currentOperation = opIter.next(); - currentPointer = currentPointer + currentOperation.chars; + currentPointer += currentOperation.chars; if (currentPointer > column) { // we got the operation of the wanted position, now collect all its attributes - Changeset.eachAttribNumber(currentOperation.attribs, function (n) { + Changeset.eachAttribNumber(currentOperation.attribs, (n) => { attributes.push([ this.rep.apool.getAttribKey(n), - this.rep.apool.getAttribValue(n) + this.rep.apool.getAttribValue(n), ]); - }.bind(this)); + }); // skip the loop return attributes; } } return attributes; - }, /* @@ -316,7 +303,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ returns a list of attributes in the format [ ["key","value"], ["key","value"], ... ] */ - getAttributesOnCaret: function(){ + getAttributesOnCaret() { return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); }, @@ -327,72 +314,72 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) */ - setAttributeOnLine: function(lineNum, attributeName, attributeValue){ - var loc = [0,0]; - var builder = Changeset.builder(this.rep.lines.totalWidth()); - var hasMarker = this.lineHasMarker(lineNum); + setAttributeOnLine(lineNum, attributeName, attributeValue) { + let loc = [0, 0]; + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); - if(hasMarker){ + if (hasMarker) { ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ - [attributeName, attributeValue] + [attributeName, attributeValue], + ], this.rep.apool); + } else { + // add a line marker + builder.insert('*', [ + ['author', this.author], + ['insertorder', 'first'], + [lineMarkerAttribute, '1'], + [attributeName, attributeValue], ], this.rep.apool); - }else{ - // add a line marker - builder.insert('*', [ - ['author', this.author], - ['insertorder', 'first'], - [lineMarkerAttribute, '1'], - [attributeName, attributeValue] - ], this.rep.apool); } return this.applyChangeset(builder); }, - /** + /** * Removes a specified attribute on a line * @param lineNum the number of the affected line * @param attributeName the name of the attribute to remove, e.g. list * @param attributeValue if given only attributes with equal value will be removed */ - removeAttributeOnLine: function(lineNum, attributeName, attributeValue){ - var builder = Changeset.builder(this.rep.lines.totalWidth()); - var hasMarker = this.lineHasMarker(lineNum); - var found = false; - - var attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) { - if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)){ - found = true; - return [attributeName, '']; - }else if (attrib[0] === 'author'){ - // update last author to make changes to line attributes on this line - return [attributeName, this.author]; - } - return attrib; - }); - - if (!found) { - return; - } - - ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); - - var countAttribsWithMarker = _.chain(attribs).filter(function(a){return !!a[1];}) - .map(function(a){return a[0];}).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); - - //if we have marker and any of attributes don't need to have marker. we need delete it - if(hasMarker && !countAttribsWithMarker){ - ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); - }else{ - ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool); - } - - return this.applyChangeset(builder); - }, - - /* + removeAttributeOnLine(lineNum, attributeName, attributeValue) { + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); + let found = false; + + const attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) { + if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { + found = true; + return [attributeName, '']; + } else if (attrib[0] === 'author') { + // update last author to make changes to line attributes on this line + return [attributeName, this.author]; + } + return attrib; + }); + + if (!found) { + return; + } + + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); + + const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) + .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); + + // if we have marker and any of attributes don't need to have marker. we need delete it + if (hasMarker && !countAttribsWithMarker) { + ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); + } else { + ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool); + } + + return this.applyChangeset(builder); + }, + + /* Toggles a line attribute for the specified line number If a line attribute with the specified name exists with any value it will be removed Otherwise it will be set to the given value @@ -400,20 +387,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param attributeKey: the name of the attribute to toggle, e.g. list @param attributeValue: the value to pass to the attribute (e.g. indention level) */ - toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) { - return this.getAttributeOnLine(lineNum, attributeName) ? - this.removeAttributeOnLine(lineNum, attributeName) : - this.setAttributeOnLine(lineNum, attributeName, attributeValue); - + toggleAttributeOnLine(lineNum, attributeName, attributeValue) { + return this.getAttributeOnLine(lineNum, attributeName) + ? this.removeAttributeOnLine(lineNum, attributeName) + : this.setAttributeOnLine(lineNum, attributeName, attributeValue); }, - hasAttributeOnSelectionOrCaretPosition: function(attributeName) { - var hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); - var hasAttrib; + hasAttributeOnSelectionOrCaretPosition(attributeName) { + const hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); + let hasAttrib; if (hasSelection) { hasAttrib = this.getAttributeOnSelection(attributeName); - }else { - var attributesOnCaretPosition = this.getAttributesOnCaret(); + } else { + const attributesOnCaretPosition = this.getAttributesOnCaret(); hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName); } return hasAttrib; diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 7e7634e4273..78d3e7c5b28 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -28,28 +28,28 @@ used to reference Attributes in Changesets. */ -var AttributePool = function () { +const AttributePool = function () { this.numToAttrib = {}; // e.g. {0: ['foo','bar']} this.attribToNum = {}; // e.g. {'foo,bar': 0} this.nextNum = 0; }; AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) { - var str = String(attrib); + const str = String(attrib); if (str in this.attribToNum) { return this.attribToNum[str]; } if (dontAddIfAbsent) { return -1; } - var num = this.nextNum++; + const num = this.nextNum++; this.attribToNum[str] = num; this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; return num; }; AttributePool.prototype.getAttrib = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) { return pair; } @@ -57,20 +57,20 @@ AttributePool.prototype.getAttrib = function (num) { }; AttributePool.prototype.getAttribKey = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[0]; }; AttributePool.prototype.getAttribValue = function (num) { - var pair = this.numToAttrib[num]; + const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[1]; }; AttributePool.prototype.eachAttrib = function (func) { - for (var n in this.numToAttrib) { - var pair = this.numToAttrib[n]; + for (const n in this.numToAttrib) { + const pair = this.numToAttrib[n]; func(pair[0], pair[1]); } }; @@ -78,7 +78,7 @@ AttributePool.prototype.eachAttrib = function (func) { AttributePool.prototype.toJsonable = function () { return { numToAttrib: this.numToAttrib, - nextNum: this.nextNum + nextNum: this.nextNum, }; }; @@ -86,7 +86,7 @@ AttributePool.prototype.fromJsonable = function (obj) { this.numToAttrib = obj.numToAttrib; this.nextNum = obj.nextNum; this.attribToNum = {}; - for (var n in this.numToAttrib) { + for (const n in this.numToAttrib) { this.attribToNum[String(this.numToAttrib[n])] = Number(n); } return this; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index e4e6d2d6391..422c7ede6ef 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -25,7 +25,7 @@ * limitations under the License. */ -var AttributePool = require("./AttributePool"); +const AttributePool = require('./AttributePool'); /** * ==================== General Util Functions ======================= @@ -36,7 +36,7 @@ var AttributePool = require("./AttributePool"); * @param msg {string} Just some message */ exports.error = function error(msg) { - var e = new Error(msg); + const e = new Error(msg); e.easysync = true; throw e; }; @@ -49,8 +49,8 @@ exports.error = function error(msg) { */ exports.assert = function assert(b, msgParts) { if (!b) { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - exports.error("Failed assertion: " + msg); + const msg = Array.prototype.slice.call(arguments, 1).join(''); + exports.error(`Failed assertion: ${msg}`); } }; @@ -79,12 +79,10 @@ exports.numToString = function (num) { * @return integer */ exports.toBaseTen = function (cs) { - var dollarIndex = cs.indexOf('$'); - var beforeDollar = cs.substring(0, dollarIndex); - var fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, function (s) { - return String(exports.parseNum(s)); - }) + fromDollar; + 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; }; @@ -116,29 +114,29 @@ exports.newLen = function (cs) { * @return {Op} type object iterator */ exports.opIterator = function (opsStr, optStartIndex) { - //print(opsStr); - var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - var startIndex = (optStartIndex || 0); - var curIndex = startIndex; - var prevIndex = curIndex; + // print(opsStr); + const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; + const startIndex = (optStartIndex || 0); + let curIndex = startIndex; + let prevIndex = curIndex; function nextRegexMatch() { prevIndex = curIndex; - var result; + let result; regex.lastIndex = curIndex; result = regex.exec(opsStr); curIndex = regex.lastIndex; if (result[0] == '?') { - exports.error("Hit error opcode in op stream"); + exports.error('Hit error opcode in op stream'); } return result; } - var regexResult = nextRegexMatch(); - var obj = exports.newOp(); + let regexResult = nextRegexMatch(); + const obj = exports.newOp(); function next(optObj) { - var op = (optObj || obj); + const op = (optObj || obj); if (regexResult[0]) { op.attribs = regexResult[1]; op.lines = exports.parseNum(regexResult[2] || 0); @@ -159,9 +157,9 @@ exports.opIterator = function (opsStr, optStartIndex) { return prevIndex; } return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex + next, + hasNext, + lastIndex, }; }; @@ -185,7 +183,7 @@ exports.newOp = function (optOpcode) { opcode: (optOpcode || ''), chars: 0, lines: 0, - attribs: '' + attribs: '', }; }; @@ -198,7 +196,7 @@ exports.cloneOp = function (op) { opcode: op.opcode, chars: op.chars, lines: op.lines, - attribs: op.attribs + attribs: op.attribs, }; }; @@ -220,7 +218,7 @@ exports.copyOp = function (op1, op2) { exports.opString = function (op) { // just for debugging if (!op.opcode) return 'null'; - var assem = exports.opAssembler(); + const assem = exports.opAssembler(); assem.append(op); return assem.toString(); }; @@ -240,33 +238,33 @@ exports.stringOp = function (str) { exports.checkRep = function (cs) { // doesn't check things that require access to attrib pool (e.g. attribute order) // or original string (e.g. newline positions) - var unpacked = exports.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; - - var assem = exports.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = exports.opIterator(ops); + const unpacked = exports.unpack(cs); + const oldLen = unpacked.oldLen; + const newLen = unpacked.newLen; + const ops = unpacked.ops; + let charBank = unpacked.charBank; + + const assem = exports.smartOpAssembler(); + let oldPos = 0; + let calcNewLen = 0; + let numInserted = 0; + const iter = exports.opIterator(ops); while (iter.hasNext()) { - var o = iter.next(); + const o = iter.next(); switch (o.opcode) { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - exports.assert(oldPos <= oldLen, oldPos, " > ", oldLen, " in ", cs); - break; - case '+': + case '=': + oldPos += o.chars; + calcNewLen += o.chars; + break; + case '-': + oldPos += o.chars; + exports.assert(oldPos <= oldLen, oldPos, ' > ', oldLen, ' in ', cs); + break; + case '+': { calcNewLen += o.chars; numInserted += o.chars; - exports.assert(calcNewLen <= newLen, calcNewLen, " > ", newLen, " in ", cs); + exports.assert(calcNewLen <= newLen, calcNewLen, ' > ', newLen, ' in ', cs); break; } } @@ -276,15 +274,15 @@ exports.checkRep = function (cs) { calcNewLen += oldLen - oldPos; charBank = charBank.substring(0, numInserted); while (charBank.length < numInserted) { - charBank += "?"; + charBank += '?'; } assem.endDocument(); - var normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); + const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); exports.assert(normalized == cs, 'Invalid changeset (checkRep failed)'); return cs; -} +}; /** @@ -303,12 +301,12 @@ exports.smartOpAssembler = function () { // - strips final "=" // - ignores 0-length changes // - reorders consecutive + and - (which margingOpAssembler doesn't do) - var minusAssem = exports.mergingOpAssembler(); - var plusAssem = exports.mergingOpAssembler(); - var keepAssem = exports.mergingOpAssembler(); - var assem = exports.stringAssembler(); - var lastOpcode = ''; - var lengthChange = 0; + const minusAssem = exports.mergingOpAssembler(); + const plusAssem = exports.mergingOpAssembler(); + const keepAssem = exports.mergingOpAssembler(); + const assem = exports.stringAssembler(); + let lastOpcode = ''; + let lengthChange = 0; function flushKeeps() { assem.append(keepAssem.toString()); @@ -348,9 +346,9 @@ exports.smartOpAssembler = function () { } function appendOpWithText(opcode, text, attribs, pool) { - var op = exports.newOp(opcode); + const op = exports.newOp(opcode); op.attribs = exports.makeAttribsString(opcode, attribs, pool); - var lastNewlinePos = text.lastIndexOf('\n'); + const lastNewlinePos = text.lastIndexOf('\n'); if (lastNewlinePos < 0) { op.chars = text.length; op.lines = 0; @@ -388,12 +386,12 @@ exports.smartOpAssembler = function () { } return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument, - appendOpWithText: appendOpWithText, - getLengthChange: getLengthChange + append, + toString, + clear, + endDocument, + appendOpWithText, + getLengthChange, }; }; @@ -403,14 +401,14 @@ exports.mergingOpAssembler = function () { // merges consecutive operations that are mergeable, ignores // no-ops, and drops final pure "keeps". It does not re-order // operations. - var assem = exports.opAssembler(); - var bufOp = exports.newOp(); + const assem = exports.opAssembler(); + const bufOp = exports.newOp(); // If we get, for example, insertions [xxx\n,yyy], those don't merge, // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. // This variable stores the length of yyy and any other newline-less // ops immediately after it. - var bufOpAdditionalCharsAfterNewline = 0; + let bufOpAdditionalCharsAfterNewline = 0; function flush(isEndDocument) { if (bufOp.opcode) { @@ -465,17 +463,16 @@ exports.mergingOpAssembler = function () { exports.clearOp(bufOp); } return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument + append, + toString, + clear, + endDocument, }; }; - exports.opAssembler = function () { - var pieces = []; + const pieces = []; // this function allows op to be mutated later (doesn't keep a ref) function append(op) { @@ -495,9 +492,9 @@ exports.opAssembler = function () { pieces.length = 0; } return { - append: append, - toString: toString, - clear: clear + append, + toString, + clear, }; }; @@ -506,28 +503,28 @@ exports.opAssembler = function () { * @param str {string} String to be iterated over */ exports.stringIterator = function (str) { - var curIndex = 0; + let curIndex = 0; // newLines is the number of \n between curIndex and str.length - var newLines = str.split("\n").length - 1 - function getnewLines(){ - return newLines + let newLines = str.split('\n').length - 1; + function getnewLines() { + return newLines; } function assertRemaining(n) { - exports.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); + exports.assert(n <= remaining(), '!(', n, ' <= ', remaining(), ')'); } function take(n) { assertRemaining(n); - var s = str.substr(curIndex, n); - newLines -= s.split("\n").length - 1 + const s = str.substr(curIndex, n); + newLines -= s.split('\n').length - 1; curIndex += n; return s; } function peek(n) { assertRemaining(n); - var s = str.substr(curIndex, n); + const s = str.substr(curIndex, n); return s; } @@ -540,11 +537,11 @@ exports.stringIterator = function (str) { return str.length - curIndex; } return { - take: take, - skip: skip, - remaining: remaining, - peek: peek, - newlines: getnewLines + take, + skip, + remaining, + peek, + newlines: getnewLines, }; }; @@ -552,7 +549,7 @@ exports.stringIterator = function (str) { * A custom made StringBuffer */ exports.stringAssembler = function () { - var pieces = []; + const pieces = []; function append(x) { pieces.push(String(x)); @@ -562,8 +559,8 @@ exports.stringAssembler = function () { return pieces.join(''); } return { - append: append, - toString: toString + append, + toString, }; }; @@ -581,11 +578,11 @@ exports.textLinesMutator = function (lines) { // is not actually a newline, but for the purposes of N and L values, // the caller should pretend it is, and for things to work right in that case, the input // to insert() should be a single line with no newlines. - var curSplice = [0, 0]; - var inSplice = false; + const curSplice = [0, 0]; + let inSplice = false; // position in document after curSplice is applied: - var curLine = 0, - curCol = 0; + let curLine = 0; + let curCol = 0; // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && // curLine >= curSplice[0] // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then @@ -617,7 +614,7 @@ exports.textLinesMutator = function (lines) { } function lines_length() { - if ((typeof lines.length) == "number") { + if ((typeof lines.length) === 'number') { return lines.length; } else { return lines.length(); @@ -645,7 +642,7 @@ exports.textLinesMutator = function (lines) { } function debugPrint(typ) { - print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); + print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`); } function putCurLineInSplice() { @@ -662,7 +659,7 @@ exports.textLinesMutator = function (lines) { if (!inSplice) { enterSplice(); } - for (var i = 0; i < L; i++) { + for (let i = 0; i < L; i++) { curCol = 0; putCurLineInSplice(); curLine++; @@ -678,14 +675,14 @@ exports.textLinesMutator = function (lines) { curLine += L; curCol = 0; } - //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); -/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + // print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); + /* if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { print("BLAH"); putCurLineInSplice(); }*/ // tests case foo in remove(), which isn't otherwise covered in current impl } - //debugPrint("skip"); + // debugPrint("skip"); } function skip(N, L, includeInSplice) { @@ -700,24 +697,24 @@ exports.textLinesMutator = function (lines) { putCurLineInSplice(); } curCol += N; - //debugPrint("skip"); + // debugPrint("skip"); } } } function removeLines(L) { - var removed = ''; + let removed = ''; if (L) { if (!inSplice) { enterSplice(); } function nextKLinesText(k) { - var m = curSplice[0] + curSplice[1]; + const m = curSplice[0] + curSplice[1]; return lines_slice(m, m + k).join(''); } if (isCurLineInSplice()) { - //print(curCol); + // print(curCol); if (curCol == 0) { removed = curSplice[curSplice.length - 1]; // print("FOO"); // case foo @@ -727,7 +724,7 @@ exports.textLinesMutator = function (lines) { } else { removed = nextKLinesText(L - 1); curSplice[1] += L - 1; - var sline = curSplice.length - 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; @@ -736,13 +733,13 @@ exports.textLinesMutator = function (lines) { removed = nextKLinesText(L); curSplice[1] += L; } - //debugPrint("remove"); + // debugPrint("remove"); } return removed; } function remove(N, L) { - var removed = ''; + let removed = ''; if (N) { if (L) { return removeLines(L); @@ -750,10 +747,10 @@ exports.textLinesMutator = function (lines) { if (!inSplice) { enterSplice(); } - var sline = putCurLineInSplice(); + const sline = putCurLineInSplice(); removed = curSplice[sline].substring(curCol, curCol + N); curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); - //debugPrint("remove"); + // debugPrint("remove"); } } return removed; @@ -765,18 +762,18 @@ exports.textLinesMutator = function (lines) { enterSplice(); } if (L) { - var newLines = exports.splitTextLines(text); + const newLines = exports.splitTextLines(text); if (isCurLineInSplice()) { - //if (curCol == 0) { - //curSplice.length--; - //curSplice[1]--; - //Array.prototype.push.apply(curSplice, newLines); - //curLine += newLines.length; - //} - //else { + // if (curCol == 0) { + // curSplice.length--; + // curSplice[1]--; + // Array.prototype.push.apply(curSplice, newLines); + // curLine += newLines.length; + // } + // else { var sline = curSplice.length - 1; - var theLine = curSplice[sline]; - var lineCol = curCol; + const theLine = curSplice[sline]; + const lineCol = curCol; curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; curLine++; newLines.splice(0, 1); @@ -784,7 +781,7 @@ exports.textLinesMutator = function (lines) { curLine += newLines.length; curSplice.push(theLine.substring(lineCol)); curCol = 0; - //} + // } } else { Array.prototype.push.apply(curSplice, newLines); curLine += newLines.length; @@ -792,18 +789,18 @@ exports.textLinesMutator = function (lines) { } else { var 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"); + 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; } - //debugPrint("insert"); + // debugPrint("insert"); } } function hasMore() { - //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); - var docLines = lines_length(); + // print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + let docLines = lines_length(); if (inSplice) { docLines += curSplice.length - 2 - curSplice[1]; } @@ -814,17 +811,17 @@ exports.textLinesMutator = function (lines) { if (inSplice) { leaveSplice(); } - //debugPrint("close"); + // debugPrint("close"); } - var self = { - skip: skip, - remove: remove, - insert: insert, - close: close, - hasMore: hasMore, - removeLines: removeLines, - skipLines: skipLines + const self = { + skip, + remove, + insert, + close, + hasMore, + removeLines, + skipLines, }; return self; }; @@ -845,18 +842,18 @@ exports.textLinesMutator = function (lines) { * @return {string} the integrated changeset */ exports.applyZip = function (in1, idx1, in2, idx2, func) { - var iter1 = exports.opIterator(in1, idx1); - var iter2 = exports.opIterator(in2, idx2); - var assem = exports.smartOpAssembler(); - var op1 = exports.newOp(); - var op2 = exports.newOp(); - var opOut = exports.newOp(); + const iter1 = exports.opIterator(in1, idx1); + const iter2 = exports.opIterator(in2, idx2); + 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) { - //print(opOut.toSource()); + // print(opOut.toSource()); assem.append(opOut); opOut.opcode = ''; } @@ -871,23 +868,23 @@ exports.applyZip = function (in1, idx1, in2, idx2, func) { * @returns {Changeset} a Changeset class */ exports.unpack = function (cs) { - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(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); - } - var oldLen = exports.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = exports.parseNum(headerMatch[3]); - var newLen = oldLen + changeSign * changeMag; - var opsStart = headerMatch[0].length; - var opsEnd = cs.indexOf("$"); + exports.error(`Not a exports: ${cs}`); + } + const oldLen = exports.parseNum(headerMatch[1]); + const changeSign = (headerMatch[2] == '>') ? 1 : -1; + const changeMag = exports.parseNum(headerMatch[3]); + const newLen = oldLen + changeSign * changeMag; + const opsStart = headerMatch[0].length; + let opsEnd = cs.indexOf('$'); if (opsEnd < 0) opsEnd = cs.length; return { - oldLen: oldLen, - newLen: newLen, + oldLen, + newLen, ops: cs.substring(opsStart, opsEnd), - charBank: cs.substring(opsEnd + 1) + charBank: cs.substring(opsEnd + 1), }; }; @@ -900,9 +897,9 @@ exports.unpack = function (cs) { * @returns {Changeset} a Changeset class */ exports.pack = function (oldLen, newLen, opsStr, bank) { - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + exports.numToString(lenDiff) : '<' + exports.numToString(-lenDiff)); - var a = []; + const lenDiff = newLen - oldLen; + const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}` : `<${exports.numToString(-lenDiff)}`); + const a = []; a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank); return a.join(''); }; @@ -913,39 +910,39 @@ exports.pack = function (oldLen, newLen, opsStr, bank) { * @params str {string} String to which a Changeset should be applied */ exports.applyToText = function (cs, str) { - var unpacked = exports.unpack(cs); - exports.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = exports.opIterator(unpacked.ops); - var bankIter = exports.stringIterator(unpacked.charBank); - var strIter = exports.stringIterator(str); - var assem = exports.stringAssembler(); + const unpacked = exports.unpack(cs); + exports.assert(str.length == unpacked.oldLen, 'mismatched apply: ', str.length, ' / ', unpacked.oldLen); + const csIter = exports.opIterator(unpacked.ops); + const bankIter = exports.stringIterator(unpacked.charBank); + const strIter = exports.stringIterator(str); + const assem = exports.stringAssembler(); while (csIter.hasNext()) { - var op = csIter.next(); + const op = csIter.next(); switch (op.opcode) { - case '+': - //op is + and op.lines 0: no newlines must be in op.chars - //op is + and op.lines >0: op.chars must include op.lines newlines - if(op.lines != bankIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op +; cs:"+cs+" and text:"+str); - } - assem.append(bankIter.take(op.chars)); - break; - case '-': - //op is - and op.lines 0: no newlines must be in the deleted string - //op is - and op.lines >0: op.lines newlines must be in the deleted string - if(op.lines != strIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op -; cs:"+cs+" and text:"+str); - } - strIter.skip(op.chars); - break; - case '=': - //op is = and op.lines 0: no newlines must be in the copied string - //op is = and op.lines >0: op.lines newlines must be in the copied string - if(op.lines != strIter.peek(op.chars).split("\n").length - 1){ - throw new Error("newline count is wrong in op =; cs:"+cs+" and text:"+str); - } - assem.append(strIter.take(op.chars)); - break; + case '+': + // op is + and op.lines 0: no newlines must be in op.chars + // op is + and op.lines >0: op.chars must include op.lines newlines + if (op.lines != bankIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op +; cs:${cs} and text:${str}`); + } + assem.append(bankIter.take(op.chars)); + break; + case '-': + // op is - and op.lines 0: no newlines must be in the deleted string + // op is - and op.lines >0: op.lines newlines must be in the deleted string + if (op.lines != strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op -; cs:${cs} and text:${str}`); + } + strIter.skip(op.chars); + break; + case '=': + // op is = and op.lines 0: no newlines must be in the copied string + // op is = and op.lines >0: op.lines newlines must be in the copied string + if (op.lines != strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op =; cs:${cs} and text:${str}`); + } + assem.append(strIter.take(op.chars)); + break; } } assem.append(strIter.take(strIter.remaining())); @@ -958,22 +955,22 @@ exports.applyToText = function (cs, str) { * @param lines The lines to which the changeset needs to be applied */ exports.mutateTextLines = function (cs, lines) { - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var bankIter = exports.stringIterator(unpacked.charBank); - var mut = exports.textLinesMutator(lines); + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const bankIter = exports.stringIterator(unpacked.charBank); + const mut = exports.textLinesMutator(lines); while (csIter.hasNext()) { - var op = csIter.next(); + const op = csIter.next(); switch (op.opcode) { - case '+': - mut.insert(bankIter.take(op.chars), op.lines); - break; - case '-': - mut.remove(op.chars, op.lines); - break; - case '=': - mut.skip(op.chars, op.lines, ( !! op.attribs)); - break; + case '+': + mut.insert(bankIter.take(op.chars), op.lines); + break; + case '-': + mut.remove(op.chars, op.lines); + break; + case '=': + mut.skip(op.chars, op.lines, (!!op.attribs)); + break; } } mut.close(); @@ -1008,16 +1005,16 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { return att2; } if (!att2) return att1; - var atts = []; - att1.replace(/\*([0-9a-z]+)/g, function (_, a) { + const atts = []; + att1.replace(/\*([0-9a-z]+)/g, (_, a) => { atts.push(pool.getAttrib(exports.parseNum(a))); return ''; }); - att2.replace(/\*([0-9a-z]+)/g, function (_, a) { - var pair = pool.getAttrib(exports.parseNum(a)); - var found = false; - for (var i = 0; i < atts.length; i++) { - var oldPair = atts[i]; + 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]; @@ -1034,12 +1031,12 @@ exports.composeAttributes = function (att1, att2, resultIsMutation, pool) { return ''; }); atts.sort(); - var buf = exports.stringAssembler(); - for (var i = 0; i < atts.length; i++) { + const buf = exports.stringAssembler(); + for (let i = 0; i < atts.length; i++) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(atts[i]))); } - //print(att1+" / "+att2+" / "+buf.toString()); + // print(att1+" / "+att2+" / "+buf.toString()); return buf.toString(); }; @@ -1051,7 +1048,7 @@ exports._slicerZipperFunc = function (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. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); if (attOp.opcode == '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1060,7 +1057,7 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { csOp.opcode = ''; } else { switch (csOp.opcode) { - case '-': + case '-': { if (csOp.chars <= attOp.chars) { // delete or delete part @@ -1090,14 +1087,14 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { } break; } - case '+': + case '+': { // insert exports.copyOp(csOp, opOut); csOp.opcode = ''; break; } - case '=': + case '=': { if (csOp.chars <= attOp.chars) { // keep or keep part @@ -1123,7 +1120,7 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { } break; } - case '': + case '': { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1140,30 +1137,28 @@ exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) { * @param pool {AttribsPool} the attibutes pool */ exports.applyToAttribution = function (cs, astr, pool) { - var unpacked = exports.unpack(cs); + const unpacked = exports.unpack(cs); - return exports.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) { - return exports._slicerZipperFunc(op1, op2, opOut, pool); - }); + return exports.applyZip(astr, 0, unpacked.ops, 0, (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); }; -/*exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { +/* exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { var iter = exports.opIterator(opsStr, optStartIndex); var bankIndex = 0; };*/ exports.mutateAttributionLines = function (cs, lines, pool) { - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var csBank = unpacked.charBank; - var csBankIndex = 0; + // dmesg(cs); + // dmesg(lines.toSource()+" ->"); + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const csBank = unpacked.charBank; + let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time - var mut = exports.textLinesMutator(lines); + const mut = exports.textLinesMutator(lines); - var lineIter = null; + let lineIter = null; function isNextMutOp() { return (lineIter && lineIter.hasNext()) || mut.hasMore(); @@ -1171,7 +1166,7 @@ exports.mutateAttributionLines = function (cs, lines, pool) { function nextMutOp(destOp) { if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { - var line = mut.removeLines(1); + const line = mut.removeLines(1); lineIter = exports.opIterator(line); } if (lineIter && lineIter.hasNext()) { @@ -1180,42 +1175,42 @@ exports.mutateAttributionLines = function (cs, lines, pool) { destOp.opcode = ''; } } - var lineAssem = null; + let lineAssem = null; function outputMutOp(op) { - //print("outputMutOp: "+op.toSource()); + // print("outputMutOp: "+op.toSource()); if (!lineAssem) { 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"); + 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; } } - var csOp = exports.newOp(); - var attOp = exports.newOp(); - var opOut = exports.newOp(); + const csOp = exports.newOp(); + const attOp = exports.newOp(); + const opOut = exports.newOp(); while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { if ((!csOp.opcode) && csIter.hasNext()) { csIter.next(csOp); } - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); - //print("csOp: "+csOp.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + // print("csOp: "+csOp.toSource()); if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { break; // done } else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { // skip multiple lines; this is what makes small changes not order of the document size mut.skipLines(csOp.lines); - //print("skipped: "+csOp.lines); + // print("skipped: "+csOp.lines); csOp.opcode = ''; } else if (csOp.opcode == '+') { if (csOp.lines > 1) { - var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; exports.copyOp(csOp, opOut); csOp.chars -= firstLineLen; csOp.lines--; @@ -1232,7 +1227,7 @@ exports.mutateAttributionLines = function (cs, lines, pool) { if ((!attOp.opcode) && isNextMutOp()) { nextMutOp(attOp); } - //print("attOp: "+attOp.toSource()); + // print("attOp: "+attOp.toSource()); exports._slicerZipperFunc(attOp, csOp, opOut, pool); if (opOut.opcode) { outputMutOp(opOut); @@ -1241,10 +1236,10 @@ exports.mutateAttributionLines = function (cs, lines, pool) { } } - exports.assert(!lineAssem, "line assembler not finished:"+cs); + exports.assert(!lineAssem, `line assembler not finished:${cs}`); mut.close(); - //dmesg("-> "+lines.toSource()); + // dmesg("-> "+lines.toSource()); }; /** @@ -1253,10 +1248,10 @@ exports.mutateAttributionLines = function (cs, lines, pool) { * @returns {string} joined Attribution lines */ exports.joinAttributionLines = function (theAlines) { - var assem = exports.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) { - var aline = theAlines[i]; - var iter = exports.opIterator(aline); + const assem = exports.mergingOpAssembler(); + for (let i = 0; i < theAlines.length; i++) { + const aline = theAlines[i]; + const iter = exports.opIterator(aline); while (iter.hasNext()) { assem.append(iter.next()); } @@ -1265,10 +1260,10 @@ exports.joinAttributionLines = function (theAlines) { }; exports.splitAttributionLines = function (attrOps, text) { - var iter = exports.opIterator(attrOps); - var assem = exports.mergingOpAssembler(); - var lines = []; - var pos = 0; + const iter = exports.opIterator(attrOps); + const assem = exports.mergingOpAssembler(); + const lines = []; + let pos = 0; function appendOp(op) { assem.append(op); @@ -1280,12 +1275,12 @@ exports.splitAttributionLines = function (attrOps, text) { } while (iter.hasNext()) { - var op = iter.next(); - var numChars = op.chars; - var numLines = op.lines; + const op = iter.next(); + let numChars = op.chars; + let numLines = op.lines; while (numLines > 1) { - var newlineEnd = text.indexOf('\n', pos) + 1; - exports.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + const newlineEnd = text.indexOf('\n', pos) + 1; + exports.assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); op.chars = newlineEnd - pos; op.lines = 1; appendOp(op); @@ -1317,24 +1312,24 @@ exports.splitTextLines = function (text) { * @param pool {AtribsPool} Attribs pool */ exports.compose = function (cs1, cs2, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - exports.assert(len2 == unpacked2.oldLen, "mismatched composition of two changesets"); - var len3 = unpacked2.newLen; - var bankIter1 = exports.stringIterator(unpacked1.charBank); - var bankIter2 = exports.stringIterator(unpacked2.charBank); - var bankAssem = exports.stringAssembler(); - - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { - //var debugBuilder = exports.stringAssembler(); - //debugBuilder.append(exports.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(exports.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; + 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) => { + // var debugBuilder = exports.stringAssembler(); + // debugBuilder.append(exports.opString(op1)); + // debugBuilder.append(','); + // debugBuilder.append(exports.opString(op2)); + // debugBuilder.append(' / '); + const op1code = op1.opcode; + const op2code = op2.opcode; if (op1code == '+' && op2code == '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } @@ -1347,12 +1342,12 @@ exports.compose = function (cs1, cs2, pool) { } } - //debugBuilder.append(exports.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(exports.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(exports.opString(opOut)); - //print(debugBuilder.toString()); + // debugBuilder.append(exports.opString(op1)); + // debugBuilder.append(','); + // debugBuilder.append(exports.opString(op2)); + // debugBuilder.append(' -> '); + // debugBuilder.append(exports.opString(opOut)); + // print(debugBuilder.toString()); }); return exports.pack(len1, len3, newOps, bankAssem.toString()); @@ -1369,11 +1364,11 @@ exports.attributeTester = function (attribPair, pool) { if (!pool) { return never; } - var attribNum = pool.putAttrib(attribPair, true); + const attribNum = pool.putAttrib(attribPair, true); if (attribNum < 0) { return never; } else { - var re = new RegExp('\\*' + exports.numToString(attribNum) + '(?!\\w)'); + const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); return function (attribs) { return re.test(attribs); }; @@ -1389,7 +1384,7 @@ exports.attributeTester = function (attribPair, pool) { * @param N {int} length of the identity changeset */ exports.identity = function (N) { - return exports.pack(N, N, "", ""); + return exports.pack(N, N, '', ''); }; @@ -1406,7 +1401,7 @@ exports.identity = function (N) { * @param pool {AttribPool} Attribution Pool */ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { - var oldLen = oldFullText.length; + const oldLen = oldFullText.length; if (spliceStart >= oldLen) { spliceStart = oldLen - 1; @@ -1414,10 +1409,10 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op if (numRemoved > oldFullText.length - spliceStart) { numRemoved = oldFullText.length - spliceStart; } - var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); - var newLen = oldLen + newText.length - oldText.length; + const oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); + const newLen = oldLen + newText.length - oldText.length; - var assem = exports.smartOpAssembler(); + const assem = exports.smartOpAssembler(); assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); assem.appendOpWithText('-', oldText); assem.appendOpWithText('+', newText, optNewTextAPairs, pool); @@ -1433,21 +1428,21 @@ exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, op */ exports.toSplices = function (cs) { // - var unpacked = exports.unpack(cs); - var splices = []; + const unpacked = exports.unpack(cs); + const splices = []; - var oldPos = 0; - var iter = exports.opIterator(unpacked.ops); - var charIter = exports.stringIterator(unpacked.charBank); - var inSplice = false; + let oldPos = 0; + const iter = exports.opIterator(unpacked.ops); + const charIter = exports.stringIterator(unpacked.charBank); + let inSplice = false; while (iter.hasNext()) { - var op = iter.next(); + const op = iter.next(); if (op.opcode == '=') { oldPos += op.chars; inSplice = false; } else { if (!inSplice) { - splices.push([oldPos, oldPos, ""]); + splices.push([oldPos, oldPos, '']); inSplice = true; } if (op.opcode == '-') { @@ -1466,16 +1461,16 @@ exports.toSplices = function (cs) { * */ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) { - var newStartChar = startChar; - var newEndChar = endChar; - var splices = exports.toSplices(cs); - var lengthChangeSoFar = 0; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - var spliceStart = splice[0] + lengthChangeSoFar; - var spliceEnd = splice[1] + lengthChangeSoFar; - var newTextLength = splice[2].length; - var thisLengthChange = newTextLength - (spliceEnd - spliceStart); + 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]; + const spliceStart = splice[0] + lengthChangeSoFar; + const spliceEnd = splice[1] + lengthChangeSoFar; + const newTextLength = splice[2].length; + const thisLengthChange = newTextLength - (spliceEnd - spliceStart); if (spliceStart <= newStartChar && spliceEnd >= newEndChar) { // splice fully replaces/deletes range @@ -1519,16 +1514,16 @@ exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter */ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { // works on exports or attribution string - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); - var fromDollar = cs.substring(dollarPos); + const upToDollar = cs.substring(0, dollarPos); + const fromDollar = cs.substring(dollarPos); // order of attribs stays the same - return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { - var oldNum = exports.parseNum(a); - var pair = oldPool.getAttrib(oldNum); + 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 @@ -1540,8 +1535,8 @@ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { pair = []; } - var newNum = newPool.putAttrib(pair); - return '*' + exports.numToString(newNum); + const newNum = newPool.putAttrib(pair); + return `*${exports.numToString(newNum)}`; }) + fromDollar; }; @@ -1550,7 +1545,7 @@ exports.moveOpsToNewPool = function (cs, oldPool, newPool) { * @param text {string} text to be inserted */ exports.makeAttribution = function (text) { - var assem = exports.smartOpAssembler(); + const assem = exports.smartOpAssembler(); assem.appendOpWithText('+', text); return assem.toString(); }; @@ -1562,13 +1557,13 @@ exports.makeAttribution = function (text) { * @param func {function} function to be called */ exports.eachAttribNumber = function (cs, func) { - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); + const upToDollar = cs.substring(0, dollarPos); - upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) { + upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { func(exports.parseNum(a)); return ''; }); @@ -1590,18 +1585,18 @@ exports.filterAttribNumbers = function (cs, filter) { * does exactly the same as exports.filterAttribNumbers */ exports.mapAttribNumbers = function (cs, func) { - var dollarPos = cs.indexOf('$'); + let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } - var upToDollar = cs.substring(0, dollarPos); + const upToDollar = cs.substring(0, dollarPos); - var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) { - var n = func(exports.parseNum(a)); + const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => { + const n = func(exports.parseNum(a)); if (n === true) { return s; - } else if ((typeof n) === "number") { - return '*' + exports.numToString(n); + } else if ((typeof n) === 'number') { + return `*${exports.numToString(n)}`; } else { return ''; } @@ -1618,8 +1613,8 @@ exports.mapAttribNumbers = function (cs, func) { */ exports.makeAText = function (text, attribs) { return { - text: text, - attribs: (attribs || exports.makeAttribution(text)) + text, + attribs: (attribs || exports.makeAttribution(text)), }; }; @@ -1632,7 +1627,7 @@ exports.makeAText = function (text, attribs) { exports.applyToAText = function (cs, atext, pool) { return { text: exports.applyToText(cs, atext.text), - attribs: exports.applyToAttribution(cs, atext.attribs, pool) + attribs: exports.applyToAttribution(cs, atext.attribs, pool), }; }; @@ -1644,9 +1639,9 @@ exports.cloneAText = function (atext) { if (atext) { return { text: atext.text, - attribs: atext.attribs - } - } else exports.error("atext is null"); + attribs: atext.attribs, + }; + } else { exports.error('atext is null'); } }; /** @@ -1665,8 +1660,8 @@ exports.copyAText = function (atext1, atext2) { */ exports.appendATextToAssembler = function (atext, assem) { // intentionally skips last newline char of atext - var iter = exports.opIterator(atext.attribs); - var op = exports.newOp(); + const iter = exports.opIterator(atext.attribs); + const op = exports.newOp(); while (iter.hasNext()) { iter.next(op); if (!iter.hasNext()) { @@ -1678,9 +1673,9 @@ exports.appendATextToAssembler = function (atext, assem) { assem.append(op); } } else { - var nextToLastNewlineEnd = + const nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; op.lines--; op.chars -= (lastLineLength + 1); assem.append(op); @@ -1702,11 +1697,11 @@ exports.appendATextToAssembler = function (atext, assem) { * @param pool {AtributePool} */ exports.prepareForWire = function (cs, pool) { - var newPool = new AttributePool(); - var newCs = exports.moveOpsToNewPool(cs, pool, newPool); + const newPool = new AttributePool(); + const newCs = exports.moveOpsToNewPool(cs, pool, newPool); return { translated: newCs, - pool: newPool + pool: newPool, }; }; @@ -1714,8 +1709,8 @@ exports.prepareForWire = function (cs, pool) { * Checks if a changeset s the identity changeset */ exports.isIdentity = function (cs) { - var unpacked = exports.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; + const unpacked = exports.unpack(cs); + return unpacked.ops == '' && unpacked.oldLen == unpacked.newLen; }; /** @@ -1737,9 +1732,9 @@ exports.opAttributeValue = function (op, key, pool) { * @param pool {AttribPool} attribute pool */ exports.attribsAttributeValue = function (attribs, key, pool) { - var value = ''; + let value = ''; if (attribs) { - exports.eachAttribNumber(attribs, function (n) { + exports.eachAttribNumber(attribs, (n) => { if (pool.getAttribKey(n) == key) { value = pool.getAttribValue(n); } @@ -1754,13 +1749,13 @@ exports.attribsAttributeValue = function (attribs, key, pool) { * @param oldLen {int} Old length */ exports.builder = function (oldLen) { - var assem = exports.smartOpAssembler(); - var o = exports.newOp(); - var charBank = exports.stringAssembler(); + const assem = exports.smartOpAssembler(); + const o = exports.newOp(); + const charBank = exports.stringAssembler(); var self = { // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) - keep: function (N, L, attribs, pool) { + keep(N, L, attribs, pool) { o.opcode = '='; o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; o.chars = N; @@ -1768,16 +1763,16 @@ exports.builder = function (oldLen) { assem.append(o); return self; }, - keepText: function (text, attribs, pool) { + keepText(text, attribs, pool) { assem.appendOpWithText('=', text, attribs, pool); return self; }, - insert: function (text, attribs, pool) { + insert(text, attribs, pool) { assem.appendOpWithText('+', text, attribs, pool); charBank.append(text); return self; }, - remove: function (N, L) { + remove(N, L) { o.opcode = '-'; o.attribs = ''; o.chars = N; @@ -1785,11 +1780,11 @@ exports.builder = function (oldLen) { assem.append(o); return self; }, - toString: function () { + toString() { assem.endDocument(); - var newLen = oldLen + assem.getLengthChange(); + const newLen = oldLen + assem.getLengthChange(); return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } + }, }; return self; @@ -1799,18 +1794,18 @@ exports.makeAttribsString = function (opcode, attribs, pool) { // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work if (!attribs) { return ''; - } else if ((typeof attribs) == "string") { + } else if ((typeof attribs) === 'string') { return attribs; } else if (pool && attribs && attribs.length) { if (attribs.length > 1) { attribs = attribs.slice(); attribs.sort(); } - var result = []; - for (var i = 0; i < attribs.length; i++) { - var pair = attribs[i]; + const result = []; + for (let i = 0; i < attribs.length; i++) { + const pair = attribs[i]; if (opcode == '=' || (opcode == '+' && pair[1])) { - result.push('*' + exports.numToString(pool.putAttrib(pair))); + result.push(`*${exports.numToString(pool.putAttrib(pair))}`); } } return result.join(''); @@ -1819,11 +1814,11 @@ exports.makeAttribsString = function (opcode, attribs, pool) { // like "substring" but on a single-line attribution string exports.subattribution = function (astr, start, optEnd) { - var iter = exports.opIterator(astr, 0); - var assem = exports.smartOpAssembler(); - var attOp = exports.newOp(); - var csOp = exports.newOp(); - var opOut = exports.newOp(); + const iter = exports.opIterator(astr, 0); + const assem = exports.smartOpAssembler(); + const attOp = exports.newOp(); + const csOp = exports.newOp(); + const opOut = exports.newOp(); function doCsOp() { if (csOp.chars) { @@ -1886,24 +1881,23 @@ exports.inverse = function (cs, lines, alines, pool) { } } - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = exports.newOp('+'); - - var unpacked = exports.unpack(cs); - var csIter = exports.opIterator(unpacked.ops); - var builder = exports.builder(unpacked.newLen); + let curLine = 0; + let curChar = 0; + let curLineOpIter = null; + let curLineOpIterLine; + const curLineNextOp = exports.newOp('+'); - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) { + const unpacked = exports.unpack(cs); + const csIter = exports.opIterator(unpacked.ops); + const builder = exports.builder(unpacked.newLen); + function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) { if ((!curLineOpIter) || (curLineOpIterLine != curLine)) { // create curLineOpIter and advance it to curChar curLineOpIter = exports.opIterator(alines_get(curLine)); curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; + let indexIntoLine = 0; + let done = false; while (!done && curLineOpIter.hasNext()) { curLineOpIter.next(curLineNextOp); if (indexIntoLine + curLineNextOp.chars >= curChar) { @@ -1926,7 +1920,7 @@ exports.inverse = function (cs, lines, alines, pool) { if (!curLineNextOp.chars) { curLineOpIter.next(curLineNextOp); } - var charsToUse = Math.min(numChars, curLineNextOp.chars); + const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); numChars -= charsToUse; curLineNextOp.chars -= charsToUse; @@ -1943,25 +1937,23 @@ exports.inverse = function (cs, lines, alines, pool) { if (L) { curLine += L; curChar = 0; + } else if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, () => {}); } else { - if (curLineOpIter && curLineOpIterLine == curLine) { - consumeAttribRuns(N, function () {}); - } else { - curChar += N; - } + curChar += N; } } function nextText(numChars) { - var len = 0; - var assem = exports.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); + let len = 0; + const assem = exports.stringAssembler(); + const firstString = lines_get(curLine).substring(curChar); len += firstString.length; assem.append(firstString); - var lineNum = curLine + 1; + let lineNum = curLine + 1; while (len < numChars) { - var nextString = lines_get(lineNum); + const nextString = lines_get(lineNum); len += nextString.length; assem.append(nextString); lineNum++; @@ -1971,7 +1963,7 @@ exports.inverse = function (cs, lines, alines, pool) { } function cachedStrFunc(func) { - var cache = {}; + const cache = {}; return function (s) { if (!cache[s]) { cache[s] = func(s); @@ -1980,31 +1972,31 @@ exports.inverse = function (cs, lines, alines, pool) { }; } - var attribKeys = []; - var attribValues = []; + const attribKeys = []; + const attribValues = []; while (csIter.hasNext()) { - var csOp = csIter.next(); + const csOp = csIter.next(); if (csOp.opcode == '=') { if (csOp.attribs) { attribKeys.length = 0; attribValues.length = 0; - exports.eachAttribNumber(csOp.attribs, function (n) { + exports.eachAttribNumber(csOp.attribs, (n) => { attribKeys.push(pool.getAttribKey(n)); attribValues.push(pool.getAttribValue(n)); }); - var undoBackToAttribs = cachedStrFunc(function (attribs) { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); + var undoBackToAttribs = cachedStrFunc((attribs) => { + const backAttribs = []; + for (let i = 0; i < attribKeys.length; i++) { + const appliedKey = attribKeys[i]; + const appliedValue = attribValues[i]; + const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); if (appliedValue != oldValue) { backAttribs.push([appliedKey, oldValue]); } } return exports.makeAttribsString('=', backAttribs, pool); }); - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); }); } else { @@ -2016,7 +2008,7 @@ exports.inverse = function (cs, lines, alines, pool) { } else if (csOp.opcode == '-') { var textBank = nextText(csOp.chars); var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) { + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.insert(textBank.substr(textBankIndex, len), attribs); textBankIndex += len; }); @@ -2028,33 +2020,33 @@ exports.inverse = function (cs, lines, alines, pool) { // %CLIENT FILE ENDS HERE% exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked2.oldLen; - exports.assert(len1 == len2, "mismatched follow - cannot transform cs1 on top of cs2"); - var chars1 = exports.stringIterator(unpacked1.charBank); - var chars2 = exports.stringIterator(unpacked2.charBank); + const unpacked1 = exports.unpack(cs1); + 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'); + const chars1 = exports.stringIterator(unpacked1.charBank); + const chars2 = exports.stringIterator(unpacked2.charBank); - var oldLen = unpacked1.newLen; - var oldPos = 0; - var newLen = 0; + const oldLen = unpacked1.newLen; + let oldPos = 0; + let newLen = 0; - var hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); + const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { + const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { if (op1.opcode == '+' || op2.opcode == '+') { - var whichToDo; + let whichToDo; if (op2.opcode != '+') { whichToDo = 1; } else if (op1.opcode != '+') { whichToDo = 2; } else { // both + - var firstChar1 = chars1.peek(1); - var firstChar2 = chars2.peek(1); - var insertFirst1 = hasInsertFirst(op1.attribs); - var insertFirst2 = hasInsertFirst(op2.attribs); + const firstChar1 = chars1.peek(1); + const firstChar2 = chars2.peek(1); + const insertFirst1 = hasInsertFirst(op1.attribs); + const insertFirst2 = hasInsertFirst(op2.attribs); if (insertFirst1 && !insertFirst2) { whichToDo = 1; } else if (insertFirst2 && !insertFirst1) { @@ -2089,19 +2081,17 @@ exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { } else if (op1.opcode == '-') { if (!op2.opcode) { op1.opcode = ''; - } else { - if (op1.chars <= op2.chars) { - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) { - op2.opcode = ''; - } - } else { - op1.chars -= op2.chars; - op1.lines -= op2.lines; + } else if (op1.chars <= op2.chars) { + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { op2.opcode = ''; } + } else { + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; } } else if (op2.opcode == '-') { exports.copyOp(op2, opOut); @@ -2153,16 +2143,16 @@ exports.follow = function (cs1, cs2, reverseInsertOrder, pool) { } } switch (opOut.opcode) { - case '=': - oldPos += opOut.chars; - newLen += opOut.chars; - break; - case '-': - oldPos += opOut.chars; - break; - case '+': - newLen += opOut.chars; - break; + case '=': + oldPos += opOut.chars; + newLen += opOut.chars; + break; + case '-': + oldPos += opOut.chars; + break; + case '+': + newLen += opOut.chars; + break; } }); newLen += oldLen - oldPos; @@ -2179,15 +2169,15 @@ exports.followAttributes = function (att1, att2, pool) { // to produce the merged set. if ((!att2) || (!pool)) return ''; if (!att1) return att2; - var atts = []; - att2.replace(/\*([0-9a-z]+)/g, function (_, a) { + const atts = []; + att2.replace(/\*([0-9a-z]+)/g, (_, a) => { atts.push(pool.getAttrib(exports.parseNum(a))); return ''; }); - att1.replace(/\*([0-9a-z]+)/g, function (_, a) { - var pair1 = pool.getAttrib(exports.parseNum(a)); - for (var i = 0; i < atts.length; i++) { - var pair2 = atts[i]; + 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 @@ -2199,8 +2189,8 @@ exports.followAttributes = function (att1, att2, pool) { return ''; }); // we've only removed attributes, so they're already sorted - var buf = exports.stringAssembler(); - for (var i = 0; i < atts.length; i++) { + const buf = exports.stringAssembler(); + for (let i = 0; i < atts.length; i++) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(atts[i]))); } @@ -2208,19 +2198,19 @@ exports.followAttributes = function (att1, att2, pool) { }; exports.composeWithDeletions = function (cs1, cs2, pool) { - var unpacked1 = exports.unpack(cs1); - var unpacked2 = exports.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - exports.assert(len2 == unpacked2.oldLen, "mismatched composition of two changesets"); - var len3 = unpacked2.newLen; - var bankIter1 = exports.stringIterator(unpacked1.charBank); - var bankIter2 = exports.stringIterator(unpacked2.charBank); - var bankAssem = exports.stringAssembler(); - - var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) { - var op1code = op1.opcode; - var op2code = op2.opcode; + 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)); } @@ -2239,11 +2229,11 @@ exports.composeWithDeletions = function (cs1, cs2, pool) { // 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= function (attOp, csOp, opOut, pool) { +exports._slicerZipperFuncWithDeletions = function (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. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + // print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); if (attOp.opcode == '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -2252,7 +2242,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { csOp.opcode = ''; } else { switch (csOp.opcode) { - case '-': + case '-': { if (csOp.chars <= attOp.chars) { // delete or delete part @@ -2260,7 +2250,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { opOut.opcode = '-'; opOut.chars = csOp.chars; opOut.lines = csOp.lines; - opOut.attribs = csOp.attribs; //changed by yammer + opOut.attribs = csOp.attribs; // changed by yammer } attOp.chars -= csOp.chars; attOp.lines -= csOp.lines; @@ -2274,7 +2264,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { opOut.opcode = '-'; opOut.chars = attOp.chars; opOut.lines = attOp.lines; - opOut.attribs = csOp.attribs; //changed by yammer + opOut.attribs = csOp.attribs; // changed by yammer } csOp.chars -= attOp.chars; csOp.lines -= attOp.lines; @@ -2282,14 +2272,14 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { } break; } - case '+': + case '+': { // insert exports.copyOp(csOp, opOut); csOp.opcode = ''; break; } - case '=': + case '=': { if (csOp.chars <= attOp.chars) { // keep or keep part @@ -2315,7 +2305,7 @@ exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) { } break; } - case '': + case '': { exports.copyOp(attOp, opOut); attOp.opcode = ''; diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js index e0b67881f03..c7333afcf53 100644 --- a/src/static/js/ChangesetUtils.js +++ b/src/static/js/ChangesetUtils.js @@ -18,43 +18,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -exports.buildRemoveRange = function(rep, builder, start, end) -{ - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); +exports.buildRemoveRange = function (rep, builder, start, end) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); - if (end[0] > start[0]) - { + if (end[0] > start[0]) { builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); builder.remove(end[1]); - } - else - { + } else { builder.remove(end[1] - start[1]); } -} +}; -exports.buildKeepRange = function(rep, builder, start, end, attribs, pool) -{ - var startLineOffset = rep.lines.offsetOfIndex(start[0]); - var endLineOffset = rep.lines.offsetOfIndex(end[0]); +exports.buildKeepRange = function (rep, builder, start, end, attribs, pool) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); - if (end[0] > start[0]) - { + if (end[0] > start[0]) { builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); builder.keep(end[1], 0, attribs, pool); - } - else - { + } else { builder.keep(end[1] - start[1], 0, attribs, pool); } -} +}; -exports.buildKeepToStartOfRange = function(rep, builder, start) -{ - var startLineOffset = rep.lines.offsetOfIndex(start[0]); +exports.buildKeepToStartOfRange = function (rep, builder, start) { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); builder.keep(startLineOffset, start[0]); builder.keep(start[1]); -} - +}; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 17834e435f8..12e3dc535cc 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -23,66 +23,57 @@ // requires: top // requires: undefined -var KERNEL_SOURCE = '../static/js/require-kernel.js'; +const KERNEL_SOURCE = '../static/js/require-kernel.js'; Ace2Editor.registry = { - nextId: 1 + nextId: 1, }; -var hooks = require('./pluginfw/hooks'); -var pluginUtils = require('./pluginfw/shared'); -var _ = require('./underscore'); +const hooks = require('./pluginfw/hooks'); +const pluginUtils = require('./pluginfw/shared'); +const _ = require('./underscore'); function scriptTag(source) { return ( - '' - ) + `` + ); } -function Ace2Editor() -{ - var ace2 = Ace2Editor; +function Ace2Editor() { + const ace2 = Ace2Editor; - var editor = {}; - var info = { - editor: editor, - id: (ace2.registry.nextId++) + const editor = {}; + let info = { + editor, + id: (ace2.registry.nextId++), }; - var loaded = false; - - var actionsPendingInit = []; - - function pendingInit(func, optDoNow) - { - return function() - { - var that = this; - var args = arguments; - var action = function() - { + let loaded = false; + + let actionsPendingInit = []; + + function pendingInit(func, optDoNow) { + return function () { + const that = this; + const args = arguments; + const action = function () { func.apply(that, args); - } - if (optDoNow) - { + }; + if (optDoNow) { optDoNow.apply(that, args); } - if (loaded) - { + if (loaded) { action(); - } - else - { + } else { actionsPendingInit.push(action); } }; } - function doActionsPendingInit() - { - _.each(actionsPendingInit, function(fn,i){ - fn() + function doActionsPendingInit() { + _.each(actionsPendingInit, (fn, i) => { + fn(); }); actionsPendingInit = []; } @@ -91,47 +82,56 @@ function Ace2Editor() // The following functions (prefixed by 'ace_') are exposed by editor, but // execution is delayed until init is complete - var aceFunctionsPendingInit = ['importText', 'importAText', 'focus', - 'setEditable', 'getFormattedCode', 'setOnKeyPress', 'setOnKeyDown', - 'setNotifyDirty', 'setProperty', 'setBaseText', 'setBaseAttributedText', - 'applyChangesToBase', 'applyPreparedChangesetToBase', - 'setUserChangeNotificationCallback', 'setAuthorInfo', - 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange']; - - _.each(aceFunctionsPendingInit, function(fnName,i){ - var prefix = 'ace_'; - var name = prefix + fnName; - editor[fnName] = pendingInit(function(){ - if(fnName === "setAuthorInfo"){ - if(!arguments[0]){ + const aceFunctionsPendingInit = ['importText', + 'importAText', + 'focus', + 'setEditable', + 'getFormattedCode', + 'setOnKeyPress', + 'setOnKeyDown', + 'setNotifyDirty', + 'setProperty', + 'setBaseText', + 'setBaseAttributedText', + 'applyChangesToBase', + 'applyPreparedChangesetToBase', + 'setUserChangeNotificationCallback', + 'setAuthorInfo', + 'setAuthorSelectionRange', + 'callWithAce', + 'execCommand', + 'replaceRange']; + + _.each(aceFunctionsPendingInit, (fnName, i) => { + const prefix = 'ace_'; + const name = prefix + fnName; + editor[fnName] = pendingInit(function () { + if (fnName === 'setAuthorInfo') { + if (!arguments[0]) { // setAuthorInfo AuthorId not set for some reason - }else{ + } else { info[prefix + fnName].apply(this, arguments); } - }else{ + } else { info[prefix + fnName].apply(this, arguments); } }); }); - editor.exportText = function() - { - if (!loaded) return "(awaiting init)\n"; + editor.exportText = function () { + if (!loaded) return '(awaiting init)\n'; return info.ace_exportText(); }; - editor.getFrame = function() - { + editor.getFrame = function () { return info.frame || null; }; - editor.getDebugProperty = function(prop) - { + editor.getDebugProperty = function (prop) { return info.ace_getDebugProperty(prop); }; - editor.getInInternationalComposition = function() - { + editor.getInInternationalComposition = function () { if (!loaded) return false; return info.ace_getInInternationalComposition(); }; @@ -145,28 +145,25 @@ function Ace2Editor() // to prepareUserChangeset will return an updated changeset that takes into account the // latest user changes, and modify the changeset to be applied by applyPreparedChangesetToBase // accordingly. - editor.prepareUserChangeset = function() - { + editor.prepareUserChangeset = function () { if (!loaded) return null; return info.ace_prepareUserChangeset(); }; - editor.getUnhandledErrors = function() - { + editor.getUnhandledErrors = function () { if (!loaded) return []; // returns array of {error: , time: +new Date()} return info.ace_getUnhandledErrors(); }; - function sortFilesByEmbeded(files) { - var embededFiles = []; - var remoteFiles = []; + const embededFiles = []; + let remoteFiles = []; if (Ace2Editor.EMBEDED) { - for (var i = 0, ii = files.length; i < ii; i++) { - var file = files[i]; + for (let i = 0, ii = files.length; i < ii; i++) { + const file = files[i]; if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) { embededFiles.push(file); } else { @@ -180,9 +177,9 @@ function Ace2Editor() return {embeded: embededFiles, remote: remoteFiles}; } function pushStyleTagsFor(buffer, files) { - var sorted = sortFilesByEmbeded(files); - var embededFiles = sorted.embeded; - var remoteFiles = sorted.remote; + const sorted = sortFilesByEmbeded(files); + const embededFiles = sorted.embeded; + const remoteFiles = sorted.remote; if (embededFiles.length > 0) { buffer.push(''); - hooks.callAll("aceInitInnerdocbodyHead", { - iframeHTML: iframeHTML + hooks.callAll('aceInitInnerdocbodyHead', { + iframeHTML, }); iframeHTML.push(' '); // Expose myself to global for my child frame. - var thisFunctionsName = "ChildAccessibleAce2Editor"; - (function () {return this}())[thisFunctionsName] = Ace2Editor; + const thisFunctionsName = 'ChildAccessibleAce2Editor'; + (function () { return this; }())[thisFunctionsName] = Ace2Editor; - var outerScript = '\ -editorId = ' + JSON.stringify(info.id) + ';\n\ -editorInfo = parent[' + JSON.stringify(thisFunctionsName) + '].registry[editorId];\n\ + const outerScript = `\ +editorId = ${JSON.stringify(info.id)};\n\ +editorInfo = parent[${JSON.stringify(thisFunctionsName)}].registry[editorId];\n\ window.onload = function () {\n\ window.onload = null;\n\ setTimeout(function () {\n\ @@ -308,34 +300,35 @@ window.onload = function () {\n\ };\n\ var doc = iframe.contentWindow.document;\n\ doc.open();\n\ - var text = (' + JSON.stringify(iframeHTML.join('\n')) + ');\n\ + var text = (${JSON.stringify(iframeHTML.join('\n'))});\n\ doc.write(text);\n\ doc.close();\n\ }, 0);\n\ -}'; +}`; - var outerHTML = [doctype, ''] + const outerHTML = [doctype, ``]; var includedCSS = []; - var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)}; - $$INCLUDE_CSS("../static/css/iframe_editor.css"); - $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString); + var $$INCLUDE_CSS = function (filename) { includedCSS.push(filename); }; + $$INCLUDE_CSS('../static/css/iframe_editor.css'); + $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){ + var additionalCSS = _(hooks.callAll('aceEditorCSS')).map((path) => { if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css return path; } - return '../static/plugins/' + path } + return `../static/plugins/${path}`; + } ); includedCSS = includedCSS.concat(additionalCSS); - $$INCLUDE_CSS("../static/skins/" + clientVars.skinName + "/pad.css?v=" + clientVars.randomVersionString); + $$INCLUDE_CSS(`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); pushStyleTagsFor(outerHTML, includedCSS); // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // (throbs busy while typing) - var pluginNames = pluginUtils.clientPluginNames(); + const pluginNames = pluginUtils.clientPluginNames(); outerHTML.push( '', '', @@ -346,14 +339,14 @@ window.onload = function () {\n\ '
        x
        ', ''); - var outerFrame = document.createElement("IFRAME"); - outerFrame.name = "ace_outer"; + const outerFrame = document.createElement('IFRAME'); + outerFrame.name = 'ace_outer'; outerFrame.frameBorder = 0; // for IE - outerFrame.title = "Ether"; + outerFrame.title = 'Ether'; info.frame = outerFrame; document.getElementById(containerId).appendChild(outerFrame); - var editorDocument = outerFrame.contentWindow.document; + const editorDocument = outerFrame.contentWindow.document; editorDocument.open(); editorDocument.write(outerHTML.join('')); diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.js index 7ad7ba0ffe6..9055b34e3a9 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.js @@ -20,31 +20,27 @@ * limitations under the License. */ -var Security = require('./security'); +const Security = require('./security'); -function isNodeText(node) -{ +function isNodeText(node) { return (node.nodeType == 3); } -function object(o) -{ - var f = function(){}; +function object(o) { + const f = function () {}; f.prototype = o; return new f(); } -function getAssoc(obj, name) -{ - return obj["_magicdom_" + name]; +function getAssoc(obj, name) { + return obj[`_magicdom_${name}`]; } -function setAssoc(obj, name, value) -{ +function setAssoc(obj, name, value) { // note that in IE designMode, properties of a node can get // copied to new nodes that are spawned during editing; also, // properties representable in HTML text can survive copy-and-paste - obj["_magicdom_" + name] = value; + obj[`_magicdom_${name}`] = value; } // "func" is a function over 0..(numItems-1) that is monotonically @@ -52,35 +48,31 @@ function setAssoc(obj, name, value) // between false and true, a number between 0 and numItems inclusive. -function binarySearch(numItems, func) -{ +function binarySearch(numItems, func) { if (numItems < 1) return 0; if (func(0)) return 0; if (!func(numItems - 1)) return numItems; - var low = 0; // func(low) is always false - var high = numItems - 1; // func(high) is always true - while ((high - low) > 1) - { - var x = Math.floor((low + high) / 2); // x != low, x != high + let low = 0; // func(low) is always false + let high = numItems - 1; // func(high) is always true + while ((high - low) > 1) { + const x = Math.floor((low + high) / 2); // x != low, x != high if (func(x)) high = x; else low = x; } return high; } -function binarySearchInfinite(expectedLength, func) -{ - var i = 0; +function binarySearchInfinite(expectedLength, func) { + let i = 0; while (!func(i)) i += expectedLength; return binarySearch(i, func); } -function htmlPrettyEscape(str) -{ +function htmlPrettyEscape(str) { return Security.escapeHTML(str).replace(/\r?\n/g, '\\n'); } -var noop = function(){}; +const noop = function () {}; exports.isNodeText = isNodeText; exports.object = object; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 65be7d90abd..fc339ab7864 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -19,130 +19,120 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var _, $, jQuery, plugins, Ace2Common; -var browser = require('./browser'); -if(browser.msie){ - // Honestly fuck IE royally. - // Basically every hack we have since V11 causes a problem - if(parseInt(browser.version) >= 11){ - delete browser.msie; - browser.chrome = true; - browser.modernIE = true; - } -} + +const padutils = require('./pad_utils').padutils; + +let _, $, jQuery, plugins, Ace2Common; +const browser = require('./browser'); Ace2Common = require('./ace2_common'); plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); $ = jQuery = require('./rjquery').$; -_ = require("./underscore"); - -var isNodeText = Ace2Common.isNodeText, - getAssoc = Ace2Common.getAssoc, - setAssoc = Ace2Common.setAssoc, - isTextNode = Ace2Common.isTextNode, - binarySearchInfinite = Ace2Common.binarySearchInfinite, - htmlPrettyEscape = Ace2Common.htmlPrettyEscape, - noop = Ace2Common.noop; -var hooks = require('./pluginfw/hooks'); - -function Ace2Inner(){ - - var makeChangesetTracker = require('./changesettracker').makeChangesetTracker; - var colorutils = require('./colorutils').colorutils; - var makeContentCollector = require('./contentcollector').makeContentCollector; - var makeCSSManager = require('./cssmanager').makeCSSManager; - var domline = require('./domline').domline; - var AttribPool = require('./AttributePool'); - var Changeset = require('./Changeset'); - var ChangesetUtils = require('./ChangesetUtils'); - var linestylefilter = require('./linestylefilter').linestylefilter; - var SkipList = require('./skiplist'); - var undoModule = require('./undomodule').undoModule; - var AttributeManager = require('./AttributeManager'); - var Scroll = require('./scroll'); - - var DEBUG = false; //$$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" +_ = require('./underscore'); + +const isNodeText = Ace2Common.isNodeText; +const getAssoc = Ace2Common.getAssoc; +const setAssoc = Ace2Common.setAssoc; +const isTextNode = Ace2Common.isTextNode; +const binarySearchInfinite = Ace2Common.binarySearchInfinite; +const htmlPrettyEscape = Ace2Common.htmlPrettyEscape; +const noop = Ace2Common.noop; +const hooks = require('./pluginfw/hooks'); + +function Ace2Inner() { + const makeChangesetTracker = require('./changesettracker').makeChangesetTracker; + const colorutils = require('./colorutils').colorutils; + const makeContentCollector = require('./contentcollector').makeContentCollector; + const makeCSSManager = require('./cssmanager').makeCSSManager; + const domline = require('./domline').domline; + const AttribPool = require('./AttributePool'); + const Changeset = require('./Changeset'); + const ChangesetUtils = require('./ChangesetUtils'); + const linestylefilter = require('./linestylefilter').linestylefilter; + const SkipList = require('./skiplist'); + const undoModule = require('./undomodule').undoModule; + const AttributeManager = require('./AttributeManager'); + const Scroll = require('./scroll'); + + const DEBUG = false; // $$ build script replaces the string "var DEBUG=true;//$$" with "var DEBUG=false;" // changed to false - var isSetUp = false; + let isSetUp = false; - var THE_TAB = ' '; //4 - var MAX_LIST_LEVEL = 16; + const THE_TAB = ' '; // 4 + const MAX_LIST_LEVEL = 16; - var FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; - var SELECT_BUTTON_CLASS = 'selected'; + const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; + const SELECT_BUTTON_CLASS = 'selected'; - var caughtErrors = []; + const caughtErrors = []; - var thisAuthor = ''; + let thisAuthor = ''; - var disposed = false; - var editorInfo = parent.editorInfo; + let disposed = false; + const editorInfo = parent.editorInfo; - var iframe = window.frameElement; - var outerWin = iframe.ace_outerWin; + const iframe = window.frameElement; + const outerWin = iframe.ace_outerWin; iframe.ace_outerWin = null; // prevent IE 6 memory leak - var sideDiv = iframe.nextSibling; - var lineMetricsDiv = sideDiv.nextSibling; + const sideDiv = iframe.nextSibling; + const lineMetricsDiv = sideDiv.nextSibling; + let lineNumbersShown; + let sideDivInner; initLineNumbers(); - var scroll = Scroll.init(outerWin); + const scroll = Scroll.init(outerWin); - var outsideKeyDown = noop; + let outsideKeyDown = noop; - var outsideKeyPress = function(){return true;}; + let outsideKeyPress = function (e) { return true; }; - var outsideNotifyDirty = noop; + let outsideNotifyDirty = noop; // selFocusAtStart -- determines whether the selection extends "backwards", so that the focus // point (controlled with the arrow keys) is at the beginning; 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! - var rep = { + const rep = { lines: new SkipList(), selStart: null, selEnd: null, selFocusAtStart: false, - alltext: "", + alltext: '', alines: [], - apool: new AttribPool() + apool: new AttribPool(), }; // lines, alltext, alines, and DOM are set up in init() - if (undoModule.enabled) - { + if (undoModule.enabled) { undoModule.apool = rep.apool; } - var root, doc; // set in init() - var isEditable = true; - var doesWrap = true; - var hasLineNumbers = true; - var isStyled = true; + let root, doc; // set in init() + let isEditable = true; + let doesWrap = true; + let hasLineNumbers = true; + let isStyled = true; - var console = (DEBUG && window.console); - var documentAttributeManager; + let console = (DEBUG && window.console); + let documentAttributeManager; - if (!window.console) - { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + if (!window.console) { + const names = ['log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd']; console = {}; - for (var i = 0; i < names.length; ++i) - console[names[i]] = noop; + for (let i = 0; i < names.length; ++i) console[names[i]] = noop; } - var PROFILER = window.PROFILER; - if (!PROFILER) - { - PROFILER = function() - { + let PROFILER = window.PROFILER; + if (!PROFILER) { + PROFILER = function () { return { start: noop, mark: noop, literal: noop, end: noop, - cancel: noop + cancel: noop, }; }; } @@ -151,146 +141,118 @@ function Ace2Inner(){ // 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. - var dmesg = noop; + let dmesg = noop; window.dmesg = noop; - var scheduler = parent; // hack for opera required + const scheduler = parent; // hack for opera required - var dynamicCSS = null; - var outerDynamicCSS = null; - var parentDynamicCSS = null; + let dynamicCSS = null; + let outerDynamicCSS = null; + let parentDynamicCSS = null; - function initDynamicCSS() - { - dynamicCSS = makeCSSManager("dynamicsyntax"); - outerDynamicCSS = makeCSSManager("dynamicsyntax", "outer"); - parentDynamicCSS = makeCSSManager("dynamicsyntax", "parent"); + function initDynamicCSS() { + dynamicCSS = makeCSSManager('dynamicsyntax'); + outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer'); + parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent'); } - var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { - withCallbacks: function(operationName, f) - { - inCallStackIfNecessary(operationName, function() - { + const changesetTracker = makeChangesetTracker(scheduler, rep.apool, { + withCallbacks(operationName, f) { + inCallStackIfNecessary(operationName, () => { fastIncorp(1); f( - { - setDocumentAttributedText: function(atext) - { - setDocAText(atext); - }, - applyChangesetToDocument: function(changeset, preferInsertionAfterCaret) - { - var oldEventType = currentCallStack.editEvent.eventType; - currentCallStack.startNewEvent("nonundoable"); - - performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); - - currentCallStack.startNewEvent(oldEventType); - } - }); + { + setDocumentAttributedText(atext) { + setDocAText(atext); + }, + applyChangesetToDocument(changeset, preferInsertionAfterCaret) { + const oldEventType = currentCallStack.editEvent.eventType; + currentCallStack.startNewEvent('nonundoable'); + + performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); + + currentCallStack.startNewEvent(oldEventType); + }, + }); }); - } + }, }); - var authorInfos = {}; // presence of key determines if author is present in doc + const authorInfos = {}; // presence of key determines if author is present in doc - function getAuthorInfos(){ + function getAuthorInfos() { return authorInfos; - }; - editorInfo.ace_getAuthorInfos= getAuthorInfos; + } + editorInfo.ace_getAuthorInfos = getAuthorInfos; - function setAuthorStyle(author, info) - { + function setAuthorStyle(author, info) { if (!dynamicCSS) { return; } - var authorSelector = getAuthorColorClassSelector(getAuthorClassName(author)); + const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author)); - var authorStyleSet = hooks.callAll('aceSetAuthorStyle', { - dynamicCSS: dynamicCSS, - parentDynamicCSS: parentDynamicCSS, - outerDynamicCSS: outerDynamicCSS, - info: info, - author: author, - authorSelector: authorSelector, + const authorStyleSet = hooks.callAll('aceSetAuthorStyle', { + dynamicCSS, + parentDynamicCSS, + outerDynamicCSS, + info, + author, + authorSelector, }); // Prevent default behaviour if any hook says so - if (_.any(authorStyleSet, function(it) { return it })) - { - return + if (_.any(authorStyleSet, (it) => it)) { + return; } - if (!info) - { + if (!info) { dynamicCSS.removeSelectorStyle(authorSelector); parentDynamicCSS.removeSelectorStyle(authorSelector); - } - else - { - if (info.bgcolor) - { - var bgcolor = info.bgcolor; - if ((typeof info.fade) == "number") - { - bgcolor = fadeColor(bgcolor, info.fade); - } + } else if (info.bgcolor) { + let bgcolor = info.bgcolor; + if ((typeof info.fade) === 'number') { + bgcolor = fadeColor(bgcolor, info.fade); + } - var authorStyle = dynamicCSS.selectorStyle(authorSelector); - var parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector); + const authorStyle = dynamicCSS.selectorStyle(authorSelector); + const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector); - // author color - authorStyle.backgroundColor = bgcolor; - parentAuthorStyle.backgroundColor = bgcolor; + // author color + authorStyle.backgroundColor = bgcolor; + parentAuthorStyle.backgroundColor = bgcolor; - var textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); - authorStyle.color = textColor; - parentAuthorStyle.color = textColor; - } + const textColor = colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); + authorStyle.color = textColor; + parentAuthorStyle.color = textColor; } } - function setAuthorInfo(author, info) - { - if ((typeof author) != "string") - { + function setAuthorInfo(author, info) { + if ((typeof author) !== 'string') { // Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802"); - throw new Error("setAuthorInfo: author (" + author + ") is not a string"); + throw new Error(`setAuthorInfo: author (${author}) is not a string`); } - if (!info) - { + if (!info) { delete authorInfos[author]; - } - else - { + } else { authorInfos[author] = info; } setAuthorStyle(author, info); } - function getAuthorClassName(author) - { - return "author-" + author.replace(/[^a-y0-9]/g, function(c) - { - if (c == ".") return "-"; - return 'z' + c.charCodeAt(0) + 'z'; - }); + function getAuthorClassName(author) { + return `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c == '.') return '-'; + return `z${c.charCodeAt(0)}z`; + })}`; } - function className2Author(className) - { - if (className.substring(0, 7) == "author-") - { - return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, function(cc) - { - if (cc == '-') return '.'; - else if (cc.charAt(0) == 'z') - { + function className2Author(className) { + if (className.substring(0, 7) == 'author-') { + return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { + if (cc == '-') { return '.'; } else if (cc.charAt(0) == 'z') { return String.fromCharCode(Number(cc.slice(1, -1))); - } - else - { + } else { return cc; } }); @@ -298,115 +260,91 @@ function Ace2Inner(){ return null; } - function getAuthorColorClassSelector(oneClassName) - { - return ".authorColors ." + oneClassName; + function getAuthorColorClassSelector(oneClassName) { + return `.authorColors .${oneClassName}`; } - function fadeColor(colorCSS, fadeFrac) - { - var color = colorutils.css2triple(colorCSS); + function fadeColor(colorCSS, fadeFrac) { + let color = colorutils.css2triple(colorCSS); color = colorutils.blend(color, [1, 1, 1], fadeFrac); return colorutils.triple2css(color); } - editorInfo.ace_getRep = function() - { + editorInfo.ace_getRep = function () { return rep; }; - editorInfo.ace_getAuthor = function() - { + editorInfo.ace_getAuthor = function () { return thisAuthor; - } + }; - var _nonScrollableEditEvents = { - "applyChangesToBase": 1 + const _nonScrollableEditEvents = { + applyChangesToBase: 1, }; - _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), function(eventType) { - _nonScrollableEditEvents[eventType] = 1; + _.each(hooks.callAll('aceRegisterNonScrollableEditEvents'), (eventType) => { + _nonScrollableEditEvents[eventType] = 1; }); - function isScrollableEditEvent(eventType) - { + function isScrollableEditEvent(eventType) { return !_nonScrollableEditEvents[eventType]; } var currentCallStack = null; - function inCallStack(type, action) - { + function inCallStack(type, action) { if (disposed) return; - if (currentCallStack) - { + if (currentCallStack) { // Do not uncomment this in production. It will break Etherpad being provided in iFrames. I'm leaving this in for testing usefulness. // top.console.error("Can't enter callstack " + type + ", already in " + currentCallStack.type); } - var profiling = false; + let profiling = false; - function profileRest() - { + function profileRest() { profiling = true; } - function newEditEvent(eventType) - { + function newEditEvent(eventType) { return { - eventType: eventType, - backset: null + eventType, + backset: null, }; } - function submitOldEvent(evt) - { - if (rep.selStart && rep.selEnd) - { - var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + function submitOldEvent(evt) { + if (rep.selStart && rep.selEnd) { + const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; evt.selStart = selStartChar; evt.selEnd = selEndChar; evt.selFocusAtStart = rep.selFocusAtStart; } - if (undoModule.enabled) - { - var undoWorked = false; - try - { - if (isPadLoading(evt.eventType)) - { + if (undoModule.enabled) { + let undoWorked = false; + try { + if (isPadLoading(evt.eventType)) { undoModule.clearHistory(); - } - else if (evt.eventType == "nonundoable") - { - if (evt.changeset) - { + } else if (evt.eventType == 'nonundoable') { + if (evt.changeset) { undoModule.reportExternalChange(evt.changeset); } - } - else - { + } else { undoModule.reportEvent(evt); } undoWorked = true; - } - finally - { - if (!undoWorked) - { + } finally { + if (!undoWorked) { undoModule.enabled = false; // for safety } } } } - function startNewEvent(eventType, dontSubmitOld) - { - var oldEvent = currentCallStack.editEvent; - if (!dontSubmitOld) - { + function startNewEvent(eventType, dontSubmitOld) { + const oldEvent = currentCallStack.editEvent; + if (!dontSubmitOld) { submitOldEvent(oldEvent); } currentCallStack.editEvent = newEditEvent(eventType); @@ -414,75 +352,62 @@ function Ace2Inner(){ } currentCallStack = { - type: type, + type, docTextChanged: false, selectionAffected: false, userChangedSelection: false, domClean: false, - profileRest: profileRest, + profileRest, isUserChange: false, // is this a "user change" type of call-stack repChanged: false, editEvent: newEditEvent(type), - startNewEvent: startNewEvent + startNewEvent, }; - var cleanExit = false; - var result; - try - { + let cleanExit = false; + let result; + try { result = action(); hooks.callAll('aceEditEvent', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + documentAttributeManager, }); cleanExit = true; - } - catch (e) - { + } catch (e) { caughtErrors.push( - { - error: e, - time: +new Date() - }); + { + error: e, + time: +new Date(), + }); dmesg(e.toString()); throw e; - } - finally - { - var cs = currentCallStack; - if (cleanExit) - { + } finally { + const cs = currentCallStack; + if (cleanExit) { submitOldEvent(cs.editEvent); - if (cs.domClean && cs.type != "setup") - { + if (cs.domClean && cs.type != 'setup') { // if (cs.isUserChange) // { // if (cs.repChanged) parenModule.notifyChange(); // else parenModule.notifyTick(); // } - if (cs.selectionAffected) - { + if (cs.selectionAffected) { updateBrowserSelectionFromRep(); } - if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) - { + if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) { scrollSelectionIntoView(); } - if (cs.docTextChanged && cs.type.indexOf("importText") < 0) - { + if (cs.docTextChanged && cs.type.indexOf('importText') < 0) { outsideNotifyDirty(); } } - } - else - { + } else { // non-clean exit - if (currentCallStack.type == "idleWorkTimer") - { + if (currentCallStack.type == 'idleWorkTimer') { idleWorkTimer.atLeast(1000); } } @@ -492,99 +417,45 @@ function Ace2Inner(){ } editorInfo.ace_inCallStack = inCallStack; - function inCallStackIfNecessary(type, action) - { - if (!currentCallStack) - { + function inCallStackIfNecessary(type, action) { + if (!currentCallStack) { inCallStack(type, action); - } - else - { + } else { action(); } } editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary; - function dispose() - { + function dispose() { disposed = true; if (idleWorkTimer) idleWorkTimer.never(); teardown(); } - function checkALines() - { - return; // disable for speed - - - function error() - { - throw new Error("checkALines"); - } - if (rep.alines.length != rep.lines.length()) - { - error(); - } - for (var i = 0; i < rep.alines.length; i++) - { - var aline = rep.alines[i]; - var lineText = rep.lines.atIndex(i).text + "\n"; - var lineTextLength = lineText.length; - var opIter = Changeset.opIterator(aline); - var alineLength = 0; - while (opIter.hasNext()) - { - var o = opIter.next(); - alineLength += o.chars; - if (opIter.hasNext()) - { - if (o.lines !== 0) error(); - } - else - { - if (o.lines != 1) error(); - } - } - if (alineLength != lineTextLength) - { - error(); - } - } - } - - function setWraps(newVal) - { + function setWraps(newVal) { doesWrap = newVal; - var dwClass = "doesWrap"; - setClassPresence(root, "doesWrap", doesWrap); - scheduler.setTimeout(function() - { - inCallStackIfNecessary("setWraps", function() - { + const dwClass = 'doesWrap'; + root.classList.toggle('doesWrap', doesWrap); + scheduler.setTimeout(() => { + inCallStackIfNecessary('setWraps', () => { fastIncorp(7); recreateDOM(); fixView(); }); }, 0); - } - function setStyled(newVal) - { - var oldVal = isStyled; - isStyled = !! newVal; + function setStyled(newVal) { + const oldVal = isStyled; + isStyled = !!newVal; - if (newVal != oldVal) - { - if (!newVal) - { + if (newVal != oldVal) { + if (!newVal) { // clear styles - inCallStackIfNecessary("setStyled", function() - { + inCallStackIfNecessary('setStyled', () => { fastIncorp(12); - var clearStyles = []; - for (var k in STYLE_ATTRIBS) - { + const clearStyles = []; + for (const k in STYLE_ATTRIBS) { clearStyles.push([k, '']); } performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); @@ -593,92 +464,66 @@ function Ace2Inner(){ } } - function setTextFace(face) - { + function setTextFace(face) { root.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face; } - function recreateDOM() - { + function recreateDOM() { // precond: normalized recolorLinesInRange(0, rep.alltext.length); } - function setEditable(newVal) - { + function setEditable(newVal) { isEditable = newVal; - - // the following may fail, e.g. if iframe is hidden - if (!isEditable) - { - setDesignMode(false); - } - else - { - setDesignMode(true); - } - setClassPresence(root, "static", !isEditable); + root.contentEditable = isEditable ? 'true' : 'false'; + root.classList.toggle('static', !isEditable); } - function enforceEditability() - { + function enforceEditability() { setEditable(isEditable); } - function importText(text, undoable, dontProcess) - { - var lines; - if (dontProcess) - { - if (text.charAt(text.length - 1) != "\n") - { - throw new Error("new raw text must end with newline"); + function importText(text, undoable, dontProcess) { + let lines; + if (dontProcess) { + if (text.charAt(text.length - 1) != '\n') { + throw new Error('new raw text must end with newline'); } - if (/[\r\t\xa0]/.exec(text)) - { - throw new Error("new raw text must not contain CR, tab, or nbsp"); + if (/[\r\t\xa0]/.exec(text)) { + throw new Error('new raw text must not contain CR, tab, or nbsp'); } lines = text.substring(0, text.length - 1).split('\n'); - } - else - { + } else { lines = _.map(text.split('\n'), textify); } - var newText = "\n"; - if (lines.length > 0) - { - newText = lines.join('\n') + '\n'; + let newText = '\n'; + if (lines.length > 0) { + newText = `${lines.join('\n')}\n`; } - inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() - { + inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocText(newText); }); - if (dontProcess && rep.alltext != text) - { - throw new Error("mismatch error setting raw text in importText"); + if (dontProcess && rep.alltext != text) { + throw new Error('mismatch error setting raw text in importText'); } } - function importAText(atext, apoolJsonObj, undoable) - { + function importAText(atext, apoolJsonObj, undoable) { atext = Changeset.cloneAText(atext); - if (apoolJsonObj) - { - var wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); + if (apoolJsonObj) { + const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool); } - inCallStackIfNecessary("importText" + (undoable ? "Undoable" : ""), function() - { + inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { setDocAText(atext); }); } - function setDocAText(atext) - { - if (atext.text === "") { + function setDocAText(atext) { + if (atext.text === '') { /* * The server is fine with atext.text being an empty string, but the front * end is not, and crashes. @@ -690,17 +535,17 @@ function Ace2Inner(){ * See for reference: * - https://github.com/ether/etherpad-lite/issues/3861 */ - atext.text = "\n"; + atext.text = '\n'; } fastIncorp(8); - var oldLen = rep.lines.totalWidth(); - var numLines = rep.lines.length(); - var upToLastLine = rep.lines.offsetOfIndex(numLines - 1); - var lastLineLength = rep.lines.atIndex(numLines - 1).text.length; - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp('-'); + const oldLen = rep.lines.totalWidth(); + const numLines = rep.lines.length(); + const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); + const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; + const assem = Changeset.smartOpAssembler(); + const o = Changeset.newOp('-'); o.chars = upToLastLine; o.lines = numLines - 1; assem.append(o); @@ -708,130 +553,104 @@ function Ace2Inner(){ o.lines = 0; assem.append(o); Changeset.appendATextToAssembler(atext, assem); - var newLen = oldLen + assem.getLengthChange(); - var changeset = Changeset.checkRep( - Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); + const newLen = oldLen + assem.getLengthChange(); + const changeset = Changeset.checkRep( + Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); performDocumentApplyChangeset(changeset); performSelectionChange([0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); idleWorkTimer.atMost(100); - if (rep.alltext != atext.text) - { + if (rep.alltext != atext.text) { dmesg(htmlPrettyEscape(rep.alltext)); dmesg(htmlPrettyEscape(atext.text)); - throw new Error("mismatch error setting raw text in setDocAText"); + throw new Error('mismatch error setting raw text in setDocAText'); } } - function setDocText(text) - { + function setDocText(text) { setDocAText(Changeset.makeAText(text)); } - function getDocText() - { - var alltext = rep.alltext; - var len = alltext.length; + function getDocText() { + const alltext = rep.alltext; + let len = alltext.length; if (len > 0) len--; // final extra newline return alltext.substring(0, len); } - function exportText() - { - if (currentCallStack && !currentCallStack.domClean) - { - inCallStackIfNecessary("exportText", function() - { + function exportText() { + if (currentCallStack && !currentCallStack.domClean) { + inCallStackIfNecessary('exportText', () => { fastIncorp(2); }); } return getDocText(); } - function editorChangedSize() - { + function editorChangedSize() { fixView(); } - function setOnKeyPress(handler) - { + function setOnKeyPress(handler) { outsideKeyPress = handler; } - function setOnKeyDown(handler) - { + function setOnKeyDown(handler) { outsideKeyDown = handler; } - function setNotifyDirty(handler) - { + function setNotifyDirty(handler) { outsideNotifyDirty = handler; } - function getFormattedCode() - { - if (currentCallStack && !currentCallStack.domClean) - { - inCallStackIfNecessary("getFormattedCode", incorporateUserChanges); + function getFormattedCode() { + if (currentCallStack && !currentCallStack.domClean) { + inCallStackIfNecessary('getFormattedCode', incorporateUserChanges); } - var buf = []; - if (rep.lines.length() > 0) - { + const buf = []; + if (rep.lines.length() > 0) { // should be the case, even for empty file - var entry = rep.lines.atIndex(0); - while (entry) - { - var domInfo = entry.domInfo; - buf.push((domInfo && domInfo.getInnerHTML()) || domline.processSpaces(domline.escapeHTML(entry.text), doesWrap) || ' ' /*empty line*/ ); + 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
        ') + '
        '; + return `
        ${buf.join('
        \n
        ')}
        `; } - var CMDS = { - clearauthorship: function(prompt) - { - if ((!(rep.selStart && rep.selEnd)) || isCaret()) - { - if (prompt) - { + const CMDS = { + clearauthorship(prompt) { + if ((!(rep.selStart && rep.selEnd)) || isCaret()) { + if (prompt) { prompt(); - } - else - { + } else { performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [ - ['author', ''] + ['author', ''], ]); } - } - else - { + } else { setAttributeOnSelection('author', ''); } - } + }, }; - function execCommand(cmd) - { + function execCommand(cmd) { cmd = cmd.toLowerCase(); - var cmdArgs = Array.prototype.slice.call(arguments, 1); - if (CMDS[cmd]) - { - inCallStackIfNecessary(cmd, function() - { + const cmdArgs = Array.prototype.slice.call(arguments, 1); + if (CMDS[cmd]) { + inCallStackIfNecessary(cmd, () => { fastIncorp(9); CMDS[cmd].apply(CMDS, cmdArgs); }); } } - function replaceRange(start, end, text) - { - inCallStackIfNecessary('replaceRange', function() - { + function replaceRange(start, end, text) { + inCallStackIfNecessary('replaceRange', () => { fastIncorp(9); performDocumentReplaceRange(start, end, text); }); @@ -850,7 +669,7 @@ function Ace2Inner(){ editorInfo.ace_setEditable = setEditable; editorInfo.ace_execCommand = execCommand; editorInfo.ace_replaceRange = replaceRange; - editorInfo.ace_getAuthorInfos= getAuthorInfos; + editorInfo.ace_getAuthorInfos = getAuthorInfos; editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; editorInfo.ace_renumberList = renumberList; @@ -858,29 +677,22 @@ function Ace2Inner(){ editorInfo.ace_isBlockElement = isBlockElement; editorInfo.ace_getLineListType = getLineListType; - editorInfo.ace_callWithAce = function(fn, callStack, normalize) - { - var wrapper = function() - { + editorInfo.ace_callWithAce = function (fn, callStack, normalize) { + let wrapper = function () { return fn(editorInfo); }; - if (normalize !== undefined) - { - var wrapper1 = wrapper; - wrapper = function() - { + if (normalize !== undefined) { + const wrapper1 = wrapper; + wrapper = function () { editorInfo.ace_fastIncorp(9); wrapper1(); }; } - if (callStack !== undefined) - { + if (callStack !== undefined) { return editorInfo.ace_inCallStack(callStack, wrapper); - } - else - { + } else { return wrapper(); } }; @@ -888,329 +700,244 @@ function Ace2Inner(){ // This methed exposes a setter for some ace properties // @param key the name of the parameter // @param value the value to set to - editorInfo.ace_setProperty = function(key, value) - { - - // Convinience function returning a setter for a class on an element - var setClassPresenceNamed = function(element, cls){ - return function(value){ - setClassPresence(element, cls, !! value) - } - }; - + editorInfo.ace_setProperty = function (key, value) { // These properties are exposed - var setters = { + const setters = { wraps: setWraps, - showsauthorcolors: setClassPresenceNamed(root, "authorColors"), - showsuserselections: setClassPresenceNamed(root, "userSelections"), - showslinenumbers : function(value){ - hasLineNumbers = !! value; - setClassPresence(sideDiv.parentNode, "line-numbers-hidden", !hasLineNumbers); + showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val), + showsuserselections: (val) => root.classList.toggle('userSelections', !!val), + showslinenumbers(value) { + hasLineNumbers = !!value; + sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); fixView(); }, - grayedout: setClassPresenceNamed(outerWin.document.body, "grayedout"), - dmesg: function(){ dmesg = window.dmesg = value; }, - userauthor: function(value){ + grayedout: (val) => outerWin.document.body.classList.toggle('grayedout', !!val), + dmesg() { dmesg = window.dmesg = value; }, + userauthor(value) { thisAuthor = String(value); documentAttributeManager.author = thisAuthor; }, styled: setStyled, textface: setTextFace, - rtlistrue: function(value) { - setClassPresence(root, "rtl", value) - setClassPresence(root, "ltr", !value) - document.documentElement.dir = value? 'rtl' : 'ltr' - } + rtlistrue(value) { + root.classList.toggle('rtl', value); + root.classList.toggle('ltr', !value); + document.documentElement.dir = value ? 'rtl' : 'ltr'; + }, }; - var setter = setters[key.toLowerCase()]; + const setter = setters[key.toLowerCase()]; // check if setter is present - if(setter !== undefined){ - setter(value) + if (setter !== undefined) { + setter(value); } }; - editorInfo.ace_setBaseText = function(txt) - { + editorInfo.ace_setBaseText = function (txt) { changesetTracker.setBaseText(txt); }; - editorInfo.ace_setBaseAttributedText = function(atxt, apoolJsonObj) - { + editorInfo.ace_setBaseAttributedText = function (atxt, apoolJsonObj) { changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); }; - editorInfo.ace_applyChangesToBase = function(c, optAuthor, apoolJsonObj) - { + editorInfo.ace_applyChangesToBase = function (c, optAuthor, apoolJsonObj) { changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); }; - editorInfo.ace_prepareUserChangeset = function() - { + editorInfo.ace_prepareUserChangeset = function () { return changesetTracker.prepareUserChangeset(); }; - editorInfo.ace_applyPreparedChangesetToBase = function() - { + editorInfo.ace_applyPreparedChangesetToBase = function () { changesetTracker.applyPreparedChangesetToBase(); }; - editorInfo.ace_setUserChangeNotificationCallback = function(f) - { + editorInfo.ace_setUserChangeNotificationCallback = function (f) { changesetTracker.setUserChangeNotificationCallback(f); }; - editorInfo.ace_setAuthorInfo = function(author, info) - { + editorInfo.ace_setAuthorInfo = function (author, info) { setAuthorInfo(author, info); }; - editorInfo.ace_setAuthorSelectionRange = function(author, start, end) - { + editorInfo.ace_setAuthorSelectionRange = function (author, start, end) { changesetTracker.setAuthorSelectionRange(author, start, end); }; - editorInfo.ace_getUnhandledErrors = function() - { + editorInfo.ace_getUnhandledErrors = function () { return caughtErrors.slice(); }; - editorInfo.ace_getDocument = function() - { + editorInfo.ace_getDocument = function () { return doc; }; - editorInfo.ace_getDebugProperty = function(prop) - { - if (prop == "debugger") - { + editorInfo.ace_getDebugProperty = function (prop) { + if (prop == 'debugger') { // obfuscate "eval" so as not to scare yuicompressor - window['ev' + 'al']("debugger"); - } - else if (prop == "rep") - { + window['ev' + 'al']('debugger'); + } else if (prop == 'rep') { return rep; - } - else if (prop == "window") - { + } else if (prop == 'window') { return window; - } - else if (prop == "document") - { + } else if (prop == 'document') { return document; } return undefined; }; - function now() - { + function now() { return Date.now(); } - function newTimeLimit(ms) - { - var startTime = now(); - var lastElapsed = 0; - var exceededAlready = false; - var printedTrace = false; - var isTimeUp = function() - { - if (exceededAlready) - { - if ((!printedTrace)) - { // && now() - startTime - ms > 300) { - printedTrace = true; - } - return true; - } - var elapsed = now() - startTime; - if (elapsed > ms) - { - exceededAlready = true; - return true; - } - else - { - lastElapsed = elapsed; - return false; + function newTimeLimit(ms) { + const startTime = now(); + let lastElapsed = 0; + let exceededAlready = false; + let printedTrace = false; + const isTimeUp = function () { + if (exceededAlready) { + if ((!printedTrace)) { // && now() - startTime - ms > 300) { + printedTrace = true; } - }; + return true; + } + const elapsed = now() - startTime; + if (elapsed > ms) { + exceededAlready = true; + return true; + } else { + lastElapsed = elapsed; + return false; + } + }; - isTimeUp.elapsed = function() - { + isTimeUp.elapsed = function () { return now() - startTime; }; return isTimeUp; } - function makeIdleAction(func) - { - var scheduledTimeout = null; - var scheduledTime = 0; + function makeIdleAction(func) { + let scheduledTimeout = null; + let scheduledTime = 0; - function unschedule() - { - if (scheduledTimeout) - { + function unschedule() { + if (scheduledTimeout) { scheduler.clearTimeout(scheduledTimeout); scheduledTimeout = null; } } - function reschedule(time) - { + function reschedule(time) { unschedule(); scheduledTime = time; - var delay = time - now(); + let delay = time - now(); if (delay < 0) delay = 0; scheduledTimeout = scheduler.setTimeout(callback, delay); } - function callback() - { + function callback() { scheduledTimeout = null; // func may reschedule the action func(); } return { - atMost: function(ms) - { - var latestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime > latestTime) - { + atMost(ms) { + const latestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime > latestTime) { reschedule(latestTime); } }, // atLeast(ms) will schedule the action if not scheduled yet. // In other words, "infinity" is replaced by ms, even though // it is technically larger. - atLeast: function(ms) - { - var earliestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime < earliestTime) - { + atLeast(ms) { + const earliestTime = now() + ms; + if ((!scheduledTimeout) || scheduledTime < earliestTime) { reschedule(earliestTime); } }, - never: function() - { + never() { unschedule(); - } + }, }; } - function fastIncorp(n) - { + function fastIncorp(n) { // normalize but don't do any lexing or anything - incorporateUserChanges(newTimeLimit(0)); + incorporateUserChanges(); } editorInfo.ace_fastIncorp = fastIncorp; - var idleWorkTimer = makeIdleAction(function() - { - - //if (! top.BEFORE) top.BEFORE = []; - //top.BEFORE.push(magicdom.root.dom.innerHTML); - //if (! isEditable) return; // and don't reschedule - if (inInternationalComposition) - { + var idleWorkTimer = makeIdleAction(() => { + if (inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); return; } - inCallStackIfNecessary("idleWorkTimer", function() - { - - var isTimeUp = newTimeLimit(250); - - var finishedImportantWork = false; - var finishedWork = false; + inCallStackIfNecessary('idleWorkTimer', () => { + const isTimeUp = newTimeLimit(250); - try - { + let finishedImportantWork = false; + let finishedWork = false; - // isTimeUp() is a soft constraint for incorporateUserChanges, - // which always renormalizes the DOM, no matter how long it takes, - // but doesn't necessarily lex and highlight it - incorporateUserChanges(isTimeUp); + try { + incorporateUserChanges(); if (isTimeUp()) return; updateLineNumbers(); // update line numbers if any time left if (isTimeUp()) return; - var visibleRange = scroll.getVisibleCharRange(rep); - var docRange = [0, rep.lines.totalWidth()]; + const visibleRange = scroll.getVisibleCharRange(rep); + const docRange = [0, rep.lines.totalWidth()]; finishedImportantWork = true; finishedWork = true; - } - finally - { - if (finishedWork) - { + } finally { + if (finishedWork) { idleWorkTimer.atMost(1000); - } - else if (finishedImportantWork) - { + } else if (finishedImportantWork) { // if we've finished highlighting the view area, // more highlighting could be counter-productive, // e.g. if the user just opened a triple-quote and will soon close it. idleWorkTimer.atMost(500); - } - else - { - var timeToWait = Math.round(isTimeUp.elapsed() / 2); + } else { + let timeToWait = Math.round(isTimeUp.elapsed() / 2); if (timeToWait < 100) timeToWait = 100; idleWorkTimer.atMost(timeToWait); } } }); - - //if (! top.AFTER) top.AFTER = []; - //top.AFTER.push(magicdom.root.dom.innerHTML); }); - var _nextId = 1; + let _nextId = 1; - function uniqueId(n) - { + function uniqueId(n) { // not actually guaranteed to be unique, e.g. if user copy-pastes // nodes with ids - var nid = n.id; + const nid = n.id; if (nid) return nid; - return (n.id = "magicdomid" + (_nextId++)); + return (n.id = `magicdomid${_nextId++}`); } - function recolorLinesInRange(startChar, endChar, isTimeUp, optModFunc) - { + function recolorLinesInRange(startChar, endChar) { if (endChar <= startChar) return; if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; - var lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary - var lineStart = rep.lines.offsetOfEntry(lineEntry); - var lineIndex = rep.lines.indexOfEntry(lineEntry); - var selectionNeedsResetting = false; - var firstLine = null; - var lastLine = null; - isTimeUp = (isTimeUp || noop); + let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary + let lineStart = rep.lines.offsetOfEntry(lineEntry); + let lineIndex = rep.lines.indexOfEntry(lineEntry); + let selectionNeedsResetting = false; + let firstLine = null; + let lastLine = null; // tokenFunc function; accesses current value of lineEntry and curDocChar, // also mutates curDocChar - var curDocChar; - var tokenFunc = function(tokenText, tokenClass) - { - lineEntry.domInfo.appendSpan(tokenText, tokenClass); - }; - if (optModFunc) - { - var f = tokenFunc; - tokenFunc = function(tokenText, tokenClass) - { - optModFunc(tokenText, tokenClass, f, curDocChar); - curDocChar += tokenText.length; - }; - } + let curDocChar; + const tokenFunc = function (tokenText, tokenClass) { + lineEntry.domInfo.appendSpan(tokenText, tokenClass); + }; - while (lineEntry && lineStart < endChar && !isTimeUp()) - { - //var timer = newTimeLimit(200); - var lineEnd = lineStart + lineEntry.width; + while (lineEntry && lineStart < endChar) { + const lineEnd = lineStart + lineEntry.width; curDocChar = lineStart; lineEntry.domInfo.clearSpans(); @@ -1219,8 +946,7 @@ function Ace2Inner(){ markNodeClean(lineEntry.lineNode); - if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) - { + if (rep.selStart && rep.selStart[0] == lineIndex || rep.selEnd && rep.selEnd[0] == lineIndex) { selectionNeedsResetting = true; } @@ -1230,8 +956,7 @@ function Ace2Inner(){ lineEntry = rep.lines.next(lineEntry); lineIndex++; } - if (selectionNeedsResetting) - { + if (selectionNeedsResetting) { currentCallStack.selectionAffected = true; } } @@ -1241,102 +966,83 @@ function Ace2Inner(){ // consideration by func - function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) - { - var lineEntryOffset = lineEntryOffsetHint; - if ((typeof lineEntryOffset) != "number") - { + function getSpansForLine(lineEntry, textAndClassFunc, lineEntryOffsetHint) { + let lineEntryOffset = lineEntryOffsetHint; + if ((typeof lineEntryOffset) !== 'number') { lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); } - var text = lineEntry.text; - var width = lineEntry.width; // text.length+1 - if (text.length === 0) - { + const text = lineEntry.text; + const width = lineEntry.width; // text.length+1 + if (text.length === 0) { // allow getLineStyleFilter to set line-div styles - var func = linestylefilter.getLineStyleFilter( - 0, '', textAndClassFunc, rep.apool); + const func = linestylefilter.getLineStyleFilter( + 0, '', textAndClassFunc, rep.apool); func('', ''); - } - else - { - var offsetIntoLine = 0; - var filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); - var lineNum = rep.lines.indexOfEntry(lineEntry); - var aline = rep.alines[lineNum]; + } else { + const offsetIntoLine = 0; + let filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); + const lineNum = rep.lines.indexOfEntry(lineEntry); + const aline = rep.alines[lineNum]; filteredFunc = linestylefilter.getLineStyleFilter( - text.length, aline, filteredFunc, rep.apool); + text.length, aline, filteredFunc, rep.apool); filteredFunc(text, ''); } } - var observedChanges; + let observedChanges; - function clearObservedChanges() - { + function clearObservedChanges() { observedChanges = { - cleanNodesNearChanges: {} + cleanNodesNearChanges: {}, }; } clearObservedChanges(); - function getCleanNodeByKey(key) - { - var p = PROFILER("getCleanNodeByKey", false); + function getCleanNodeByKey(key) { + const p = PROFILER('getCleanNodeByKey', false); p.extra = 0; - var n = doc.getElementById(key); + let n = doc.getElementById(key); // copying and pasting can lead to duplicate ids - while (n && isNodeDirty(n)) - { + while (n && isNodeDirty(n)) { p.extra++; - n.id = ""; + n.id = ''; n = doc.getElementById(key); } - p.literal(p.extra, "extra"); + p.literal(p.extra, 'extra'); p.end(); return n; } - function observeChangesAroundNode(node) - { + function observeChangesAroundNode(node) { // Around this top-level DOM node, look for changes to the document // (from how it looks in our representation) and record them in a way // that can be used to "normalize" the document (apply the changes to our // representation, and put the DOM in a canonical form). - var cleanNode; - var hasAdjacentDirtyness; - if (!isNodeDirty(node)) - { + let cleanNode; + let hasAdjacentDirtyness; + if (!isNodeDirty(node)) { cleanNode = node; var prevSib = cleanNode.previousSibling; var nextSib = cleanNode.nextSibling; hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || (nextSib && isNodeDirty(nextSib))); - } - else - { + } else { // node is dirty, look for clean node above - var upNode = node.previousSibling; - while (upNode && isNodeDirty(upNode)) - { + let upNode = node.previousSibling; + while (upNode && isNodeDirty(upNode)) { upNode = upNode.previousSibling; } - if (upNode) - { + if (upNode) { cleanNode = upNode; - } - else - { - var downNode = node.nextSibling; - while (downNode && isNodeDirty(downNode)) - { + } else { + let downNode = node.nextSibling; + while (downNode && isNodeDirty(downNode)) { downNode = downNode.nextSibling; } - if (downNode) - { + if (downNode) { cleanNode = downNode; } } - if (!cleanNode) - { + if (!cleanNode) { // Couldn't find any adjacent clean nodes! // Since top and bottom of doc is dirty, the dirty area will be detected. return; @@ -1344,122 +1050,94 @@ function Ace2Inner(){ hasAdjacentDirtyness = true; } - if (hasAdjacentDirtyness) - { + if (hasAdjacentDirtyness) { // previous or next line is dirty - observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; - } - else - { + observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; + } else { // next and prev lines are clean (if they exist) - var lineKey = uniqueId(cleanNode); + const lineKey = uniqueId(cleanNode); var prevSib = cleanNode.previousSibling; var nextSib = cleanNode.nextSibling; - var actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); - var actualNextKey = ((nextSib && uniqueId(nextSib)) || null); - var repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); - var repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); - var repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); - var repNextKey = ((repNextEntry && repNextEntry.key) || null); - if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) - { - observedChanges.cleanNodesNearChanges['$' + uniqueId(cleanNode)] = true; + const actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); + const actualNextKey = ((nextSib && uniqueId(nextSib)) || null); + const repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); + const repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); + const repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); + const repNextKey = ((repNextEntry && repNextEntry.key) || null); + if (actualPrevKey != repPrevKey || actualNextKey != repNextKey) { + observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; } } } - function observeChangesAroundSelection() - { + function observeChangesAroundSelection() { if (currentCallStack.observedSelection) return; currentCallStack.observedSelection = true; - var p = PROFILER("getSelection", false); - var selection = getSelection(); + const p = PROFILER('getSelection', false); + const selection = getSelection(); p.end(); - if (selection) - { - var node1 = topLevel(selection.startPoint.node); - var node2 = topLevel(selection.endPoint.node); + if (selection) { + const node1 = topLevel(selection.startPoint.node); + const node2 = topLevel(selection.endPoint.node); if (node1) observeChangesAroundNode(node1); - if (node2 && node1 != node2) - { + if (node2 && node1 != node2) { observeChangesAroundNode(node2); } } } - function observeSuspiciousNodes() - { + function 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) - { - var nds = root.getElementsByTagName("style"); - for (var i = 0; i < nds.length; i++) - { - var n = topLevel(nds[i]); - if (n && n.parentNode == root) - { + 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) { observeChangesAroundNode(n); } } } } - function incorporateUserChanges(isTimeUp) - { - + function incorporateUserChanges() { if (currentCallStack.domClean) return false; currentCallStack.isUserChange = true; - isTimeUp = (isTimeUp || - function() - { - return false; - }); - if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; - var p = PROFILER("incorp", false); + const p = PROFILER('incorp', false); - //if (doc.body.innerHTML.indexOf("AppJet") >= 0) - //dmesg(htmlPrettyEscape(doc.body.innerHTML)); - //if (top.RECORD) top.RECORD.push(doc.body.innerHTML); // returns true if dom changes were made - if (!root.firstChild) - { - root.innerHTML = "
        "; + if (!root.firstChild) { + root.innerHTML = '
        '; } - p.mark("obs"); + p.mark('obs'); observeChangesAroundSelection(); observeSuspiciousNodes(); - p.mark("dirty"); - var dirtyRanges = getDirtyRanges(); - var dirtyRangesCheckOut = true; - var j = 0; - var a, b; - while (j < dirtyRanges.length) - { + p.mark('dirty'); + let dirtyRanges = getDirtyRanges(); + let dirtyRangesCheckOut = true; + let j = 0; + let a, b; + while (j < dirtyRanges.length) { a = dirtyRanges[j][0]; b = dirtyRanges[j][1]; - if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) - { + if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && (b == rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { dirtyRangesCheckOut = false; break; } j++; } - if (!dirtyRangesCheckOut) - { - var numBodyNodes = root.childNodes.length; - for (var k = 0; k < numBodyNodes; k++) - { - var bodyNode = root.childNodes.item(k); - if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) - { + if (!dirtyRangesCheckOut) { + const numBodyNodes = root.childNodes.length; + for (var k = 0; k < numBodyNodes; k++) { + const bodyNode = root.childNodes.item(k); + if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); } } @@ -1468,202 +1146,173 @@ function Ace2Inner(){ clearObservedChanges(); - p.mark("getsel"); - var selection = getSelection(); - - var selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection - var i = 0; - var splicesToDo = []; - var netNumLinesChangeSoFar = 0; - var toDeleteAtEnd = []; - p.mark("ranges"); - p.literal(dirtyRanges.length, "numdirt"); - var domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] - while (i < dirtyRanges.length) - { - var range = dirtyRanges[i]; + p.mark('getsel'); + const selection = getSelection(); + + let selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection + let i = 0; + const splicesToDo = []; + let netNumLinesChangeSoFar = 0; + const toDeleteAtEnd = []; + p.mark('ranges'); + p.literal(dirtyRanges.length, 'numdirt'); + const domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] + while (i < dirtyRanges.length) { + const range = dirtyRanges[i]; a = range[0]; b = range[1]; - var firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); + let firstDirtyNode = (((a === 0) && root.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); - var lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); + let lastDirtyNode = (((b == rep.lines.length()) && root.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); - if (firstDirtyNode && lastDirtyNode) - { - var cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); + if (firstDirtyNode && lastDirtyNode) { + const cc = makeContentCollector(isStyled, browser, rep.apool, null, className2Author); cc.notifySelection(selection); - var dirtyNodes = []; - for (var n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); - n = n.nextSibling) - { - if (browser.msie) - { - // try to undo IE's pesky and overzealous linkification - try - { - n.createTextRange().execCommand("unlink", false, null); - } - catch (e) - {} - } + const dirtyNodes = []; + for (let n = firstDirtyNode; n && !(n.previousSibling && n.previousSibling == lastDirtyNode); + n = n.nextSibling) { cc.collectContent(n); dirtyNodes.push(n); } cc.notifyNextNode(lastDirtyNode.nextSibling); - var lines = cc.getLines(); - if ((lines.length <= 1 || lines[lines.length - 1] !== "") && lastDirtyNode.nextSibling) - { + let lines = cc.getLines(); + if ((lines.length <= 1 || lines[lines.length - 1] !== '') && lastDirtyNode.nextSibling) { // dirty region doesn't currently end a line, even taking the following node // (or lack of node) into account, so include the following clean node. // It could be SPAN or a DIV; basically this is any case where the contentCollector // decides it isn't done. // Note that this clean node might need to be there for the next dirty range. b++; - var cleanLine = lastDirtyNode.nextSibling; + const cleanLine = lastDirtyNode.nextSibling; cc.collectContent(cleanLine); toDeleteAtEnd.push(cleanLine); cc.notifyNextNode(cleanLine.nextSibling); } - var ccData = cc.finish(); - var ss = ccData.selStart; - var se = ccData.selEnd; + const ccData = cc.finish(); + const ss = ccData.selStart; + const se = ccData.selEnd; lines = ccData.lines; - var lineAttribs = ccData.lineAttribs; - var linesWrapped = ccData.linesWrapped; + const lineAttribs = ccData.lineAttribs; + const linesWrapped = ccData.linesWrapped; var scrollToTheLeftNeeded = false; - if (linesWrapped > 0) - { - if(!browser.msie){ - // chrome decides in it's infinite wisdom that its okay to put the browsers visisble window in the middle of the span - // an outcome of this is that the first chars of the string are no longer visible to the user.. Yay chrome.. - // Move the browsers visible area to the left hand side of the span - // Firefox isn't quite so bad, but it's still pretty quirky. - var scrollToTheLeftNeeded = true; - } + if (linesWrapped > 0) { + // Chrome decides in its infinite wisdom that it's okay to put the browser's visisble + // window in the middle of the span. An outcome of this is that the first chars of the + // string are no longer visible to the user.. Yay chrome.. Move the browser's visible area + // to the left hand side of the span. Firefox isn't quite so bad, but it's still pretty + // quirky. + var scrollToTheLeftNeeded = true; } if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]]; - var entries = []; - var nodeToAddAfter = lastDirtyNode; - var lineNodeInfos = new Array(lines.length); - for (var k = 0; k < lines.length; k++) - { - var lineString = lines[k]; - var newEntry = createDomLineEntry(lineString); + const entries = []; + const nodeToAddAfter = lastDirtyNode; + const lineNodeInfos = new Array(lines.length); + for (var k = 0; k < lines.length; k++) { + const lineString = lines[k]; + const newEntry = createDomLineEntry(lineString); entries.push(newEntry); lineNodeInfos[k] = newEntry.domInfo; } - //var fragment = magicdom.wrapDom(document.createDocumentFragment()); + // var fragment = magicdom.wrapDom(document.createDocumentFragment()); domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - _.each(dirtyNodes,function(n){ + _.each(dirtyNodes, (n) => { toDeleteAtEnd.push(n); }); - var spliceHints = {}; + 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, [], - [] - ]); + } else if (b > a) { + splicesToDo.push([a + netNumLinesChangeSoFar, + b - a, + [], + []]); } i++; } - var domChanges = (splicesToDo.length > 0); + const domChanges = (splicesToDo.length > 0); // update the representation - p.mark("splice"); - _.each(splicesToDo, function(splice) - { + p.mark('splice'); + _.each(splicesToDo, (splice) => { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); }); - //p.mark("relex"); - //rep.lexer.lexCharRange(scroll.getVisibleCharRange(rep), function() { return false; }); - //var isTimeUp = newTimeLimit(100); // do DOM inserts - p.mark("insert"); - _.each(domInsertsNeeded,function(ins) - { - insertDomLines(ins[0], ins[1], isTimeUp); + p.mark('insert'); + _.each(domInsertsNeeded, (ins) => { + insertDomLines(ins[0], ins[1]); }); - p.mark("del"); + p.mark('del'); // delete old dom nodes - _.each(toDeleteAtEnd,function(n) - { - //var id = n.uniqueId(); + _.each(toDeleteAtEnd, (n) => { + // var id = n.uniqueId(); // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) - if(n.parentNode) n.parentNode.removeChild(n); + if (n.parentNode) n.parentNode.removeChild(n); - //dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); + // dmesg(htmlPrettyEscape(htmlForRemovedChild(n))); }); - if(scrollToTheLeftNeeded){ // needed to stop chrome from breaking the ui when long strings without spaces are pasted - $("#innerdocbody").scrollLeft(0); + if (scrollToTheLeftNeeded) { // needed to stop chrome from breaking the ui when long strings without spaces are pasted + $('#innerdocbody').scrollLeft(0); } - p.mark("findsel"); + p.mark('findsel'); // if the nodes that define the selection weren't encountered during // content collection, figure out where those nodes are now. - if (selection && !selStart) - { - //if (domChanges) dmesg("selection not collected"); - var selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { + if (selection && !selStart) { + // if (domChanges) dmesg("selection not collected"); + const selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - root:root, - point:selection.startPoint, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + root, + point: selection.startPoint, + documentAttributeManager, }); - selStart = (selStartFromHook==null||selStartFromHook.length==0)?getLineAndCharForPoint(selection.startPoint):selStartFromHook; + selStart = (selStartFromHook == null || selStartFromHook.length == 0) ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook; } - if (selection && !selEnd) - { - var selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { + if (selection && !selEnd) { + const selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - root:root, - point:selection.endPoint, - documentAttributeManager: documentAttributeManager + editorInfo, + rep, + root, + point: selection.endPoint, + documentAttributeManager, }); - selEnd = (selEndFromHook==null||selEndFromHook.length==0)?getLineAndCharForPoint(selection.endPoint):selEndFromHook; + selEnd = (selEndFromHook == null || selEndFromHook.length == 0) ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook; } // selection from content collection can, in various ways, extend past final // BR in firefox DOM, so cap the line - var numLines = rep.lines.length(); - if (selStart && selStart[0] >= numLines) - { + const numLines = rep.lines.length(); + if (selStart && selStart[0] >= numLines) { selStart[0] = numLines - 1; selStart[1] = rep.lines.atIndex(selStart[0]).text.length; } - if (selEnd && selEnd[0] >= numLines) - { + if (selEnd && selEnd[0] >= numLines) { selEnd[0] = numLines - 1; selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; } - p.mark("repsel"); + p.mark('repsel'); // update rep if we have a new selection // NOTE: IE loses the selection when you click stuff in e.g. the // editbar, so removing the selection when it's lost is not a good // idea. if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart); // update browser selection - p.mark("browsel"); - if (selection && (domChanges || isCaret())) - { + p.mark('browsel'); + if (selection && (domChanges || isCaret())) { // if no DOM changes (not this case), want to treat range selection delicately, // e.g. in IE not lose which end of the selection is the focus/anchor; // on the other hand, we may have just noticed a press of PageUp/PageDown @@ -1672,11 +1321,11 @@ function Ace2Inner(){ currentCallStack.domClean = true; - p.mark("fixview"); + p.mark('fixview'); fixView(); - p.end("END"); + p.end('END'); return domChanges; } @@ -1686,90 +1335,67 @@ function Ace2Inner(){ italic: true, underline: true, strikethrough: true, - list: true + list: true, }; - function isStyleAttribute(aname) - { + function isStyleAttribute(aname) { return !!STYLE_ATTRIBS[aname]; } - function isDefaultLineAttribute(aname) - { + function isDefaultLineAttribute(aname) { return AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1; } - function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) - { - isTimeUp = (isTimeUp || - function() - { - return false; - }); - - var lastEntry; - var lineStartOffset; + function insertDomLines(nodeToAddAfter, infoStructs) { + let lastEntry; + let lineStartOffset; if (infoStructs.length < 1) return; - var startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); - var endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); - var charStart = rep.lines.offsetOfEntry(startEntry); - var charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; - - //rep.lexer.lexCharRange([charStart, charEnd], isTimeUp); - _.each(infoStructs, function(info) - { - var p2 = PROFILER("insertLine", false); - var node = info.node; - var key = uniqueId(node); - var entry; - p2.mark("findEntry"); - if (lastEntry) - { + const startEntry = rep.lines.atKey(uniqueId(infoStructs[0].node)); + const endEntry = rep.lines.atKey(uniqueId(infoStructs[infoStructs.length - 1].node)); + const charStart = rep.lines.offsetOfEntry(startEntry); + const charEnd = rep.lines.offsetOfEntry(endEntry) + endEntry.width; + + _.each(infoStructs, (info) => { + const p2 = PROFILER('insertLine', false); + const node = info.node; + const key = uniqueId(node); + let entry; + p2.mark('findEntry'); + if (lastEntry) { // optimization to avoid recalculation - var next = rep.lines.next(lastEntry); - if (next && next.key == key) - { + const next = rep.lines.next(lastEntry); + if (next && next.key == key) { entry = next; lineStartOffset += lastEntry.width; } } - if (!entry) - { - p2.literal(1, "nonopt"); + if (!entry) { + p2.literal(1, 'nonopt'); entry = rep.lines.atKey(key); lineStartOffset = rep.lines.offsetOfKey(key); - } - else p2.literal(0, "nonopt"); + } else { p2.literal(0, 'nonopt'); } lastEntry = entry; - p2.mark("spans"); - getSpansForLine(entry, function(tokenText, tokenClass) - { + p2.mark('spans'); + getSpansForLine(entry, (tokenText, tokenClass) => { info.appendSpan(tokenText, tokenClass); - }, lineStartOffset, isTimeUp()); - //else if (entry.text.length > 0) { - //info.appendSpan(entry.text, 'dirty'); - //} - p2.mark("addLine"); + }, lineStartOffset); + p2.mark('addLine'); info.prepareForAdd(); entry.lineMarker = info.lineMarker; - if (!nodeToAddAfter) - { + if (!nodeToAddAfter) { root.insertBefore(node, root.firstChild); - } - else - { + } else { root.insertBefore(node, nodeToAddAfter.nextSibling); } nodeToAddAfter = node; info.notifyAdded(); - p2.mark("markClean"); + p2.mark('markClean'); markNodeClean(node); p2.end(); }); } - function isCaret() - { + function isCaret() { return (rep.selStart && rep.selEnd && rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]); } editorInfo.ace_isCaret = isCaret; @@ -1777,430 +1403,285 @@ function Ace2Inner(){ // prereq: isCaret() - function caretLine() - { + function caretLine() { return rep.selStart[0]; } editorInfo.ace_caretLine = caretLine; - function caretColumn() - { + function caretColumn() { return rep.selStart[1]; } editorInfo.ace_caretColumn = caretColumn; - function caretDocChar() - { + function caretDocChar() { return rep.lines.offsetOfIndex(caretLine()) + caretColumn(); } editorInfo.ace_caretDocChar = caretDocChar; - function handleReturnIndentation() - { + function handleReturnIndentation() { // on return, indent to level of previous line - if (isCaret() && caretColumn() === 0 && caretLine() > 0) - { - var lineNum = caretLine(); - var thisLine = rep.lines.atIndex(lineNum); - var prevLine = rep.lines.prev(thisLine); - var prevLineText = prevLine.text; - var theIndent = /^ *(?:)/.exec(prevLineText)[0]; - var shouldIndent = parent.parent.clientVars.indentationOnNewLine; - if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) - { + if (isCaret() && caretColumn() === 0 && caretLine() > 0) { + const lineNum = caretLine(); + const thisLine = rep.lines.atIndex(lineNum); + const prevLine = rep.lines.prev(thisLine); + const prevLineText = prevLine.text; + let theIndent = /^ *(?:)/.exec(prevLineText)[0]; + const shouldIndent = parent.parent.clientVars.indentationOnNewLine; + if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) { theIndent += THE_TAB; } - var cs = Changeset.builder(rep.lines.totalWidth()).keep( - rep.lines.offsetOfIndex(lineNum), lineNum).insert( - theIndent, [ - ['author', thisAuthor] - ], rep.apool).toString(); + const cs = Changeset.builder(rep.lines.totalWidth()).keep( + rep.lines.offsetOfIndex(lineNum), lineNum).insert( + theIndent, [ + ['author', thisAuthor], + ], rep.apool).toString(); performDocumentApplyChangeset(cs); performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); } } - function getPointForLineAndChar(lineAndChar) - { - var line = lineAndChar[0]; - var charsLeft = lineAndChar[1]; + function getPointForLineAndChar(lineAndChar) { + const line = lineAndChar[0]; + let charsLeft = lineAndChar[1]; // Do not uncomment this in production it will break iFrames. - //top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, - //getCleanNodeByKey(rep.lines.atIndex(line).key)); - var lineEntry = rep.lines.atIndex(line); + // top.console.log("line: %d, key: %s, node: %o", line, rep.lines.atIndex(line).key, + // getCleanNodeByKey(rep.lines.atIndex(line).key)); + const lineEntry = rep.lines.atIndex(line); charsLeft -= lineEntry.lineMarker; - if (charsLeft < 0) - { + if (charsLeft < 0) { charsLeft = 0; } - var lineNode = lineEntry.lineNode; - var n = lineNode; - var after = false; - if (charsLeft === 0) - { - var index = 0; - - if (browser.msie && parseInt(browser.version) >= 11) { - browser.msie = false; // Temp fix to resolve enter and backspace issues.. - // Note that this makes MSIE behave like modern browsers.. - } - if (browser.msie && line == (rep.lines.length() - 1) && lineNode.childNodes.length === 0) - { - // best to stay at end of last empty div in IE - index = 1; - } + const lineNode = lineEntry.lineNode; + let n = lineNode; + let after = false; + if (charsLeft === 0) { + let index = 0; return { node: lineNode, - index: index, - maxIndex: 1 + index, + maxIndex: 1, }; } - while (!(n == lineNode && after)) - { - if (after) - { - if (n.nextSibling) - { + while (!(n == lineNode && after)) { + if (after) { + if (n.nextSibling) { n = n.nextSibling; after = false; + } else { n = n.parentNode; } + } else if (isNodeText(n)) { + const len = n.nodeValue.length; + if (charsLeft <= len) { + return { + node: n, + index: charsLeft, + maxIndex: len, + }; } - else n = n.parentNode; - } - else - { - if (isNodeText(n)) - { - var len = n.nodeValue.length; - if (charsLeft <= len) - { - return { - node: n, - index: charsLeft, - maxIndex: len - }; - } - charsLeft -= len; - after = true; - } - else - { - if (n.firstChild) n = n.firstChild; - else after = true; - } - } + charsLeft -= len; + after = true; + } else if (n.firstChild) { n = n.firstChild; } else { after = true; } } return { node: lineNode, index: 1, - maxIndex: 1 + maxIndex: 1, }; } - function nodeText(n) - { - if (browser.msie) { - return n.innerText; - } else { - return n.textContent || n.nodeValue || ''; - } + function nodeText(n) { + return n.textContent || n.nodeValue || ''; } - function getLineAndCharForPoint(point) - { + function getLineAndCharForPoint(point) { // Turn DOM node selection into [line,char] selection. // This method has to work when the DOM is not pristine, // assuming the point is not in a dirty node. - if (point.node == root) - { - if (point.index === 0) - { + if (point.node == root) { + if (point.index === 0) { return [0, 0]; - } - else - { - var N = rep.lines.length(); - var ln = rep.lines.atIndex(N - 1); + } else { + const N = rep.lines.length(); + const ln = rep.lines.atIndex(N - 1); return [N - 1, ln.text.length]; } - } - else - { - var n = point.node; - var col = 0; + } else { + let n = point.node; + let col = 0; // if this part fails, it probably means the selection node // was dirty, and we didn't see it when collecting dirty nodes. - if (isNodeText(n)) - { + if (isNodeText(n)) { col = point.index; - } - else if (point.index > 0) - { + } else if (point.index > 0) { col = nodeText(n).length; } - var parNode, prevSib; - while ((parNode = n.parentNode) != root) - { - if ((prevSib = n.previousSibling)) - { + let parNode, prevSib; + while ((parNode = n.parentNode) != root) { + if ((prevSib = n.previousSibling)) { n = prevSib; col += nodeText(n).length; - } - else - { + } else { n = parNode; } } - if (n.firstChild && isBlockElement(n.firstChild)) - { + if (n.firstChild && isBlockElement(n.firstChild)) { col += 1; // lineMarker } - var lineEntry = rep.lines.atKey(n.id); - var lineNum = rep.lines.indexOfEntry(lineEntry); + const lineEntry = rep.lines.atKey(n.id); + const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, col]; } } editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; - function createDomLineEntry(lineString) - { - var info = doCreateDomLine(lineString.length > 0); - var newNode = info.node; + function createDomLineEntry(lineString) { + const info = doCreateDomLine(lineString.length > 0); + const newNode = info.node; return { key: uniqueId(newNode), text: lineString, lineNode: newNode, domInfo: info, - lineMarker: 0 + lineMarker: 0, }; } - function canApplyChangesetToDocument(changes) - { + function canApplyChangesetToDocument(changes) { return Changeset.oldLen(changes) == rep.alltext.length; } - function performDocumentApplyChangeset(changes, insertsAfterSelection) - { + function performDocumentApplyChangeset(changes, insertsAfterSelection) { doRepApplyChangeset(changes, insertsAfterSelection); - var requiredSelectionSetting = null; - if (rep.selStart && rep.selEnd) - { - var selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - var selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; - var result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); + let requiredSelectionSetting = null; + if (rep.selStart && rep.selEnd) { + const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; + const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; + const result = Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; } - var linesMutatee = { - splice: function(start, numRemoved, newLinesVA) - { - var args = Array.prototype.slice.call(arguments, 2); - domAndRepSplice(start, numRemoved, _.map(args, function(s){ return s.slice(0, -1); }), null); + const linesMutatee = { + splice(start, numRemoved, newLinesVA) { + const args = Array.prototype.slice.call(arguments, 2); + domAndRepSplice(start, numRemoved, _.map(args, (s) => s.slice(0, -1))); }, - get: function(i) - { - return rep.lines.atIndex(i).text + '\n'; + get(i) { + return `${rep.lines.atIndex(i).text}\n`; }, - length: function() - { + length() { return rep.lines.length(); }, - slice_notused: function(start, end) - { - return _.map(rep.lines.slice(start, end), function(e) - { - return e.text + '\n'; - }); - } + slice_notused(start, end) { + return _.map(rep.lines.slice(start, end), (e) => `${e.text}\n`); + }, }; Changeset.mutateTextLines(changes, linesMutatee); - checkALines(); - - if (requiredSelectionSetting) - { + if (requiredSelectionSetting) { performSelectionChange(lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), requiredSelectionSetting[2]); } - function domAndRepSplice(startLine, deleteCount, newLineStrings, isTimeUp) - { - // dgreensp 3/2009: the spliced lines may be in the middle of a dirty region, - // so if no explicit time limit, don't spend a lot of time highlighting - isTimeUp = (isTimeUp || newTimeLimit(50)); - - var keysToDelete = []; - if (deleteCount > 0) - { - var entryToDelete = rep.lines.atIndex(startLine); - for (var i = 0; i < deleteCount; i++) - { + function domAndRepSplice(startLine, deleteCount, newLineStrings) { + const keysToDelete = []; + if (deleteCount > 0) { + let entryToDelete = rep.lines.atIndex(startLine); + for (let i = 0; i < deleteCount; i++) { keysToDelete.push(entryToDelete.key); entryToDelete = rep.lines.next(entryToDelete); } } - var lineEntries = _.map(newLineStrings, createDomLineEntry); + const lineEntries = _.map(newLineStrings, createDomLineEntry); doRepLineSplice(startLine, deleteCount, lineEntries); - var nodeToAddAfter; - if (startLine > 0) - { + let nodeToAddAfter; + if (startLine > 0) { nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key); - } - else nodeToAddAfter = null; + } else { nodeToAddAfter = null; } - insertDomLines(nodeToAddAfter, _.map(lineEntries, function(entry) - { - return entry.domInfo; - }), isTimeUp); + insertDomLines(nodeToAddAfter, _.map(lineEntries, (entry) => entry.domInfo)); - _.each(keysToDelete, function(k) - { - var n = doc.getElementById(k); + _.each(keysToDelete, (k) => { + const n = doc.getElementById(k); n.parentNode.removeChild(n); }); - if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) - { + if ((rep.selStart && rep.selStart[0] >= startLine && rep.selStart[0] <= startLine + deleteCount) || (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) { currentCallStack.selectionAffected = true; } } } - function checkChangesetLineInformationAgainstRep(changes) - { - return true; // disable for speed - var opIter = Changeset.opIterator(Changeset.unpack(changes).ops); - var curOffset = 0; - var curLine = 0; - var curCol = 0; - while (opIter.hasNext()) - { - var o = opIter.next(); - if (o.opcode == '-' || o.opcode == '=') - { - curOffset += o.chars; - if (o.lines) - { - curLine += o.lines; - curCol = 0; - } - else - { - curCol += o.chars; - } - } - var calcLine = rep.lines.indexOfOffset(curOffset); - var calcLineStart = rep.lines.offsetOfIndex(calcLine); - var calcCol = curOffset - calcLineStart; - if (calcCol != curCol || calcLine != curLine) - { - return false; - } - } - return true; - } - - function doRepApplyChangeset(changes, insertsAfterSelection) - { + function doRepApplyChangeset(changes, insertsAfterSelection) { Changeset.checkRep(changes); - if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error("doRepApplyChangeset length mismatch: " + Changeset.oldLen(changes) + "/" + rep.alltext.length); - - if (!checkChangesetLineInformationAgainstRep(changes)) - { - throw new Error("doRepApplyChangeset line break mismatch"); - } + if (Changeset.oldLen(changes) != rep.alltext.length) throw new Error(`doRepApplyChangeset length mismatch: ${Changeset.oldLen(changes)}/${rep.alltext.length}`); - (function doRecordUndoInformation(changes) - { - var editEvent = currentCallStack.editEvent; - if (editEvent.eventType == "nonundoable") - { - if (!editEvent.changeset) - { + (function doRecordUndoInformation(changes) { + const editEvent = currentCallStack.editEvent; + if (editEvent.eventType == 'nonundoable') { + if (!editEvent.changeset) { editEvent.changeset = changes; - } - else - { + } else { editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); } - } - else - { - var inverseChangeset = Changeset.inverse(changes, { - get: function(i) - { - return rep.lines.atIndex(i).text + '\n'; + } else { + const inverseChangeset = Changeset.inverse(changes, { + get(i) { + return `${rep.lines.atIndex(i).text}\n`; }, - length: function() - { + length() { return rep.lines.length(); - } + }, }, rep.alines, rep.apool); - if (!editEvent.backset) - { + if (!editEvent.backset) { editEvent.backset = inverseChangeset; - } - else - { + } else { editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); } } })(changes); - //rep.alltext = Changeset.applyToText(changes, rep.alltext); + // rep.alltext = Changeset.applyToText(changes, rep.alltext); Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); - if (changesetTracker.isTracking()) - { + if (changesetTracker.isTracking()) { changesetTracker.composeUserChangeset(changes); } - } /* Converts the position of a char (index in String) into a [row, col] tuple */ - function lineAndColumnFromChar(x) - { - var lineEntry = rep.lines.atOffset(x); - var lineStart = rep.lines.offsetOfEntry(lineEntry); - var lineNum = rep.lines.indexOfEntry(lineEntry); + function lineAndColumnFromChar(x) { + const lineEntry = rep.lines.atOffset(x); + const lineStart = rep.lines.offsetOfEntry(lineEntry); + const lineNum = rep.lines.indexOfEntry(lineEntry); return [lineNum, x - lineStart]; } - function performDocumentReplaceCharRange(startChar, endChar, newText) - { - if (startChar == endChar && newText.length === 0) - { + function performDocumentReplaceCharRange(startChar, endChar, newText) { + if (startChar == endChar && newText.length === 0) { return; } // Requires that the replacement preserve the property that the // internal document text ends in a newline. Given this, we // rewrite the splice so that it doesn't touch the very last // char of the document. - if (endChar == rep.alltext.length) - { - if (startChar == endChar) - { + if (endChar == rep.alltext.length) { + if (startChar == endChar) { // an insert at end startChar--; endChar--; - newText = '\n' + newText.substring(0, newText.length - 1); - } - else if (newText.length === 0) - { + newText = `\n${newText.substring(0, newText.length - 1)}`; + } else if (newText.length === 0) { // a delete at end startChar--; endChar--; - } - else - { + } else { // a replace at end endChar--; newText = newText.substring(0, newText.length - 1); @@ -2209,106 +1690,102 @@ function Ace2Inner(){ performDocumentReplaceRange(lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); } - function performDocumentReplaceRange(start, end, newText) - { + function performDocumentReplaceRange(start, end, newText) { if (start === undefined) start = rep.selStart; if (end === undefined) end = rep.selEnd; - //dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); + // dmesg(String([start.toSource(),end.toSource(),newText.toSource()])); // start[0]: <--- start[1] --->CCCCCCCCCCC\n // CCCCCCCCCCCCCCCCCCCC\n // CCCC\n // end[0]: -------\n - var builder = Changeset.builder(rep.lines.totalWidth()); + const builder = Changeset.builder(rep.lines.totalWidth()); ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); ChangesetUtils.buildRemoveRange(rep, builder, start, end); builder.insert(newText, [ - ['author', thisAuthor] + ['author', thisAuthor], ], rep.apool); - var cs = builder.toString(); + const cs = builder.toString(); performDocumentApplyChangeset(cs); } - function performDocumentApplyAttributesToCharRange(start, end, attribs) - { + function performDocumentApplyAttributesToCharRange(start, end, attribs) { end = Math.min(end, rep.alltext.length - 1); documentAttributeManager.setAttributesOnRange(lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); } editorInfo.ace_performDocumentApplyAttributesToCharRange = performDocumentApplyAttributesToCharRange; - function setAttributeOnSelection(attributeName, attributeValue) - { + function setAttributeOnSelection(attributeName, attributeValue) { if (!(rep.selStart && rep.selEnd)) return; documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ - [attributeName, attributeValue] + [attributeName, attributeValue], ]); } editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection; - function getAttributeOnSelection(attributeName, prevChar){ - if (!(rep.selStart && rep.selEnd)) return - var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if(isNotSelection){ - if(prevChar){ + function getAttributeOnSelection(attributeName, prevChar) { + if (!(rep.selStart && rep.selEnd)) return; + const isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); + if (isNotSelection) { + if (prevChar) { // If it's not the start of the line - if(rep.selStart[1] !== 0){ + if (rep.selStart[1] !== 0) { rep.selStart[1]--; } } } - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); - function hasIt(attribs) - { + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); + function hasIt(attribs) { return withItRegex.test(attribs); } - return rangeHasAttrib(rep.selStart, rep.selEnd) + return rangeHasAttrib(rep.selStart, rep.selEnd); function rangeHasAttrib(selStart, selEnd) { // if range is collapsed -> no attribs in range - if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false + if (selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false; - if(selStart[0] != selEnd[0]) { // -> More than one line selected - var hasAttrib = true + if (selStart[0] != selEnd[0]) { // -> More than one line selected + var hasAttrib = true; // from selStart to the end of the first line - hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]) + hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); // for all lines in between - for(var n=selStart[0]+1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]) + for (let n = selStart[0] + 1; n < selEnd[0]; n++) { + hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); } // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]) + hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - return hasAttrib + return hasAttrib; } // Logic tells us we now have a range on a single line - var lineNum = selStart[0] - , start = selStart[1] - , end = selEnd[1] - , hasAttrib = true + const lineNum = selStart[0]; + const start = selStart[1]; + const end = selEnd[1]; + var hasAttrib = true; // Iterate over attribs on this line - var opIter = Changeset.opIterator(rep.alines[lineNum]) - , indexIntoLine = 0 + const opIter = Changeset.opIterator(rep.alines[lineNum]); + let indexIntoLine = 0; while (opIter.hasNext()) { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { // does op overlap selection? if (!(opEndInLine <= start || opStartInLine >= end)) { @@ -2319,70 +1796,61 @@ function Ace2Inner(){ indexIntoLine = opEndInLine; } - return hasAttrib + return hasAttrib; } } editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection; - function toggleAttributeOnSelection(attributeName) - { + function toggleAttributeOnSelection(attributeName) { if (!(rep.selStart && rep.selEnd)) return; - var selectionAllHasIt = true; - var withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'] + let selectionAllHasIt = true; + const withIt = Changeset.makeAttribsString('+', [ + [attributeName, 'true'], ], rep.apool); - var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)"); + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - function hasIt(attribs) - { + function hasIt(attribs) { return withItRegex.test(attribs); } - var selStartLine = rep.selStart[0]; - var selEndLine = rep.selEnd[0]; - for (var n = selStartLine; n <= selEndLine; n++) - { - var opIter = Changeset.opIterator(rep.alines[n]); - var indexIntoLine = 0; - var selectionStartInLine = 0; + const selStartLine = rep.selStart[0]; + const selEndLine = rep.selEnd[0]; + for (let n = selStartLine; n <= selEndLine; n++) { + const opIter = Changeset.opIterator(rep.alines[n]); + let indexIntoLine = 0; + let selectionStartInLine = 0; if (documentAttributeManager.lineHasMarker(n)) { selectionStartInLine = 1; // ignore "*" used as line marker } - var selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline - if (n == selStartLine) - { + let selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline + if (n == selStartLine) { selectionStartInLine = rep.selStart[1]; } - if (n == selEndLine) - { + if (n == selEndLine) { selectionEndInLine = rep.selEnd[1]; } - while (opIter.hasNext()) - { - var op = opIter.next(); - var opStartInLine = indexIntoLine; - var opEndInLine = opStartInLine + op.chars; - if (!hasIt(op.attribs)) - { + while (opIter.hasNext()) { + const op = opIter.next(); + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; + if (!hasIt(op.attribs)) { // does op overlap selection? - if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) - { + if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) { selectionAllHasIt = false; break; } } indexIntoLine = opEndInLine; } - if (!selectionAllHasIt) - { + if (!selectionAllHasIt) { break; } } - var attributeValue = selectionAllHasIt ? '' : 'true'; + const attributeValue = selectionAllHasIt ? '' : 'true'; documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); if (attribIsFormattingStyle(attributeName)) { updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... @@ -2390,8 +1858,7 @@ function Ace2Inner(){ } editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; - function performDocumentReplaceSelection(newText) - { + function performDocumentReplaceSelection(newText) { if (!(rep.selStart && rep.selEnd)) return; performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); } @@ -2400,73 +1867,60 @@ function Ace2Inner(){ // Must be called after rep.alltext is set. - function doRepLineSplice(startLine, deleteCount, newLineEntries) - { - - _.each(newLineEntries, function(entry) - { + function doRepLineSplice(startLine, deleteCount, newLineEntries) { + _.each(newLineEntries, (entry) => { entry.width = entry.text.length + 1; }); - var startOldChar = rep.lines.offsetOfIndex(startLine); - var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + const startOldChar = rep.lines.offsetOfIndex(startLine); + const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - var oldRegionStart = rep.lines.offsetOfIndex(startLine); - var oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); + const oldRegionStart = rep.lines.offsetOfIndex(startLine); + const oldRegionEnd = rep.lines.offsetOfIndex(startLine + deleteCount); rep.lines.splice(startLine, deleteCount, newLineEntries); currentCallStack.docTextChanged = true; currentCallStack.repChanged = true; - var newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); + const newRegionEnd = rep.lines.offsetOfIndex(startLine + newLineEntries.length); - var newText = _.map(newLineEntries, function(e) - { - return e.text + '\n'; - }).join(''); + const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); rep.alltext = rep.alltext.substring(0, startOldChar) + newText + rep.alltext.substring(endOldChar, rep.alltext.length); - //var newTotalLength = rep.alltext.length; - //rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, - //newRegionEnd - oldRegionStart); + // var newTotalLength = rep.alltext.length; + // rep.lexer.updateBuffer(rep.alltext, oldRegionStart, oldRegionEnd - oldRegionStart, + // newRegionEnd - oldRegionStart); } - function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) - { - var startOldChar = rep.lines.offsetOfIndex(startLine); - var endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); + function doIncorpLineSplice(startLine, deleteCount, newLineEntries, lineAttribs, hints) { + const startOldChar = rep.lines.offsetOfIndex(startLine); + const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - var oldRegionStart = rep.lines.offsetOfIndex(startLine); + const oldRegionStart = rep.lines.offsetOfIndex(startLine); - var selStartHintChar, selEndHintChar; - if (hints && hints.selStart) - { + let selStartHintChar, selEndHintChar; + if (hints && hints.selStart) { selStartHintChar = rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; } - if (hints && hints.selEnd) - { + if (hints && hints.selEnd) { selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; } - var newText = _.map(newLineEntries, function(e) - { - return e.text + '\n'; - }).join(''); - var oldText = rep.alltext.substring(startOldChar, endOldChar); - var oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); - var newAttribs = lineAttribs.join('|1+1') + '|1+1'; // not valid in a changeset - var analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); - var commonStart = analysis[0]; - var commonEnd = analysis[1]; - var shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); - var shortNewText = newText.substring(commonStart, newText.length - commonEnd); - var spliceStart = startOldChar + commonStart; - var spliceEnd = endOldChar - commonEnd; - var shiftFinalNewlineToBeforeNewText = false; + const newText = _.map(newLineEntries, (e) => `${e.text}\n`).join(''); + const oldText = rep.alltext.substring(startOldChar, endOldChar); + const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); + const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset + const analysis = analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); + const commonStart = analysis[0]; + let commonEnd = analysis[1]; + let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); + let shortNewText = newText.substring(commonStart, newText.length - commonEnd); + let spliceStart = startOldChar + commonStart; + let spliceEnd = endOldChar - commonEnd; + let shiftFinalNewlineToBeforeNewText = false; // adjust the splice to not involve the final newline of the document; // be very defensive - if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') - { + if (shortOldText.charAt(shortOldText.length - 1) == '\n' && shortNewText.charAt(shortNewText.length - 1) == '\n') { // replacing text that ends in newline with text that also ends in newline // (still, after analysis, somehow) shortOldText = shortOldText.slice(0, -1); @@ -2474,19 +1928,16 @@ function Ace2Inner(){ spliceEnd--; commonEnd++; } - if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) - { + if (shortOldText.length === 0 && spliceStart == rep.alltext.length && shortNewText.length > 0) { // inserting after final newline, bad spliceStart--; spliceEnd--; - shortNewText = '\n' + shortNewText.slice(0, -1); + shortNewText = `\n${shortNewText.slice(0, -1)}`; shiftFinalNewlineToBeforeNewText = true; } - if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) - { + if (spliceEnd == rep.alltext.length && shortOldText.length > 0 && shortNewText.length === 0) { // deletion at end of rep.alltext - if (rep.alltext.charAt(spliceStart - 1) == '\n') - { + if (rep.alltext.charAt(spliceStart - 1) == '\n') { // (if not then what the heck? it will definitely lead // to a rep.alltext without a final newline) spliceStart--; @@ -2494,288 +1945,228 @@ function Ace2Inner(){ } } - if (!(shortOldText.length === 0 && shortNewText.length === 0)) - { - var oldDocText = rep.alltext; - var oldLen = oldDocText.length; + if (!(shortOldText.length === 0 && shortNewText.length === 0)) { + const oldDocText = rep.alltext; + const oldLen = oldDocText.length; - var spliceStartLine = rep.lines.indexOfOffset(spliceStart); - var spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); + const spliceStartLine = rep.lines.indexOfOffset(spliceStart); + const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); - var startBuilder = function() - { - var builder = Changeset.builder(oldLen); + const startBuilder = function () { + const builder = Changeset.builder(oldLen); builder.keep(spliceStartLineStart, spliceStartLine); builder.keep(spliceStart - spliceStartLineStart); return builder; }; - var eachAttribRun = function(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) - { - var attribsIter = Changeset.opIterator(attribs); - var textIndex = 0; - var newTextStart = commonStart; - var newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); - while (attribsIter.hasNext()) - { - var op = attribsIter.next(); - var nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) - { + const eachAttribRun = function (attribs, func /* (startInNewText, endInNewText, attribs)*/) { + const attribsIter = Changeset.opIterator(attribs); + let textIndex = 0; + const newTextStart = commonStart; + const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); + 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; } }; - var justApplyStyles = (shortNewText == shortOldText); - var theChangeset; + const justApplyStyles = (shortNewText == shortOldText); + let theChangeset; - if (justApplyStyles) - { + if (justApplyStyles) { // create changeset that clears the incorporated styles on // the existing text. we compose this with the // changeset the applies the styles found in the DOM. // This allows us to incorporate, e.g., Safari's native "unbold". - var incorpedAttribClearer = cachedStrFunc(function(oldAtts) - { - return Changeset.mapAttribNumbers(oldAtts, function(n) - { - var k = rep.apool.getAttribKey(n); - if (isStyleAttribute(k)) - { - return rep.apool.putAttrib([k, '']); - } - return false; - }); - }); + const incorpedAttribClearer = cachedStrFunc((oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => { + const k = rep.apool.getAttribKey(n); + if (isStyleAttribute(k)) { + return rep.apool.putAttrib([k, '']); + } + return false; + })); - var builder1 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) - { + const builder1 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { builder1.keep(1, 1); } - eachAttribRun(oldAttribs, function(start, end, attribs) - { + eachAttribRun(oldAttribs, (start, end, attribs) => { builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs)); }); - var clearer = builder1.toString(); + const clearer = builder1.toString(); - var builder2 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) - { + const builder2 = startBuilder(); + if (shiftFinalNewlineToBeforeNewText) { builder2.keep(1, 1); } - eachAttribRun(newAttribs, function(start, end, attribs) - { + eachAttribRun(newAttribs, (start, end, attribs) => { builder2.keepText(newText.substring(start, end), attribs); }); - var styler = builder2.toString(); + const styler = builder2.toString(); theChangeset = Changeset.compose(clearer, styler, rep.apool); - } - else - { - var builder = startBuilder(); + } else { + const builder = startBuilder(); - var spliceEndLine = rep.lines.indexOfOffset(spliceEnd); - var spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); - if (spliceEndLineStart > spliceStart) - { + const spliceEndLine = rep.lines.indexOfOffset(spliceEnd); + const spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); + if (spliceEndLineStart > spliceStart) { builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine); builder.remove(spliceEnd - spliceEndLineStart); - } - else - { + } else { builder.remove(spliceEnd - spliceStart); } - var isNewTextMultiauthor = false; - var authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ - ['author', thisAuthor] + let isNewTextMultiauthor = false; + const authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ + ['author', thisAuthor], ] : []), rep.apool); - var authorizer = cachedStrFunc(function(oldAtts) - { - if (isNewTextMultiauthor) - { + const authorizer = cachedStrFunc((oldAtts) => { + if (isNewTextMultiauthor) { // prefer colors from DOM return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool); - } - else - { + } else { // use this author's color return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool); } }); - var foundDomAuthor = ''; - eachAttribRun(newAttribs, function(start, end, attribs) - { - var a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); - if (a && a != foundDomAuthor) - { - if (!foundDomAuthor) - { + let foundDomAuthor = ''; + eachAttribRun(newAttribs, (start, end, attribs) => { + const a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); + if (a && a != foundDomAuthor) { + if (!foundDomAuthor) { foundDomAuthor = a; - } - else - { + } else { isNewTextMultiauthor = true; // multiple authors in DOM! } } }); - if (shiftFinalNewlineToBeforeNewText) - { + if (shiftFinalNewlineToBeforeNewText) { builder.insert('\n', authorizer('')); } - eachAttribRun(newAttribs, function(start, end, attribs) - { + eachAttribRun(newAttribs, (start, end, attribs) => { builder.insert(newText.substring(start, end), authorizer(attribs)); }); theChangeset = builder.toString(); } - //dmesg(htmlPrettyEscape(theChangeset)); + // dmesg(htmlPrettyEscape(theChangeset)); doRepApplyChangeset(theChangeset); } // do this no matter what, because we need to get the right // line keys into the rep. doRepLineSplice(startLine, deleteCount, newLineEntries); - - checkALines(); } - function cachedStrFunc(func) - { - var cache = {}; - return function(s) - { - if (!cache[s]) - { + function cachedStrFunc(func) { + const cache = {}; + return function (s) { + if (!cache[s]) { cache[s] = func(s); } return cache[s]; }; } - function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) - { + function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) { // we need to take into account both the styles attributes & attributes defined by // the plugins, so basically we can ignore only the default line attribs used by // Etherpad - function incorpedAttribFilter(anum) - { + function incorpedAttribFilter(anum) { return !isDefaultLineAttribute(rep.apool.getAttribKey(anum)); } - function attribRuns(attribs) - { - var lengs = []; - var atts = []; - var iter = Changeset.opIterator(attribs); - while (iter.hasNext()) - { - var op = iter.next(); + function attribRuns(attribs) { + const lengs = []; + const atts = []; + const iter = Changeset.opIterator(attribs); + while (iter.hasNext()) { + const op = iter.next(); lengs.push(op.chars); atts.push(op.attribs); } return [lengs, atts]; } - function attribIterator(runs, backward) - { - var lengs = runs[0]; - var atts = runs[1]; - var i = (backward ? lengs.length - 1 : 0); - var j = 0; - return function next() - { - while (j >= lengs[i]) - { + function attribIterator(runs, backward) { + const lengs = runs[0]; + const atts = runs[1]; + let i = (backward ? lengs.length - 1 : 0); + let j = 0; + return function next() { + while (j >= lengs[i]) { if (backward) i--; else i++; j = 0; } - var a = atts[i]; + const a = atts[i]; j++; return a; }; } - var oldLen = oldText.length; - var newLen = newText.length; - var minLen = Math.min(oldLen, newLen); + const oldLen = oldText.length; + const newLen = newText.length; + const minLen = Math.min(oldLen, newLen); - var oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); - var newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); + const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); + const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); - var commonStart = 0; - var oldStartIter = attribIterator(oldARuns, false); - var newStartIter = attribIterator(newARuns, false); - while (commonStart < minLen) - { - if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) - { + let commonStart = 0; + const oldStartIter = attribIterator(oldARuns, false); + const newStartIter = attribIterator(newARuns, false); + while (commonStart < minLen) { + if (oldText.charAt(commonStart) == newText.charAt(commonStart) && oldStartIter() == newStartIter()) { commonStart++; - } - else break; + } else { break; } } - var commonEnd = 0; - var oldEndIter = attribIterator(oldARuns, true); - var newEndIter = attribIterator(newARuns, true); - while (commonEnd < minLen) - { - if (commonEnd === 0) - { + let commonEnd = 0; + const oldEndIter = attribIterator(oldARuns, true); + const newEndIter = attribIterator(newARuns, true); + while (commonEnd < minLen) { + if (commonEnd === 0) { // assume newline in common oldEndIter(); newEndIter(); commonEnd++; - } - else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) - { + } else if (oldText.charAt(oldLen - 1 - commonEnd) == newText.charAt(newLen - 1 - commonEnd) && oldEndIter() == newEndIter()) { commonEnd++; - } - else break; + } else { break; } } - var hintedCommonEnd = -1; - if ((typeof optSelEndHint) == "number") - { + let hintedCommonEnd = -1; + if ((typeof optSelEndHint) === 'number') { hintedCommonEnd = newLen - optSelEndHint; } - if (commonStart + commonEnd > oldLen) - { + if (commonStart + commonEnd > oldLen) { // ambiguous insertion var minCommonEnd = oldLen - commonStart; var maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) - { + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; - } - else - { + } else { commonEnd = minCommonEnd; } commonStart = oldLen - commonEnd; } - if (commonStart + commonEnd > newLen) - { + if (commonStart + commonEnd > newLen) { // ambiguous deletion var minCommonEnd = newLen - commonStart; var maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) - { + if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { commonEnd = hintedCommonEnd; - } - else - { + } else { commonEnd = minCommonEnd; } commonStart = newLen - commonEnd; @@ -2784,17 +2175,14 @@ function Ace2Inner(){ return [commonStart, commonEnd]; } - function equalLineAndChars(a, b) - { + function equalLineAndChars(a, b) { if (!a) return !b; if (!b) return !a; return (a[0] == b[0] && a[1] == b[1]); } - function performSelectionChange(selectStart, selectEnd, focusAtStart) - { - if (repSelectionChange(selectStart, selectEnd, focusAtStart)) - { + function performSelectionChange(selectStart, selectEnd, focusAtStart) { + if (repSelectionChange(selectStart, selectEnd, focusAtStart)) { currentCallStack.selectionAffected = true; } } @@ -2804,14 +2192,12 @@ function Ace2Inner(){ // Should not rely on the line representation. Should not affect the DOM. - function repSelectionChange(selectStart, selectEnd, focusAtStart) - { - focusAtStart = !! focusAtStart; + function repSelectionChange(selectStart, selectEnd, focusAtStart) { + focusAtStart = !!focusAtStart; - var newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); + const newSelFocusAtStart = (focusAtStart && ((!selectStart) || (!selectEnd) || (selectStart[0] != selectEnd[0]) || (selectStart[1] != selectEnd[1]))); - if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) - { + if ((!equalLineAndChars(rep.selStart, selectStart)) || (!equalLineAndChars(rep.selEnd, selectEnd)) || (rep.selFocusAtStart != newSelFocusAtStart)) { rep.selStart = selectStart; rep.selEnd = selectEnd; rep.selFocusAtStart = newSelFocusAtStart; @@ -2821,37 +2207,36 @@ function Ace2Inner(){ selectFormattingButtonIfLineHasStyleApplied(rep); hooks.callAll('aceSelectionChanged', { - rep: rep, + rep, callstack: currentCallStack, - documentAttributeManager: documentAttributeManager, + documentAttributeManager, }); // we scroll when user places the caret at the last line of the pad // when this settings is enabled - var docTextChanged = currentCallStack.docTextChanged; - if(!docTextChanged){ - var isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type); - var innerHeight = getInnerHeight(); - scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight); + const docTextChanged = currentCallStack.docTextChanged; + if (!docTextChanged) { + const isScrollableEvent = !isPadLoading(currentCallStack.type) && isScrollableEditEvent(currentCallStack.type); + const innerHeight = getInnerHeight(); + scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep, isScrollableEvent, innerHeight); } return true; // Do not uncomment this in production it will break iFrames. - //top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, - //String(!!rep.selFocusAtStart)); + // top.console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, + // String(!!rep.selFocusAtStart)); } return false; // Do not uncomment this in production it will break iFrames. - //top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); + // top.console.log("%o %o %s", rep.selStart, rep.selEnd, rep.selFocusAtStart); } - function isPadLoading(eventType) - { + function isPadLoading(eventType) { return (eventType === 'setup') || (eventType === 'setBaseText') || (eventType === 'importText'); } function updateStyleButtonState(attribName, hasStyleOnRepSelection) { - var $formattingButton = parent.parent.$('[data-key="' + attribName + '"]').find('a'); + const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection); } @@ -2859,154 +2244,81 @@ function Ace2Inner(){ return _.contains(FORMATTING_STYLES, attributeName); } - function selectFormattingButtonIfLineHasStyleApplied (rep) { - _.each(FORMATTING_STYLES, function (style) { - var hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); + function selectFormattingButtonIfLineHasStyleApplied(rep) { + _.each(FORMATTING_STYLES, (style) => { + const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); updateStyleButtonState(style, hasStyleOnRepSelection); - }) - } - - function doCreateDomLine(nonEmpty) - { - if (browser.msie && (!nonEmpty)) - { - var result = { - node: null, - appendSpan: noop, - prepareForAdd: noop, - notifyAdded: noop, - clearSpans: noop, - finishUpdate: noop, - lineMarker: 0 - }; - - var lineElem = doc.createElement("div"); - result.node = lineElem; - - result.notifyAdded = function() - { - // magic -- settng an empty div's innerHTML to the empty string - // keeps it from collapsing. Apparently innerHTML must be set *after* - // adding the node to the DOM. - // Such a div is what IE 6 creates naturally when you make a blank line - // in a document of divs. However, when copy-and-pasted the div will - // contain a space, so we note its emptiness with a property. - lineElem.innerHTML = " "; // Frist we set a value that isnt blank - // a primitive-valued property survives copy-and-paste - setAssoc(lineElem, "shouldBeEmpty", true); - // an object property doesn't - setAssoc(lineElem, "unpasted", {}); - lineElem.innerHTML = ""; // Then we make it blank.. New line and no space = Awesome :) - }; - var lineClass = 'ace-line'; - result.appendSpan = function(txt, cls) - { - if ((!txt) && cls) - { - // gain a whole-line style (currently to show insertion point in CSS) - lineClass = domline.addToLineClass(lineClass, cls); - } - // otherwise, ignore appendSpan, this is an empty line - }; - result.clearSpans = function() - { - lineClass = ''; // non-null to cause update - }; - - var writeClass = function() - { - if (lineClass !== null) lineElem.className = lineClass; - }; + }); + } - result.prepareForAdd = writeClass; - result.finishUpdate = writeClass; - result.getInnerHTML = function() - { - return ""; - }; - return result; - } - else - { - return domline.createDomLine(nonEmpty, doesWrap, browser, doc); - } + function doCreateDomLine(nonEmpty) { + return domline.createDomLine(nonEmpty, doesWrap, browser, doc); } - function textify(str) - { + function textify(str) { return str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); } - var _blockElems = { - "div": 1, - "p": 1, - "pre": 1, - "li": 1, - "ol": 1, - "ul": 1 + const _blockElems = { + div: 1, + p: 1, + pre: 1, + li: 1, + ol: 1, + ul: 1, }; - _.each(hooks.callAll('aceRegisterBlockElements'), function(element){ - _blockElems[element] = 1; + _.each(hooks.callAll('aceRegisterBlockElements'), (element) => { + _blockElems[element] = 1; }); - function isBlockElement(n) - { - return !!_blockElems[(n.tagName || "").toLowerCase()]; + function isBlockElement(n) { + return !!_blockElems[(n.tagName || '').toLowerCase()]; } - function getDirtyRanges() - { + function getDirtyRanges() { // based on observedChanges, return a list of ranges of original lines // that need to be removed or replaced with new user content to incorporate // the user's changes into the line representation. ranges may be zero-length, // indicating inserted content. for example, [0,0] means content was inserted // at the top of the document, while [3,4] means line 3 was deleted, modified, // or replaced with one or more new lines of content. ranges do not touch. - var p = PROFILER("getDirtyRanges", false); + const p = PROFILER('getDirtyRanges', false); p.forIndices = 0; p.consecutives = 0; p.corrections = 0; - var cleanNodeForIndexCache = {}; - var N = rep.lines.length(); // old number of lines + const cleanNodeForIndexCache = {}; + const N = rep.lines.length(); // old number of lines - function cleanNodeForIndex(i) - { + function cleanNodeForIndex(i) { // if line (i) in the un-updated line representation maps to a clean node // in the document, return that node. // if (i) is out of bounds, return true. else return false. - if (cleanNodeForIndexCache[i] === undefined) - { + if (cleanNodeForIndexCache[i] === undefined) { p.forIndices++; - var result; - if (i < 0 || i >= N) - { + let result; + if (i < 0 || i >= N) { result = true; // truthy, but no actual node - } - else - { - var key = rep.lines.atIndex(i).key; + } else { + const key = rep.lines.atIndex(i).key; result = (getCleanNodeByKey(key) || false); } cleanNodeForIndexCache[i] = result; } return cleanNodeForIndexCache[i]; } - var isConsecutiveCache = {}; + const isConsecutiveCache = {}; - function isConsecutive(i) - { - if (isConsecutiveCache[i] === undefined) - { + function isConsecutive(i) { + if (isConsecutiveCache[i] === undefined) { p.consecutives++; - isConsecutiveCache[i] = (function() - { + isConsecutiveCache[i] = (function () { // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, // or document boundaries, are consecutive in the changed DOM - var a = cleanNodeForIndex(i - 1); - var b = cleanNodeForIndex(i); + 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.previousSibling) return false; @@ -3018,8 +2330,7 @@ function Ace2Inner(){ return isConsecutiveCache[i]; } - function isClean(i) - { + function isClean(i) { // returns whether line (i) in the un-updated representation maps to a clean node, // or is outside the bounds of the document return !!cleanNodeForIndex(i); @@ -3027,16 +2338,14 @@ function Ace2Inner(){ // list of pairs, each representing a range of lines that is clean and consecutive // in the changed DOM. lines (-1) and (N) are always clean, but may or may not // be consecutive with lines in the document. pairs are in sorted order. - var cleanRanges = [ - [-1, N + 1] + const cleanRanges = [ + [-1, N + 1], ]; - function rangeForLine(i) - { + function rangeForLine(i) { // returns index of cleanRange containing i, or -1 if none - var answer = -1; - _.each(cleanRanges ,function(r, idx) - { + let answer = -1; + _.each(cleanRanges, (r, idx) => { if (i >= r[1]) return false; // keep looking if (i < r[0]) return true; // not found, stop looking answer = idx; @@ -3045,29 +2354,26 @@ function Ace2Inner(){ return answer; } - function removeLineFromRange(rng, line) - { + function removeLineFromRange(rng, line) { // rng is index into cleanRanges, line is line number // precond: line is in rng - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; if ((a + 1) == b) cleanRanges.splice(rng, 1); else if (line == a) cleanRanges[rng][0]++; else if (line == (b - 1)) cleanRanges[rng][1]--; else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]); } - function splitRange(rng, pt) - { + function splitRange(rng, pt) { // precond: pt splits cleanRanges[rng] into two non-empty ranges - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; cleanRanges.splice(rng, 1, [a, pt], [pt, b]); } - var correctedLines = {}; + const correctedLines = {}; - function correctlyAssignLine(line) - { + function correctlyAssignLine(line) { if (correctedLines[line]) return true; p.corrections++; correctedLines[line] = true; @@ -3075,39 +2381,32 @@ function Ace2Inner(){ // returns whether line was already correctly assigned (i.e. correctly // clean or dirty, according to cleanRanges, and if clean, correctly // attached or not attached (i.e. in the same range as) the prev and next lines). - var rng = rangeForLine(line); - var lineClean = isClean(line); - if (rng < 0) - { - if (lineClean) - { + const rng = rangeForLine(line); + const lineClean = isClean(line); + if (rng < 0) { + if (lineClean) { // somehow lost clean line } return true; } - if (!lineClean) - { + if (!lineClean) { // a clean-range includes this dirty line, fix it removeLineFromRange(rng, line); return false; - } - else - { + } else { // line is clean, but could be wrongly connected to a clean line // above or below - var a = cleanRanges[rng][0]; - var b = cleanRanges[rng][1]; - var didSomething = false; + const a = cleanRanges[rng][0]; + const b = cleanRanges[rng][1]; + let didSomething = false; // we'll leave non-clean adjacent nodes in the clean range for the caller to // detect and deal with. we deal with whether the range should be split // just above or just below this line. - if (a < line && isClean(line - 1) && !isConsecutive(line)) - { + if (a < line && isClean(line - 1) && !isConsecutive(line)) { splitRange(rng, line); didSomething = true; } - if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) - { + if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) { splitRange(rng, line + 1); didSomething = true; } @@ -3115,70 +2414,56 @@ function Ace2Inner(){ } } - function detectChangesAroundLine(line, reqInARow) - { + function detectChangesAroundLine(line, reqInARow) { // make sure cleanRanges is correct about line number "line" and the surrounding // lines; only stops checking at end of document or after no changes need // making for several consecutive lines. note that iteration is over old lines, // so this operation takes time proportional to the number of old lines // that are changed or missing, not the number of new lines inserted. - var correctInARow = 0; - var currentIndex = line; - while (correctInARow < reqInARow && currentIndex >= 0) - { - if (correctlyAssignLine(currentIndex)) - { + let correctInARow = 0; + let currentIndex = line; + while (correctInARow < reqInARow && currentIndex >= 0) { + if (correctlyAssignLine(currentIndex)) { correctInARow++; - } - else correctInARow = 0; + } else { correctInARow = 0; } currentIndex--; } correctInARow = 0; currentIndex = line; - while (correctInARow < reqInARow && currentIndex < N) - { - if (correctlyAssignLine(currentIndex)) - { + while (correctInARow < reqInARow && currentIndex < N) { + if (correctlyAssignLine(currentIndex)) { correctInARow++; - } - else correctInARow = 0; + } else { correctInARow = 0; } currentIndex++; } } - if (N === 0) - { + if (N === 0) { p.cancel(); - if (!isConsecutive(0)) - { + if (!isConsecutive(0)) { splitRange(0, 0); } - } - else - { - p.mark("topbot"); + } else { + p.mark('topbot'); detectChangesAroundLine(0, 1); detectChangesAroundLine(N - 1, 1); - p.mark("obs"); - for (var k in observedChanges.cleanNodesNearChanges) - { - var key = k.substring(1); - if (rep.lines.containsKey(key)) - { - var line = rep.lines.indexOfKey(key); + p.mark('obs'); + for (const k in observedChanges.cleanNodesNearChanges) { + const key = k.substring(1); + if (rep.lines.containsKey(key)) { + const line = rep.lines.indexOfKey(key); detectChangesAroundLine(line, 2); } } - p.mark("stats&calc"); - p.literal(p.forIndices, "byidx"); - p.literal(p.consecutives, "cons"); - p.literal(p.corrections, "corr"); + p.mark('stats&calc'); + p.literal(p.forIndices, 'byidx'); + p.literal(p.consecutives, 'cons'); + p.literal(p.corrections, 'corr'); } - var dirtyRanges = []; - for (var r = 0; r < cleanRanges.length - 1; r++) - { + const dirtyRanges = []; + for (let r = 0; r < cleanRanges.length - 1; r++) { dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]); } @@ -3187,108 +2472,81 @@ function Ace2Inner(){ return dirtyRanges; } - function markNodeClean(n) - { + function markNodeClean(n) { // clean nodes have knownHTML that matches their innerHTML - var dirtiness = {}; + const dirtiness = {}; dirtiness.nodeId = uniqueId(n); dirtiness.knownHTML = n.innerHTML; - if (browser.msie) - { - // adding a space to an "empty" div in IE designMode doesn't - // change the innerHTML of the div's parent; also, other - // browsers don't support innerText - dirtiness.knownText = n.innerText; - } - setAssoc(n, "dirtiness", dirtiness); + setAssoc(n, 'dirtiness', dirtiness); } - function isNodeDirty(n) - { - var p = PROFILER("cleanCheck", false); + function isNodeDirty(n) { + const p = PROFILER('cleanCheck', false); if (n.parentNode != root) return true; - var data = getAssoc(n, "dirtiness"); + const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; - if (browser.msie) - { - if (n.innerText !== data.knownText) return true; - } if (n.innerHTML !== data.knownHTML) return true; p.end(); return false; } - function getViewPortTopBottom() - { - var theTop = scroll.getScrollY(); - var doc = outerWin.document; - var height = doc.documentElement.clientHeight; // includes padding + function getViewPortTopBottom() { + const theTop = scroll.getScrollY(); + const doc = outerWin.document; + const height = doc.documentElement.clientHeight; // includes padding // we have to get the exactly height of the viewport. So it has to subtract all the values which changes // the viewport height (E.g. padding, position top) - var viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable(); + const viewportExtraSpacesAndPosition = getEditorPositionTop() + getPaddingTopAddedWhenPageViewIsEnable(); return { top: theTop, - bottom: (theTop + height - viewportExtraSpacesAndPosition) + bottom: (theTop + height - viewportExtraSpacesAndPosition), }; } - function getEditorPositionTop() - { - var editor = parent.document.getElementsByTagName('iframe'); - var editorPositionTop = editor[0].offsetTop; + function getEditorPositionTop() { + const editor = parent.document.getElementsByTagName('iframe'); + const editorPositionTop = editor[0].offsetTop; return editorPositionTop; } // ep_page_view adds padding-top, which makes the viewport smaller - function getPaddingTopAddedWhenPageViewIsEnable() - { - var rootDocument = parent.parent.document; - var aceOuter = rootDocument.getElementsByName("ace_outer"); - var aceOuterPaddingTop = parseInt($(aceOuter).css("padding-top")); + function getPaddingTopAddedWhenPageViewIsEnable() { + const rootDocument = parent.parent.document; + const aceOuter = rootDocument.getElementsByName('ace_outer'); + const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top')); return aceOuterPaddingTop; } - function handleCut(evt) - { - inCallStackIfNecessary("handleCut", function() - { + function handleCut(evt) { + inCallStackIfNecessary('handleCut', () => { doDeleteKey(evt); }); return true; } - function handleClick(evt) - { - inCallStackIfNecessary("handleClick", function() - { + function handleClick(evt) { + inCallStackIfNecessary('handleClick', () => { idleWorkTimer.atMost(200); }); - function isLink(n) - { - return (n.tagName || '').toLowerCase() == "a" && n.href; + function isLink(n) { + return (n.tagName || '').toLowerCase() == 'a' && n.href; } // only want to catch left-click - if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) - { + if ((!evt.ctrlKey) && (evt.button != 2) && (evt.button != 3)) { // find A tag with HREF - var n = evt.target; - while (n && n.parentNode && !isLink(n)) - { + let n = evt.target; + while (n && n.parentNode && !isLink(n)) { n = n.parentNode; } - if (n && isLink(n)) - { - try - { + if (n && isLink(n)) { + try { window.open(n.href, '_blank', 'noopener,noreferrer'); - } - catch (e) - { + } catch (e) { // absorb "user canceled" error in IE for certain prompts } evt.preventDefault(); @@ -3298,245 +2556,192 @@ function Ace2Inner(){ hideEditBarDropdowns(); } - function 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"); + function 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'); } } - function doReturnKey() - { - if (!(rep.selStart && rep.selEnd)) - { + function doReturnKey() { + if (!(rep.selStart && rep.selEnd)) { return; } - var lineNum = rep.selStart[0]; - var listType = getLineListType(lineNum); + const lineNum = rep.selStart[0]; + let listType = getLineListType(lineNum); - if (listType) - { - var text = rep.lines.atIndex(lineNum).text; + if (listType) { + const text = rep.lines.atIndex(lineNum).text; listType = /([a-z]+)([0-9]+)/.exec(listType); - var type = listType[1]; - var level = Number(listType[2]); - - //detect empty list item; exclude indentation - if(text === '*' && type !== "indent") - { - //if not already on the highest level - if(level > 1) - { - setLineListType(lineNum, type+(level-1));//automatically decrease the level - } - else - { - setLineListType(lineNum, '');//remove the list - renumberList(lineNum + 1);//trigger renumbering of list that may be right after + const type = listType[1]; + const level = Number(listType[2]); + + // detect empty list item; exclude indentation + if (text === '*' && type !== 'indent') { + // if not already on the highest level + if (level > 1) { + setLineListType(lineNum, type + (level - 1));// automatically decrease the level + } else { + setLineListType(lineNum, '');// remove the list + renumberList(lineNum + 1);// trigger renumbering of list that may be right after } - } - else if (lineNum + 1 <= rep.lines.length()) - { + } else if (lineNum + 1 <= rep.lines.length()) { performDocumentReplaceSelection('\n'); - setLineListType(lineNum + 1, type+level); + setLineListType(lineNum + 1, type + level); } - } - else - { + } else { performDocumentReplaceSelection('\n'); handleReturnIndentation(); } } - function doIndentOutdent(isOut) - { + function doIndentOutdent(isOut) { if (!((rep.selStart && rep.selEnd) || - ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) && + ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1]) && rep.selEnd[1] > 1)) && (isOut != true) - ) - { + ) { return false; } - var firstLine, lastLine; + let firstLine, lastLine; firstLine = rep.selStart[0]; lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); - var mods = []; - for (var n = firstLine; n <= lastLine; n++) - { - var listType = getLineListType(n); - var t = 'indent'; - var level = 0; - if (listType) - { + const mods = []; + for (let n = firstLine; n <= lastLine; n++) { + let listType = getLineListType(n); + let t = 'indent'; + let level = 0; + if (listType) { listType = /([a-z]+)([0-9]+)/.exec(listType); - if (listType) - { + if (listType) { t = listType[1]; level = Number(listType[2]); } } - var newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); - if (level != newLevel) - { + const newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); + if (level != newLevel) { mods.push([n, (newLevel > 0) ? t + newLevel : '']); } } - _.each(mods, function(mod){ + _.each(mods, (mod) => { setLineListType(mod[0], mod[1]); }); return true; } editorInfo.ace_doIndentOutdent = doIndentOutdent; - function doTabKey(shiftDown) - { - if (!doIndentOutdent(shiftDown)) - { + function doTabKey(shiftDown) { + if (!doIndentOutdent(shiftDown)) { performDocumentReplaceSelection(THE_TAB); } } - function doDeleteKey(optEvt) - { - var evt = optEvt || {}; - var handled = false; - if (rep.selStart) - { - if (isCaret()) - { - var lineNum = caretLine(); - var col = caretColumn(); + function doDeleteKey(optEvt) { + const evt = optEvt || {}; + let handled = false; + if (rep.selStart) { + if (isCaret()) { + const lineNum = caretLine(); + const col = caretColumn(); var lineEntry = rep.lines.atIndex(lineNum); - var lineText = lineEntry.text; - var lineMarker = lineEntry.lineMarker; - if (/^ +$/.exec(lineText.substring(lineMarker, col))) - { - var col2 = col - lineMarker; - var tabSize = THE_TAB.length; - var toDelete = ((col2 - 1) % tabSize) + 1; + const lineText = lineEntry.text; + const lineMarker = lineEntry.lineMarker; + if (/^ +$/.exec(lineText.substring(lineMarker, col))) { + const col2 = col - lineMarker; + const tabSize = THE_TAB.length; + const toDelete = ((col2 - 1) % tabSize) + 1; performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], ''); - //scrollSelectionIntoView(); + // scrollSelectionIntoView(); handled = true; } } - if (!handled) - { - if (isCaret()) - { - var theLine = caretLine(); + if (!handled) { + if (isCaret()) { + const theLine = caretLine(); var lineEntry = rep.lines.atIndex(theLine); - if (caretColumn() <= lineEntry.lineMarker) - { + if (caretColumn() <= lineEntry.lineMarker) { // delete at beginning of line - var action = 'delete_newline'; - var prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); - var thisLineListType = getLineListType(theLine); - var prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); - var prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); + const action = 'delete_newline'; + const prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); + const thisLineListType = getLineListType(theLine); + const prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); + const prevLineBlank = (prevLineEntry && prevLineEntry.text.length == prevLineEntry.lineMarker); - var thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); + const thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); - if (thisLineListType) - { + if (thisLineListType) { // this line is a list - if (prevLineBlank && !prevLineListType) - { + if (prevLineBlank && !prevLineListType) { // previous line is blank, remove it performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); - } - else - { + } else { // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); } - }else if (thisLineHasMarker && prevLineEntry){ + } else if (thisLineHasMarker && prevLineEntry) { // If the line has any attributes assigned, remove them by removing the marker '*' - performDocumentReplaceRange([theLine -1 , prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); - } - else if (theLine > 0) - { + performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); + } else if (theLine > 0) { // remove newline performDocumentReplaceRange([theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } - } - else - { - var docChar = caretDocChar(); - if (docChar > 0) - { - if (evt.metaKey || evt.ctrlKey || evt.altKey) - { + } else { + const docChar = caretDocChar(); + if (docChar > 0) { + if (evt.metaKey || evt.ctrlKey || evt.altKey) { // delete as many unicode "letters or digits" in a row as possible; // always delete one char, delete further even if that first char // isn't actually a word char. - var deleteBackTo = docChar - 1; - while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) - { + let deleteBackTo = docChar - 1; + while (deleteBackTo > lineEntry.lineMarker && isWordChar(rep.alltext.charAt(deleteBackTo - 1))) { deleteBackTo--; } performDocumentReplaceCharRange(deleteBackTo, docChar, ''); - } - else - { + } else { // normal delete performDocumentReplaceCharRange(docChar - 1, docChar, ''); } } } - } - else - { + } else { performDocumentReplaceSelection(''); } } } - //if the list has been removed, it is necessary to renumber - //starting from the *next* line because the list may have been - //separated. If it returns null, it means that the list was not cut, try - //from the current one. - var line = caretLine(); - if(line != -1 && renumberList(line+1) === null) - { + // if the list has been removed, it is necessary to renumber + // starting from the *next* line because the list may have been + // separated. If it returns null, it means that the list was not cut, try + // from the current one. + const line = caretLine(); + if (line != -1 && renumberList(line + 1) === null) { renumberList(line); } } - // set of "letter or digit" chars is based on section 20.5.16 of the original Java Language Spec - var REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; - var REGEX_SPACE = /\s/; + const REGEX_SPACE = /\s/; - function isWordChar(c) - { - return !!REGEX_WORDCHAR.exec(c); - } + const isWordChar = (c) => padutils.wordCharRegex.test(c); editorInfo.ace_isWordChar = isWordChar; - function isSpaceChar(c) - { + function isSpaceChar(c) { return !!REGEX_SPACE.exec(c); } - function moveByWordInLine(lineText, initialIndex, forwardNotBack) - { - var i = initialIndex; + function moveByWordInLine(lineText, initialIndex, forwardNotBack) { + let i = initialIndex; - function nextChar() - { + function nextChar() { if (forwardNotBack) return lineText.charAt(i); else return lineText.charAt(i - 1); } - function advance() - { + function advance() { if (forwardNotBack) i++; else i--; } - function isDone() - { + function isDone() { if (forwardNotBack) return i >= lineText.length; else return i <= 0; } @@ -3544,99 +2749,74 @@ function Ace2Inner(){ // On Mac and Linux, move right moves to end of word and move left moves to start; // on Windows, always move to start of word. // On Windows, Firefox and IE disagree on whether to stop for punctuation (FF says no). - if (browser.msie && forwardNotBack) - { - while ((!isDone()) && isWordChar(nextChar())) - { - advance(); - } - while ((!isDone()) && !isWordChar(nextChar())) - { - advance(); - } + while ((!isDone()) && !isWordChar(nextChar())) { + advance(); } - else - { - while ((!isDone()) && !isWordChar(nextChar())) - { - advance(); - } - while ((!isDone()) && isWordChar(nextChar())) - { - advance(); - } + while ((!isDone()) && isWordChar(nextChar())) { + advance(); } return i; } - function handleKeyEvent(evt) - { - // if (DEBUG && window.DONT_INCORP) return; + function handleKeyEvent(evt) { if (!isEditable) return; - var type = evt.type; - var charCode = evt.charCode; - var keyCode = evt.keyCode; - var which = evt.which; - var altKey = evt.altKey; - var shiftKey = evt.shiftKey; + const type = evt.type; + const charCode = evt.charCode; + const keyCode = evt.keyCode; + const which = evt.which; + const altKey = evt.altKey; + const shiftKey = evt.shiftKey; // Is caret potentially hidden by the chat button? - var myselection = document.getSelection(); // get the current caret selection - var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 + const myselection = document.getSelection(); // get the current caret selection + const caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 - if(myselection.focusNode.wholeText){ // Is there any content? If not lineHeight will report wrong.. + if (myselection.focusNode.wholeText) { // Is there any content? If not lineHeight will report wrong.. var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links - }else{ + } else { var lineHeight = myselection.focusNode.offsetHeight; // line height of blank lines } - //dmesg("keyevent type: "+type+", which: "+which); + // dmesg("keyevent type: "+type+", which: "+which); // 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. - var 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')) { return; // 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 } - var specialHandled = false; - var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); - var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); - var stopped = false; + 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')); + let stopped = false; - inCallStackIfNecessary("handleKeyEvent", function() - { - if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) - { + inCallStackIfNecessary('handleKeyEvent', function () { + if (type == 'keypress' || (isTypeForSpecialKey && keyCode == 13 /* return*/)) { // in IE, special keys don't send keypress, the keydown does the action - if (!outsideKeyPress(evt)) - { + if (!outsideKeyPress(evt)) { evt.preventDefault(); stopped = true; } - } - else if (evt.key === "Dead"){ + } else if (evt.key === 'Dead') { // If it's a dead key we don't want to do any Etherpad behavior. stopped = true; return true; - } - else if (type == "keydown") - { + } else if (type == 'keydown') { outsideKeyDown(evt); } - if (!stopped) - { - var specialHandledInHook = hooks.callAll('aceKeyEvent', { + if (!stopped) { + const specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager, - evt:evt + editorInfo, + rep, + documentAttributeManager, + evt, }); // if any hook returned true, set specialHandled with true @@ -3644,89 +2824,86 @@ function Ace2Inner(){ specialHandled = _.contains(specialHandledInHook, true); } - var padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9){ + const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; + if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120 && padShortcutEnabled.altF9) { // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it) - var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first(); + const firstEditbarElement = parent.parent.$('#editbar').children('ul').first().children().first().children().first().children().first(); $(this).blur(); firstEditbarElement.focus(); evt.preventDefault(); } - if ((!specialHandled) && altKey && keyCode == 67 && type === "keydown" && padShortcutEnabled.altC){ + if ((!specialHandled) && altKey && keyCode == 67 && type === 'keydown' && padShortcutEnabled.altC) { // Alt c focuses on the Chat window $(this).blur(); parent.parent.chat.show(); - parent.parent.$("#chatinput").focus(); + parent.parent.$('#chatinput').focus(); evt.preventDefault(); } - if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === "keydown" && padShortcutEnabled.cmdShift2){ + if ((!specialHandled) && evt.ctrlKey && shiftKey && keyCode == 50 && type === 'keydown' && padShortcutEnabled.cmdShift2) { // Control-Shift-2 shows a gritter popup showing a line author - var lineNumber = rep.selEnd[0]; - var alineAttrs = rep.alines[lineNumber]; - var apool = rep.apool; + const lineNumber = rep.selEnd[0]; + const alineAttrs = rep.alines[lineNumber]; + const apool = rep.apool; // TODO: support selection ranges // TODO: Still work when authorship colors have been cleared // TODO: i18n // TODO: There appears to be a race condition or so. - var author = null; + let author = null; if (alineAttrs) { var authors = []; var authorNames = []; - var opIter = Changeset.opIterator(alineAttrs); + const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()){ - var op = opIter.next(); + while (opIter.hasNext()) { + const op = opIter.next(); authorId = Changeset.opAttributeValue(op, 'author', apool); // Only push unique authors and ones with values - if(authors.indexOf(authorId) === -1 && authorId !== ""){ + if (authors.indexOf(authorId) === -1 && authorId !== '') { authors.push(authorId); } - } - } // No author information is available IE on a new pad. - if(authors.length === 0){ - var authorString = "No author information is available"; - } - else{ + if (authors.length === 0) { + var authorString = 'No author information is available'; + } else { // Known authors info, both current and historical - var padAuthors = parent.parent.pad.userList(); - var authorObj = {}; - authors.forEach(function(authorId){ - padAuthors.forEach(function(padAuthor){ + 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){ + if (padAuthor.userId === authorId) { + if (parent.parent.clientVars.userId === authorId) { authorObj = { - name: "Me" - } - }else{ + name: 'Me', + }; + } else { authorObj = padAuthor; } } }); - if(!authorObj){ - author = "Unknown"; + if (!authorObj) { + author = 'Unknown'; return; } author = authorObj.name; - if(!author) author = "Unknown"; + if (!author) author = 'Unknown'; authorNames.push(author); - }) + }); } - if(authors.length === 1){ - var authorString = "The author of this line is " + authorNames; + if (authors.length === 1) { + var authorString = `The author of this line is ${authorNames}`; } - if(authors.length > 1){ - var authorString = "The authors of this line are " + authorNames.join(" & "); + if (authors.length > 1) { + var authorString = `The authors of this line are ${authorNames.join(' & ')}`; } parent.parent.$.gritter.add({ @@ -3737,11 +2914,10 @@ function Ace2Inner(){ // (bool | optional) if you want it to fade out on its own or just sit there sticky: false, // (int | optional) the time you want it to be alive for before fading out - time: '4000' + 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. // -- @@ -3754,22 +2930,19 @@ function Ace2Inner(){ 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); evt.preventDefault(); doReturnKey(); - //scrollSelectionIntoView(); - scheduler.setTimeout(function() - { + // scrollSelectionIntoView(); + scheduler.setTimeout(() => { outerWin.scrollBy(-100, 0); }, 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. @@ -3780,229 +2953,202 @@ function Ace2Inner(){ // close all gritters when the user hits escape key parent.parent.$.gritter.removeAll(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */ + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 's' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdS) /* Do a saved revision on ctrl S */ { evt.preventDefault(); - var originalBackground = parent.parent.$('#revisionlink').css("background") - parent.parent.$('#revisionlink').css({"background":"lightyellow"}); - scheduler.setTimeout(function(){ - parent.parent.$('#revisionlink').css({"background":originalBackground}); + const originalBackground = parent.parent.$('#revisionlink').css('background'); + parent.parent.$('#revisionlink').css({background: 'lightyellow'}); + scheduler.setTimeout(() => { + parent.parent.$('#revisionlink').css({background: originalBackground}); }, 1000); - parent.parent.pad.collabClient.sendMessage({"type":"SAVE_REVISION"}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */ + parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); /* The parent.parent part of this is BAD and I feel bad.. It may break something */ specialHandled = true; } - if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) - { + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 9 && !(evt.metaKey || evt.ctrlKey) && padShortcutEnabled.tab) { // tab fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); - //scrollSelectionIntoView(); + // scrollSelectionIntoView(); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "z" && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'z' && (evt.metaKey || evt.ctrlKey) && !evt.altKey && padShortcutEnabled.cmdZ) { // cmd-Z (undo) fastIncorp(6); evt.preventDefault(); - if (evt.shiftKey) - { - doUndoRedo("redo"); - } - else - { - doUndoRedo("undo"); + if (evt.shiftKey) { + doUndoRedo('redo'); + } else { + doUndoRedo('undo'); } specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "y" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'y' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdY) { // cmd-Y (redo) fastIncorp(10); evt.preventDefault(); - doUndoRedo("redo"); + doUndoRedo('redo'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "b" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'b' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdB) { // cmd-B (bold) fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('bold'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "i" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'i' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdI) { // cmd-I (italic) fastIncorp(14); evt.preventDefault(); toggleAttributeOnSelection('italic'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "u" && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'u' && (evt.metaKey || evt.ctrlKey) && padShortcutEnabled.cmdU) { // cmd-U (underline) fastIncorp(15); evt.preventDefault(); toggleAttributeOnSelection('underline'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "5" && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == '5' && (evt.metaKey || evt.ctrlKey) && evt.altKey !== true && padShortcutEnabled.cmd5) { // cmd-5 (strikethrough) fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('strikethrough'); specialHandled = true; } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "l" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'l' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftL) { // cmd-shift-L (unorderedlist) fastIncorp(9); evt.preventDefault(); - doInsertUnorderedList() + doInsertUnorderedList(); specialHandled = true; - } - if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == "n" && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) - { + } + if ((!specialHandled) && isTypeForCmdKey && ((String.fromCharCode(which).toLowerCase() == 'n' && padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) == 1 && padShortcutEnabled.cmdShift1)) && (evt.metaKey || evt.ctrlKey) && evt.shiftKey) { // cmd-shift-N and cmd-shift-1 (orderedlist) fastIncorp(9); evt.preventDefault(); - doInsertOrderedList() + doInsertOrderedList(); specialHandled = true; - } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "c" && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) { + } + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'c' && (evt.metaKey || evt.ctrlKey) && evt.shiftKey && padShortcutEnabled.cmdShiftC) { // cmd-shift-C (clearauthorship) fastIncorp(9); evt.preventDefault(); CMDS.clearauthorship(); } - if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "h" && (evt.ctrlKey) && padShortcutEnabled.cmdH) - { + if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == 'h' && (evt.ctrlKey) && padShortcutEnabled.cmdH) { // cmd-H (backspace) fastIncorp(20); evt.preventDefault(); doDeleteKey(); specialHandled = true; } - if((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome){ scroll.setScrollY(0); } // Control Home send to Y = 0 - if((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey){ - + if ((evt.which == 36 && evt.ctrlKey == true) && padShortcutEnabled.ctrlHome) { scroll.setScrollY(0); } // Control Home send to Y = 0 + if ((evt.which == 33 || evt.which == 34) && type == 'keydown' && !evt.ctrlKey) { evt.preventDefault(); // This is required, browsers will try to do normal default behavior on page up / down and the default behavior SUCKS - var oldVisibleLineRange = scroll.getVisibleLineRange(rep); - var topOffset = rep.selStart[0] - oldVisibleLineRange[0]; - if(topOffset < 0 ){ + const oldVisibleLineRange = scroll.getVisibleLineRange(rep); + let topOffset = rep.selStart[0] - oldVisibleLineRange[0]; + if (topOffset < 0) { topOffset = 0; } - var isPageDown = evt.which === 34; - var isPageUp = evt.which === 33; + const isPageDown = evt.which === 34; + const isPageUp = evt.which === 33; - scheduler.setTimeout(function(){ - var newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10 - var linesCount = rep.lines.length(); // total count of lines in pad IE 10 - var numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now? + scheduler.setTimeout(() => { + const newVisibleLineRange = scroll.getVisibleLineRange(rep); // the visible lines IE 1,10 + const linesCount = rep.lines.length(); // total count of lines in pad IE 10 + const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; // How many lines are in the viewport right now? - if(isPageUp && padShortcutEnabled.pageUp){ + if (isPageUp && padShortcutEnabled.pageUp) { rep.selEnd[0] = rep.selEnd[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) rep.selStart[0] = rep.selStart[0] - numberOfLinesInViewport; // move to the bottom line +1 in the viewport (essentially skipping over a page) } - if(isPageDown && padShortcutEnabled.pageDown){ // if we hit page down - if(rep.selEnd[0] >= oldVisibleLineRange[0]){ // If the new viewpoint position is actually further than where we are right now - rep.selStart[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content - rep.selEnd[0] = oldVisibleLineRange[1] -1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content + if (isPageDown && padShortcutEnabled.pageDown) { // if we hit page down + if (rep.selEnd[0] >= oldVisibleLineRange[0]) { // If the new viewpoint position is actually further than where we are right now + rep.selStart[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content + rep.selEnd[0] = oldVisibleLineRange[1] - 1; // dont go further in the page down than what's visible IE go from 0 to 50 if 50 is visible on screen but dont go below that else we miss content } } - //ensure min and max - if(rep.selEnd[0] < 0){ + // ensure min and max + if (rep.selEnd[0] < 0) { rep.selEnd[0] = 0; } - if(rep.selStart[0] < 0){ + if (rep.selStart[0] < 0) { rep.selStart[0] = 0; } - if(rep.selEnd[0] >= linesCount){ - rep.selEnd[0] = linesCount-1; + if (rep.selEnd[0] >= linesCount) { + rep.selEnd[0] = linesCount - 1; } updateBrowserSelectionFromRep(); - var myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current - var caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 + const myselection = document.getSelection(); // get the current caret selection, can't use rep. here because that only gives us the start position not the current + let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 // sometimes the first selection is -1 which causes problems (Especially with ep_page_view) // so use focusNode.offsetTop value. - if(caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; + if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; scroll.setScrollY(caretOffsetTop); // set the scrollY offset of the viewport on the document - }, 200); } // scroll to viewport when user presses arrow keys and caret is out of the viewport - if((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)){ + if ((evt.which == 37 || evt.which == 38 || evt.which == 39 || evt.which == 40)) { // we use arrowKeyWasReleased to avoid triggering the animation when a key is continuously pressed // this makes the scroll smooth - if(!continuouslyPressingArrowKey(type)){ + if (!continuouslyPressingArrowKey(type)) { // We use getSelection() instead of rep to get the caret position. This avoids errors like when // the caret position is not synchronized with the rep. For example, when an user presses arrow // down to scroll the pad without releasing the key. When the key is released the rep is not // synchronized, so we don't get the right node where caret is. - var selection = getSelection(); + const selection = getSelection(); - if(selection){ - var arrowUp = evt.which === 38; - var innerHeight = getInnerHeight(); + if (selection) { + const arrowUp = evt.which === 38; + const innerHeight = getInnerHeight(); scroll.scrollWhenPressArrowKeys(arrowUp, rep, innerHeight); } } } } - if (type == "keydown") - { + if (type == 'keydown') { idleWorkTimer.atLeast(500); - } - else if (type == "keypress") - { - if ((!specialHandled) && false /*parenModule.shouldNormalizeOnChar(charCode)*/) - { + } else if (type == 'keypress') { + if ((!specialHandled) && false /* parenModule.shouldNormalizeOnChar(charCode)*/) { idleWorkTimer.atMost(0); - } - else - { + } else { idleWorkTimer.atLeast(500); } - } - else if (type == "keyup") - { - var wait = 0; + } else if (type == 'keyup') { + const wait = 0; idleWorkTimer.atLeast(wait); idleWorkTimer.atMost(wait); } // Is part of multi-keystroke international character on Firefox Mac - var isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); + const isFirefoxHalfCharacter = (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); // Is part of multi-keystroke international character on Safari Mac - var isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); + const isSafariHalfCharacter = (browser.safari && evt.altKey && keyCode == 229); - if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) - { + if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) { idleWorkTimer.atLeast(3000); // give user time to type // if this is a keydown, e.g., the keyup shouldn't trigger a normalize thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) - { - if (type != "keyup") - { + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { + if (type != 'keyup') { observeChangesAroundSelection(); } } - if (type == "keyup") - { + if (type == 'keyup') { thisKeyDoesntTriggerNormalize = false; } }); @@ -4010,12 +3156,11 @@ function Ace2Inner(){ var thisKeyDoesntTriggerNormalize = false; - var arrowKeyWasReleased = true; + let arrowKeyWasReleased = true; function continuouslyPressingArrowKey(type) { - var firstTimeKeyIsContinuouslyPressed = false; + let firstTimeKeyIsContinuouslyPressed = false; - if (type == 'keyup') arrowKeyWasReleased = true; - else if (type == 'keydown' && arrowKeyWasReleased) { + if (type == 'keyup') { arrowKeyWasReleased = true; } else if (type == 'keydown' && arrowKeyWasReleased) { firstTimeKeyIsContinuouslyPressed = true; arrowKeyWasReleased = false; } @@ -4023,29 +3168,23 @@ function Ace2Inner(){ return !firstTimeKeyIsContinuouslyPressed; } - function doUndoRedo(which) - { + function doUndoRedo(which) { // precond: normalized DOM - if (undoModule.enabled) - { - var whichMethod; - if (which == "undo") whichMethod = 'performUndo'; - if (which == "redo") whichMethod = 'performRedo'; - if (whichMethod) - { - var oldEventType = currentCallStack.editEvent.eventType; + if (undoModule.enabled) { + let whichMethod; + if (which == 'undo') whichMethod = 'performUndo'; + if (which == 'redo') whichMethod = 'performRedo'; + if (whichMethod) { + const oldEventType = currentCallStack.editEvent.eventType; currentCallStack.startNewEvent(which); - undoModule[whichMethod](function(backset, selectionInfo) - { - if (backset) - { + undoModule[whichMethod]((backset, selectionInfo) => { + if (backset) { performDocumentApplyChangeset(backset); } - if (selectionInfo) - { + if (selectionInfo) { performSelectionChange(lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), selectionInfo.selFocusAtStart); } - var oldEvent = currentCallStack.startNewEvent(oldEventType, true); + const oldEvent = currentCallStack.startNewEvent(oldEventType, true); return oldEvent; }); } @@ -4053,764 +3192,301 @@ function Ace2Inner(){ } editorInfo.ace_doUndoRedo = doUndoRedo; - function updateBrowserSelectionFromRep() - { + function updateBrowserSelectionFromRep() { // requires normalized DOM! - var selStart = rep.selStart, - selEnd = rep.selEnd; + const selStart = rep.selStart; + const selEnd = rep.selEnd; - if (!(selStart && selEnd)) - { + if (!(selStart && selEnd)) { setSelection(null); return; } - var selection = {}; + const selection = {}; - var ss = [selStart[0], selStart[1]]; + const ss = [selStart[0], selStart[1]]; selection.startPoint = getPointForLineAndChar(ss); - var se = [selEnd[0], selEnd[1]]; + const se = [selEnd[0], selEnd[1]]; selection.endPoint = getPointForLineAndChar(se); - selection.focusAtStart = !! rep.selFocusAtStart; + selection.focusAtStart = !!rep.selFocusAtStart; setSelection(selection); } editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep; - function nodeMaxIndex(nd) - { + function nodeMaxIndex(nd) { if (isNodeText(nd)) return nd.nodeValue.length; else return 1; } - function hasIESelection() - { - var browserSelection; - try - { - browserSelection = doc.selection; - } - catch (e) - {} - if (!browserSelection) return false; - var origSelectionRange; - try - { - origSelectionRange = browserSelection.createRange(); - } - catch (e) - {} - if (!origSelectionRange) return false; - return true; - } - - function getSelection() - { + function getSelection() { // returns null, or a structure containing startPoint and endPoint, // each of which has node (a magicdom node), index, and maxIndex. If the node // is a text node, maxIndex is the length of the text; else maxIndex is 1. // index is between 0 and maxIndex, inclusive. - if (browser.msie) - { - var browserSelection; - try - { - browserSelection = doc.selection; - } - catch (e) - {} - if (!browserSelection) return null; - var origSelectionRange; - try - { - origSelectionRange = browserSelection.createRange(); - } - catch (e) - {} - if (!origSelectionRange) return null; - var selectionParent = origSelectionRange.parentElement(); - if (selectionParent.ownerDocument != doc) return null; - - var newRange = function() - { - return doc.body.createTextRange(); - }; - - var rangeForElementNode = function(nd) - { - var rng = newRange(); - // doesn't work on text nodes - rng.moveToElementText(nd); - return rng; - }; - - var pointFromCollapsedRange = function(rng) - { - var parNode = rng.parentElement(); - var elemBelow = -1; - var elemAbove = parNode.childNodes.length; - var rangeWithin = rangeForElementNode(parNode); + var browserSelection = window.getSelection(); + if (!browserSelection || browserSelection.type === 'None' || + browserSelection.rangeCount === 0) { + return null; + } + const range = browserSelection.getRangeAt(0); - if (rng.compareEndPoints("StartToStart", rangeWithin) === 0) - { - return { - node: parNode, - index: 0, - maxIndex: 1 - }; - } - else if (rng.compareEndPoints("EndToEnd", rangeWithin) === 0) - { - if (isBlockElement(parNode) && parNode.nextSibling) - { - // caret after block is not consistent across browsers - // (same line vs next) so put caret before next node - return { - node: parNode.nextSibling, - index: 0, - maxIndex: 1 - }; - } - return { - node: parNode, - index: 1, - maxIndex: 1 - }; - } - else if (parNode.childNodes.length === 0) - { - return { - node: parNode, - index: 0, - maxIndex: 1 - }; - } + function isInBody(n) { + while (n && !(n.tagName && n.tagName.toLowerCase() == 'body')) { + n = n.parentNode; + } + return !!n; + } - for (var i = 0; i < parNode.childNodes.length; i++) - { - var n = parNode.childNodes.item(i); - if (!isNodeText(n)) - { - var nodeRange = rangeForElementNode(n); - var startComp = rng.compareEndPoints("StartToStart", nodeRange); - var endComp = rng.compareEndPoints("EndToEnd", nodeRange); - if (startComp >= 0 && endComp <= 0) - { - var index = 0; - if (startComp > 0) - { - index = 1; - } - return { - node: n, - index: index, - maxIndex: 1 - }; - } - else if (endComp > 0) - { - if (i > elemBelow) - { - elemBelow = i; - rangeWithin.setEndPoint("StartToEnd", nodeRange); - } - } - else if (startComp < 0) - { - if (i < elemAbove) - { - elemAbove = i; - rangeWithin.setEndPoint("EndToStart", nodeRange); - } - } - } - } - if ((elemAbove - elemBelow) == 1) - { - if (elemBelow >= 0) - { - return { - node: parNode.childNodes.item(elemBelow), - index: 1, - maxIndex: 1 - }; - } - else - { - return { - node: parNode.childNodes.item(elemAbove), - index: 0, - maxIndex: 1 - }; - } - } - var idx = 0; - var r = rng.duplicate(); - // infinite stateful binary search! call function for values 0 to inf, - // expecting the answer to be about 40. return index of smallest - // true value. - var indexIntoRange = binarySearchInfinite(40, function(i) - { - // the search algorithm whips the caret back and forth, - // though it has to be moved relatively and may hit - // the end of the buffer - var delta = i - idx; - var moved = Math.abs(r.move("character", -delta)); - // next line is work-around for fact that when moving left, the beginning - // of a text node is considered to be after the start of the parent element: - if (r.move("character", -1)) r.move("character", 1); - if (delta < 0) idx -= moved; - else idx += moved; - return (r.compareEndPoints("StartToStart", rangeWithin) <= 0); - }); - // iterate over consecutive text nodes, point is in one of them - var textNode = elemBelow + 1; - var indexLeft = indexIntoRange; - while (textNode < elemAbove) - { - var tn = parNode.childNodes.item(textNode); - if (indexLeft <= tn.nodeValue.length) - { - return { - node: tn, - index: indexLeft, - maxIndex: tn.nodeValue.length - }; - } - indexLeft -= tn.nodeValue.length; - textNode++; - } - var tn = parNode.childNodes.item(textNode - 1); + function pointFromRangeBound(container, offset) { + if (!isInBody(container)) { + // command-click in Firefox selects whole document, HEAD and BODY! return { - node: tn, - index: tn.nodeValue.length, - maxIndex: tn.nodeValue.length + node: root, + index: 0, + maxIndex: 1, }; - }; - - var selection = {}; - if (origSelectionRange.compareEndPoints("StartToEnd", origSelectionRange) === 0) - { - // collapsed - var pnt = pointFromCollapsedRange(origSelectionRange); - selection.startPoint = pnt; - selection.endPoint = { - node: pnt.node, - index: pnt.index, - maxIndex: pnt.maxIndex + } + const n = container; + const childCount = n.childNodes.length; + if (isNodeText(n)) { + return { + node: n, + index: offset, + maxIndex: n.nodeValue.length, + }; + } else if (childCount === 0) { + return { + node: n, + index: 0, + maxIndex: 1, }; } - else - { - var start = origSelectionRange.duplicate(); - start.collapse(true); - var end = origSelectionRange.duplicate(); - end.collapse(false); - selection.startPoint = pointFromCollapsedRange(start); - selection.endPoint = pointFromCollapsedRange(end); + // treat point between two nodes as BEFORE the second (rather than after the first) + // if possible; this way point at end of a line block-element is treated as + // at beginning of next line + else if (offset == childCount) { + var nd = n.childNodes.item(childCount - 1); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: max, + maxIndex: max, + }; + } else { + var nd = n.childNodes.item(offset); + var max = nodeMaxIndex(nd); + return { + node: nd, + index: 0, + maxIndex: max, + }; } - return selection; } - else - { - // non-IE browser - var browserSelection = window.getSelection(); - if (browserSelection && browserSelection.type != "None" && browserSelection.rangeCount !== 0) - { - var range = browserSelection.getRangeAt(0); - - function isInBody(n) - { - while (n && !(n.tagName && n.tagName.toLowerCase() == "body")) - { - n = n.parentNode; - } - return !!n; - } - - function pointFromRangeBound(container, offset) - { - if (!isInBody(container)) - { - // command-click in Firefox selects whole document, HEAD and BODY! - return { - node: root, - index: 0, - maxIndex: 1 - }; - } - var n = container; - var childCount = n.childNodes.length; - if (isNodeText(n)) - { - return { - node: n, - index: offset, - maxIndex: n.nodeValue.length - }; - } - else if (childCount === 0) - { - return { - node: n, - index: 0, - maxIndex: 1 - }; - } - // treat point between two nodes as BEFORE the second (rather than after the first) - // if possible; this way point at end of a line block-element is treated as - // at beginning of next line - else if (offset == childCount) - { - var nd = n.childNodes.item(childCount - 1); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: max, - maxIndex: max - }; - } - else - { - var nd = n.childNodes.item(offset); - var max = nodeMaxIndex(nd); - return { - node: nd, - index: 0, - maxIndex: max - }; - } - } - var selection = {}; - selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); - selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); - selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); - - if(selection.startPoint.node.ownerDocument !== window.document){ - return null; - } + var selection = {}; + selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); + selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); + selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); - return selection; - } - else return null; + if (selection.startPoint.node.ownerDocument !== window.document) { + return null; } + + return selection; } - function setSelection(selection) - { - function copyPoint(pt) - { + function setSelection(selection) { + function copyPoint(pt) { return { node: pt.node, index: pt.index, - maxIndex: pt.maxIndex + maxIndex: pt.maxIndex, }; } - if (browser.msie) - { - // Oddly enough, accessing scrollHeight fixes return key handling on IE 8, - // presumably by forcing some kind of internal DOM update. - doc.body.scrollHeight; - - function moveToElementText(s, n) - { - while (n.firstChild && !isNodeText(n.firstChild)) - { - n = n.firstChild; - } - s.moveToElementText(n); - } - - function newRange() - { - return doc.body.createTextRange(); - } - - function setCollapsedBefore(s, n) - { - // s is an IE TextRange, n is a dom node - if (isNodeText(n)) - { - // previous node should not also be text, but prevent inf recurs - if (n.previousSibling && !isNodeText(n.previousSibling)) - { - setCollapsedAfter(s, n.previousSibling); - } - else - { - setCollapsedBefore(s, n.parentNode); + let isCollapsed; + + function pointToRangeBound(pt) { + const p = copyPoint(pt); + // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, + // and also problem where cut/copy of a whole line selected with fake arrow-keys + // copies the next line too. + if (isCollapsed) { + function diveDeep() { + while (p.node.childNodes.length > 0) { + // && (p.node == root || p.node.parentNode == root)) { + if (p.index === 0) { + p.node = p.node.firstChild; + p.maxIndex = nodeMaxIndex(p.node); + } else if (p.index == p.maxIndex) { + p.node = p.node.lastChild; + p.maxIndex = nodeMaxIndex(p.node); + p.index = p.maxIndex; + } else { break; } } } - else - { - moveToElementText(s, n); - // work around for issue that caret at beginning of line - // somehow ends up at end of previous line - if (s.move('character', 1)) - { - s.move('character', -1); + // now fix problem where cursor at end of text node at end of span-like element + // with background doesn't seem to show up... + if (isNodeText(p.node) && p.index == p.maxIndex) { + let n = p.node; + while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) { + n = n.parentNode; } - s.collapse(true); // to start - } - } - - function setCollapsedAfter(s, n) - { - // s is an IE TextRange, n is a magicdom node - if (isNodeText(n)) - { - // can't use end of container when no nextSibling (could be on next line), - // so use previousSibling or start of container and move forward. - setCollapsedBefore(s, n); - s.move("character", n.nodeValue.length); - } - else - { - moveToElementText(s, n); - s.collapse(false); // to end - } - } - - function getPointRange(point) - { - var s = newRange(); - var n = point.node; - if (isNodeText(n)) - { - setCollapsedBefore(s, n); - s.move("character", point.index); - } - else if (point.index === 0) - { - setCollapsedBefore(s, n); - } - else - { - setCollapsedAfter(s, n); - } - return s; - } - - if (selection) - { - if (!hasIESelection()) - { - return; // don't steal focus - } - - var startPoint = copyPoint(selection.startPoint); - var endPoint = copyPoint(selection.endPoint); - - // fix issue where selection can't be extended past end of line - // with shift-rightarrow or shift-downarrow - if (endPoint.index == endPoint.maxIndex && endPoint.node.nextSibling) - { - endPoint.node = endPoint.node.nextSibling; - endPoint.index = 0; - endPoint.maxIndex = nodeMaxIndex(endPoint.node); - } - var range = getPointRange(startPoint); - range.setEndPoint("EndToEnd", getPointRange(endPoint)); - - // setting the selection in IE causes everything to scroll - // so that the selection is visible. if setting the selection - // definitely accomplishes nothing, don't do it. - - - function isEqualToDocumentSelection(rng) - { - var browserSelection; - try - { - browserSelection = doc.selection; + if (n.nextSibling && (!((typeof n.nextSibling.tagName) === 'string' && n.nextSibling.tagName.toLowerCase() == 'br')) && (n != p.node) && (n != root) && (n.parentNode != root)) { + // found a parent, go to next node and dive in + p.node = n.nextSibling; + p.maxIndex = nodeMaxIndex(p.node); + p.index = 0; + diveDeep(); } - catch (e) - {} - if (!browserSelection) return false; - var rng2 = browserSelection.createRange(); - if (rng2.parentElement().ownerDocument != doc) return false; - if (rng.compareEndPoints("StartToStart", rng2) !== 0) return false; - if (rng.compareEndPoints("EndToEnd", rng2) !== 0) return false; - return true; } - if (!isEqualToDocumentSelection(range)) - { - //dmesg(toSource(selection)); - //dmesg(escapeHTML(doc.body.innerHTML)); - range.select(); + // try to make sure insertion point is styled; + // also fixes other FF problems + if (!isNodeText(p.node)) { + diveDeep(); } } - else - { - try - { - doc.selection.empty(); - } - catch (e) - {} + if (isNodeText(p.node)) { + return { + container: p.node, + offset: p.index, + }; + } else { + // p.index in {0,1} + return { + container: p.node.parentNode, + offset: childIndex(p.node) + p.index, + }; } } - else - { - // non-IE browser - var isCollapsed; - - function pointToRangeBound(pt) - { - var p = copyPoint(pt); - // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, - // and also problem where cut/copy of a whole line selected with fake arrow-keys - // copies the next line too. - if (isCollapsed) - { - function diveDeep() - { - while (p.node.childNodes.length > 0) - { - //&& (p.node == root || p.node.parentNode == root)) { - if (p.index === 0) - { - p.node = p.node.firstChild; - p.maxIndex = nodeMaxIndex(p.node); - } - else if (p.index == p.maxIndex) - { - p.node = p.node.lastChild; - p.maxIndex = nodeMaxIndex(p.node); - p.index = p.maxIndex; - } - else break; - } - } - // now fix problem where cursor at end of text node at end of span-like element - // with background doesn't seem to show up... - if (isNodeText(p.node) && p.index == p.maxIndex) - { - var n = p.node; - while ((!n.nextSibling) && (n != root) && (n.parentNode != root)) - { - n = n.parentNode; - } - if (n.nextSibling && (!((typeof n.nextSibling.tagName) == "string" && n.nextSibling.tagName.toLowerCase() == "br")) && (n != p.node) && (n != root) && (n.parentNode != root)) - { - // found a parent, go to next node and dive in - p.node = n.nextSibling; - p.maxIndex = nodeMaxIndex(p.node); - p.index = 0; - diveDeep(); - } - } - // try to make sure insertion point is styled; - // also fixes other FF problems - if (!isNodeText(p.node)) - { - diveDeep(); - } - } - if (isNodeText(p.node)) - { - return { - container: p.node, - offset: p.index - }; - } - else - { - // p.index in {0,1} - return { - container: p.node.parentNode, - offset: childIndex(p.node) + p.index - }; - } - } - var browserSelection = window.getSelection(); - if (browserSelection) - { - browserSelection.removeAllRanges(); - if (selection) - { - isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); - var start = pointToRangeBound(selection.startPoint); - var end = pointToRangeBound(selection.endPoint); + const browserSelection = window.getSelection(); + if (browserSelection) { + browserSelection.removeAllRanges(); + if (selection) { + isCollapsed = (selection.startPoint.node === selection.endPoint.node && selection.startPoint.index === selection.endPoint.index); + const start = pointToRangeBound(selection.startPoint); + const end = pointToRangeBound(selection.endPoint); - if ((!isCollapsed) && selection.focusAtStart && browserSelection.collapse && browserSelection.extend) - { - // can handle "backwards"-oriented selection, shift-arrow-keys move start - // of selection - browserSelection.collapse(end.container, end.offset); - browserSelection.extend(start.container, start.offset); - } - else - { - var range = doc.createRange(); - range.setStart(start.container, start.offset); - range.setEnd(end.container, end.offset); - browserSelection.removeAllRanges(); - browserSelection.addRange(range); - } + 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 { + var range = doc.createRange(); + range.setStart(start.container, start.offset); + range.setEnd(end.container, end.offset); + browserSelection.removeAllRanges(); + browserSelection.addRange(range); } } } } - function childIndex(n) - { - var idx = 0; - while (n.previousSibling) - { + function childIndex(n) { + let idx = 0; + while (n.previousSibling) { idx++; n = n.previousSibling; } return idx; } - function fixView() - { + function fixView() { // calling this method repeatedly should be fast - if (getInnerWidth() === 0 || getInnerHeight() === 0) - { + if (getInnerWidth() === 0 || getInnerHeight() === 0) { return; } - var win = outerWin; + const win = outerWin; enforceEditability(); $(sideDiv).addClass('sidedivdelayed'); } - var _teardownActions = []; + const _teardownActions = []; - function teardown() - { - _.each(_teardownActions, function(a) - { + function teardown() { + _.each(_teardownActions, (a) => { a(); }); } - function setDesignMode(newVal) - { - try - { - function setIfNecessary(target, prop, val) - { - if (String(target[prop]).toLowerCase() != val) - { - target[prop] = val; - return true; - } - return false; - } - if (browser.msie || browser.safari) - { - setIfNecessary(root, 'contentEditable', (newVal ? 'true' : 'false')); - } - else - { - var wasSet = setIfNecessary(doc, 'designMode', (newVal ? 'on' : 'off')); - if (wasSet && newVal && browser.opera) - { - // turning on designMode clears event handlers - bindTheEventHandlers(); - } - } - return true; - } - catch (e) - { - return false; - } - } - - var iePastedLines = null; - - function handleIEPaste(evt) - { - // Pasting in IE loses blank lines in a way that loses information; - // "one\n\ntwo\nthree" becomes "

        one

        two

        three

        ", - // which becomes "one\ntwo\nthree". We can get the correct text - // from the clipboard directly, but we still have to let the paste - // happen to get the style information. - var clipText = window.clipboardData && window.clipboardData.getData("Text"); - if (clipText && doc.selection) - { - // this "paste" event seems to mess with the selection whether we try to - // stop it or not, so can't really do document-level manipulation now - // or in an idle call-stack. instead, use IE native manipulation - //function escapeLine(txt) { - //return processSpaces(escapeHTML(textify(txt))); - //} - //var newHTML = map(clipText.replace(/\r/g,'').split('\n'), escapeLine).join('
        '); - //doc.selection.createRange().pasteHTML(newHTML); - //evt.preventDefault(); - //iePastedLines = map(clipText.replace(/\r/g,'').split('\n'), textify); - } - } - - var inInternationalComposition = false; - function handleCompositionEvent(evt) - { + function handleCompositionEvent(evt) { // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == "compositionstart") - { + if (evt.type == 'compositionstart') { inInternationalComposition = true; - } - else if (evt.type == "compositionend") - { + } else if (evt.type == 'compositionend') { inInternationalComposition = false; } } - editorInfo.ace_getInInternationalComposition = function () - { + editorInfo.ace_getInInternationalComposition = function () { return inInternationalComposition; - } + }; - function bindTheEventHandlers() - { - $(document).on("keydown", handleKeyEvent); - $(document).on("keypress", handleKeyEvent); - $(document).on("keyup", handleKeyEvent); - $(document).on("click", handleClick); + function bindTheEventHandlers() { + $(document).on('keydown', handleKeyEvent); + $(document).on('keypress', handleKeyEvent); + $(document).on('keyup', handleKeyEvent); + $(document).on('click', handleClick); // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer - $(outerWin.document).on("click", hideEditBarDropdowns); + $(outerWin.document).on('click', hideEditBarDropdowns); // Disabled: https://github.com/ether/etherpad-lite/issues/2546 // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533 // $(document).on("cut", handleCut); - $(root).on("blur", handleBlur); - if (browser.msie) - { - $(document).on("click", handleIEOuterClick); - } - if (browser.msie) $(root).on("paste", handleIEPaste); + $(root).on('blur', handleBlur); + + // If non-nullish, pasting on a link should be suppressed. + let suppressPasteOnLink = null; + + $(root).on('auxclick', (e) => { + if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) { + // The user middle-clicked on a link. Usually users do this to open a link in a new tab, but + // in X11 (Linux) this will instead paste the contents of the primary selection at the mouse + // cursor. Users almost certainly do not want to paste when middle-clicking on a link, so + // tell the 'paste' event handler to suppress the paste. This is done by starting a + // short-lived timer that suppresses paste (when the target is a link) until either the + // paste event arrives or the timer fires. + // + // Why it is implemented this way: + // * Users want to be able to paste on a link via Ctrl-V, the Edit menu, or the context + // menu (https://github.com/ether/etherpad-lite/issues/2775) so we cannot simply + // suppress all paste actions when the target is a link. + // * Non-X11 systems do not paste when the user middle-clicks, so the paste suppression + // must be self-resetting. + // * On non-X11 systems, middle click should continue to open the link in a new tab. + // Suppressing the middle click here in the 'auxclick' handler (via e.preventDefault()) + // would break that behavior. + suppressPasteOnLink = scheduler.setTimeout(() => { suppressPasteOnLink = null; }, 0); + } + }); - // Don't paste on middle click of links - $(root).on("paste", function(e){ - // TODO: this breaks pasting strings into URLS when using - // Control C and Control V -- the Event is never available - // here.. :( - if(e.target.a || e.target.localName === "a"){ + $(root).on('paste', (e) => { + if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { + scheduler.clearTimeout(suppressPasteOnLink); + suppressPasteOnLink = null; e.preventDefault(); + return; } // Call paste hook hooks.callAll('acePaste', { - editorInfo: editorInfo, - rep: rep, - documentAttributeManager: documentAttributeManager, - e: e + editorInfo, + rep, + documentAttributeManager, + e, }); - }) + }); // We reference document here, this is because if we don't this will expose a bug // in Google Chrome. This bug will cause the last character on the last line to // not fire an event when dropped into.. - $(document).on("drop", function(e){ - if(e.target.a || e.target.localName === "a"){ + $(document).on('drop', (e) => { + if (e.target.a || e.target.localName === 'a') { e.preventDefault(); } @@ -4818,172 +3494,110 @@ function Ace2Inner(){ // need to merge the changes into a single changeset. So mark origin with diff --git a/src/templates/index.html b/src/templates/index.html index 5c1aa1d3181..213071fef19 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -5,30 +5,6 @@ <%=settings.title%> - - @@ -37,6 +13,8 @@ + + empty
        ', + expectedHTML: 'empty

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

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

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

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

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

        ', + expectedText: ' word1 word2 word3\n\n' + }, + nonBreakingSpacePreceededBySpaceBetweenWords: { + description: 'A non-breaking space preceeded by a normal space', + input: '  word1  word2  word3
        ', + expectedHTML: ' word1  word2  word3

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

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

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

        something
        ', + expectedHTML: 'something

        something

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

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

        something
        ', + expectedHTML: 'something

        something

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

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

           something
        ', + expectedHTML: 'something

           something

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

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

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

        first line

        second line
        ', + expectedHTML: '



        first line

        second line

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

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

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

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

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

        multiple
        +   lines
        + in
        +      pre
        +

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

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

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

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

        + 1 +

        preline
        +

        `, + expectedHTML: '1
        preline


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

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

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

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

        ', + expectedText: 'Need more space s !\n\n' + }, +}; -Object.keys(testImports).forEach(function (testName) { - var testPadId = makeid(); - test = testImports[testName]; - describe('createPad', function(){ - it('creates a new Pad', function(done) { - api.get(endPoint('createPad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create new Pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) +describe(__filename, function () { + Object.keys(testImports).forEach((testName) => { + const testPadId = makeid(); + const test = testImports[testName]; + if (test.disabled) { + return xit(`DISABLED: ${testName}`, function (done) { + done(); + }); + } + describe(`createPad ${testName}`, function () { + it('creates a new Pad', function (done) { + api.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('setHTML', function(){ - it('Sets the HTML', function(done) { - api.get(endPoint('setHTML')+"&padID="+testPadId+"&html="+test.input) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Error:"+testName) - }) - .expect('Content-Type', /json/) - .expect(200, done) + + describe(`setHTML ${testName}`, function () { + it('Sets the HTML', function (done) { + api.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${encodeURIComponent(test.input)}`) + .expect((res) => { + if (res.body.code !== 0) throw new Error(`Error:${testName}`); + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); }); - }) - - describe('getHTML', function(){ - it('Gets back the HTML of a Pad', function(done) { - api.get(endPoint('getHTML')+"&padID="+testPadId) - .expect(function(res){ - var receivedHtml = res.body.data.html; - if (receivedHtml !== test.expectedHTML) { - throw new Error(`HTML received from export is not the one we were expecting. + + describe(`getHTML ${testName}`, function () { + it('Gets back the HTML of a Pad', function (done) { + api.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect((res) => { + const receivedHtml = res.body.data.html; + if (receivedHtml !== test.expectedHTML) { + throw new Error(`HTML received from export is not the one we were expecting. Test Name: ${testName} Received: - ${receivedHtml} + ${JSON.stringify(receivedHtml)} Expected: - ${test.expectedHTML} + ${JSON.stringify(test.expectedHTML)} Which is a different version of the originally imported one: ${test.input}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done) + } + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); }); - }) - - describe('getText', function(){ - it('Gets back the Text of a Pad', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - var receivedText = res.body.data.text; - if (receivedText !== test.expectedText) { - throw new Error(`Text received from export is not the one we were expecting. + + describe(`getText ${testName}`, function () { + it('Gets back the Text of a Pad', function (done) { + api.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect((res) => { + const receivedText = res.body.data.text; + if (receivedText !== test.expectedText) { + throw new Error(`Text received from export is not the one we were expecting. Test Name: ${testName} Received: - ${receivedText} + ${JSON.stringify(receivedText)} Expected: - ${test.expectedText} + ${JSON.stringify(test.expectedText)} Which is a different version of the originally imported one: ${test.input}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done) + } + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); }); - }) + }); }); -var endPoint = function(point, version){ +function endPoint(point, version) { version = version || apiVersion; - return '/api/'+version+'/'+point+'?apikey='+apiKey; -} + return `/api/${version}/${point}?apikey=${apiKey}`; +}; -function makeid() -{ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function makeid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 5; i++ ){ + for (let i = 0; i < 5; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } - -function generateLongText(){ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for( var i=0; i < 80000; i++ ){ - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -// Need this to compare arrays (listSavedRevisions test) -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} diff --git a/tests/backend/specs/api/importexportGetPost.js b/tests/backend/specs/api/importexportGetPost.js index f678f7de75b..eec5fb9996c 100644 --- a/tests/backend/specs/api/importexportGetPost.js +++ b/tests/backend/specs/api/importexportGetPost.js @@ -1,410 +1,368 @@ +/* global __dirname, __filename, afterEach, before, beforeEach, describe, it, require */ + /* * Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints. */ -const assert = require('assert'); -const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); +const assert = require('assert').strict; +const common = require('../../common'); +const superagent = require(`${__dirname}/../../../../src/node_modules/superagent`); const fs = require('fs'); -const settings = require(__dirname+'/../../../../src/node/utils/Settings'); -const host = 'http://127.0.0.1:'+settings.port; -const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); -const request = require(__dirname+'/../../../../src/node_modules/request'); -const padText = fs.readFileSync("../tests/backend/specs/api/test.txt"); -const etherpadDoc = fs.readFileSync("../tests/backend/specs/api/test.etherpad"); -const wordDoc = fs.readFileSync("../tests/backend/specs/api/test.doc"); -const wordXDoc = fs.readFileSync("../tests/backend/specs/api/test.docx"); -const odtDoc = fs.readFileSync("../tests/backend/specs/api/test.odt"); -const pdfDoc = fs.readFileSync("../tests/backend/specs/api/test.pdf"); -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); -var apiVersion = 1; -var testPadId = makeid(); -var lastEdited = ""; -var text = generateLongText(); - -describe('Connectivity', function(){ - it('can connect', function(done) { - api.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done) +const settings = require(`${__dirname}/../../../../src/node/utils/Settings`); +const padManager = require(`${__dirname}/../../../../src/node/db/PadManager`); +const plugins = require(`${__dirname}/../../../../src/static/js/pluginfw/plugin_defs`); + +const padText = fs.readFileSync('../tests/backend/specs/api/test.txt'); +const etherpadDoc = fs.readFileSync('../tests/backend/specs/api/test.etherpad'); +const wordDoc = fs.readFileSync('../tests/backend/specs/api/test.doc'); +const wordXDoc = fs.readFileSync('../tests/backend/specs/api/test.docx'); +const odtDoc = fs.readFileSync('../tests/backend/specs/api/test.odt'); +const pdfDoc = fs.readFileSync('../tests/backend/specs/api/test.pdf'); + +let agent; +const apiKey = common.apiKey; +const apiVersion = 1; +const testPadId = makeid(); +const testPadIdEnc = encodeURIComponent(testPadId); + +describe(__filename, function () { + before(async function () { agent = await common.init(); }); + + describe('Connectivity', 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) { - api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) + + describe('API Versioning', function () { + it('finds the version tag', async function () { + await agent.get('/api/') + .expect(200) + .expect((res) => assert(res.body.currentVersion)); + }); }); -}) -/* -Tests ------ - -Test. - / Create a pad - / Set pad contents - / Try export pad in various formats - / Get pad contents and ensure it matches imported contents - -Test. - / Try to export a pad that doesn't exist // Expect failure - -Test. - / Try to import an unsupported file to a pad that exists - --- TODO: Test. - Try to import to a file and abort it half way through - -Test. - Try to import to files of varying size. - -Example Curl command for testing import URI: - curl -s -v --form file=@/home/jose/test.txt http://127.0.0.1:9001/p/foo/import -*/ - -describe('Imports and Exports', function(){ - it('creates a new Pad, imports content to it, checks that content', function(done) { - if(!settings.allowAnyoneToImport){ - console.warn("not anyone can import so not testing -- to include this test set allowAnyoneToImport to true in settings.json"); - done(); - }else{ - api.get(endPoint('createPad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create new Pad"); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== padText.toString()){ - throw new Error("text is wrong on export"); - } - }) - } - }); + /* + Tests + ----- - let form = req.form(); + Test. + / Create a pad + / Set pad contents + / Try export pad in various formats + / Get pad contents and ensure it matches imported contents - form.append('file', padText, { - filename: '/test.txt', - contentType: 'text/plain' - }); + Test. + / Try to export a pad that doesn't exist // Expect failure - }) - .expect('Content-Type', /json/) - .expect(200, done) - } - }); + Test. + / Try to import an unsupported file to a pad that exists - // For some reason word import does not work in testing.. - // TODO: fix support for .doc files.. - it('Tries to import .doc that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOC import", testPadId); - }else{ - done(); - } - } - }); + -- TODO: Test. + Try to import to a file and abort it half way through - let form = req.form(); - form.append('file', wordDoc, { - filename: '/test.doc', - contentType: 'application/msword' - }); - }); + Test. + Try to import to files of varying size. - it('exports DOC', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - try{ - request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9000){ - done(); - }else{ - throw new Error("Word Document export length is not right"); - } - }) - }catch(e){ - throw new Error(e); - } - }) - - it('Tries to import .docx that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed DOCX import"); - }else{ - done(); - } + Example Curl command for testing import URI: + curl -s -v --form file=@/home/jose/test.txt http://127.0.0.1:9001/p/foo/import + */ + + describe('Imports and Exports', function () { + const backups = {}; + + beforeEach(async function () { + backups.hooks = {}; + for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) { + backups.hooks[hookName] = plugins.hooks[hookName]; + plugins.hooks[hookName] = []; } + // Note: This is a shallow copy. + backups.settings = Object.assign({}, settings); + settings.requireAuthentication = false; + settings.requireAuthorization = false; + settings.users = {user: {password: 'user-password'}}; }); - let form = req.form(); - form.append('file', wordXDoc, { - filename: '/test.docx', - contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + afterEach(async function () { + Object.assign(plugins.hooks, backups.hooks); + // Note: This does not unset settings that were added. + Object.assign(settings, backups.settings); }); - }); - it('exports DOC from imported DOCX', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - request(host + '/p/'+testPadId+'/export/doc', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 9100){ - done(); - }else{ - throw new Error("Word Document export length is not right"); - } - }) - }) - - it('Tries to import .pdf that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed PDF import"); - }else{ - done(); - } - } + it('creates a new Pad, imports content to it, checks that content', async function () { + await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => assert.equal(res.body.code, 0)); + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect((res) => assert.equal(res.body.data.text, padText.toString())); }); - let form = req.form(); - form.append('file', pdfDoc, { - filename: '/test.pdf', - contentType: 'application/pdf' + it('gets read only pad Id and exports the html and text for this pad', async function () { + const ro = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect(200) + .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); + const readOnlyId = JSON.parse(ro.text).data.readOnlyID; + + await agent.get(`/p/${readOnlyId}/export/html`) + .expect(200) + .expect((res) => assert(res.text.indexOf('This is the') !== -1)); + + await agent.get(`/p/${readOnlyId}/export/txt`) + .expect(200) + .expect((res) => assert(res.text.indexOf('This is the') !== -1)); }); - }); - it('exports PDF', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - request(host + '/p/'+testPadId+'/export/pdf', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 1000){ - done(); - }else{ - throw new Error("PDF Document export length is not right"); - } - }) - }) - - it('Tries to import .odt that uses soffice or abiword', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") === -1){ - throw new Error("Failed ODT import", testPadId); - }else{ - done(); + + describe('Import/Export tests requiring AbiWord/LibreOffice', function () { + before(function () { + if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && + (!settings.soffice || settings.soffice.indexOf('/') === -1)) { + this.skip(); } - } + }); + + // For some reason word import does not work in testing.. + // TODO: fix support for .doc files.. + it('Tries to import .doc that uses soffice or abiword', async function () { + await agent.post(`/p/${testPadId}/import`) + .attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); + }); + + it('exports DOC', async function () { + await agent.get(`/p/${testPadId}/export/doc`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 9000)); + }); + + it('Tries to import .docx that uses soffice or abiword', async function () { + await agent.post(`/p/${testPadId}/import`) + .attach('file', wordXDoc, { + filename: '/test.docx', + contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); + }); + + it('exports DOC from imported DOCX', async function () { + await agent.get(`/p/${testPadId}/export/doc`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 9100)); + }); + + it('Tries to import .pdf that uses soffice or abiword', async function () { + await agent.post(`/p/${testPadId}/import`) + .attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); + }); + + it('exports PDF', async function () { + await agent.get(`/p/${testPadId}/export/pdf`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 1000)); + }); + + it('Tries to import .odt that uses soffice or abiword', async function () { + await agent.post(`/p/${testPadId}/import`) + .attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'}) + .expect(200) + .expect(/FrameCall\('undefined', 'ok'\);/); + }); + + it('exports ODT', async function () { + await agent.get(`/p/${testPadId}/export/odt`) + .buffer(true).parse(superagent.parse['application/octet-stream']) + .expect(200) + .expect((res) => assert(res.body.length >= 7000)); + }); + }); // End of AbiWord/LibreOffice tests. + + it('Tries to import .etherpad', async function () { + await agent.post(`/p/${testPadId}/import`) + .attach('file', etherpadDoc, { + filename: '/test.etherpad', + contentType: 'application/etherpad', + }) + .expect(200) + .expect(/FrameCall\('true', 'ok'\);/); }); - let form = req.form(); - form.append('file', odtDoc, { - filename: '/test.odt', - contentType: 'application/odt' + it('exports Etherpad', async function () { + await agent.get(`/p/${testPadId}/export/etherpad`) + .buffer(true).parse(superagent.parse.text) + .expect(200) + .expect(/hello/); }); - }); - it('exports ODT', function(done) { - if(!settings.allowAnyoneToImport) return done(); - if((settings.abiword && settings.abiword.indexOf("/" === -1)) && (settings.office && settings.soffice.indexOf("/" === -1))) return done(); - request(host + '/p/'+testPadId+'/export/odt', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.length >= 7000){ - done(); - }else{ - throw new Error("ODT Document export length is not right"); - } - }) - }) - - it('Tries to import .etherpad', function(done) { - if(!settings.allowAnyoneToImport) return done(); - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall(\'true\', \'ok\');") === -1){ - throw new Error("Failed Etherpad import", err, testPadId); - }else{ - done(); - } - } + it('exports HTML for this Etherpad file', async function () { + await agent.get(`/p/${testPadId}/export/html`) + .expect(200) + .expect('content-type', 'text/html; charset=utf-8') + .expect(/
          • hello<\/ul><\/li><\/ul>/); }); - let form = req.form(); - form.append('file', etherpadDoc, { - filename: '/test.etherpad', - contentType: 'application/etherpad' + it('Tries to import unsupported file type', async function () { + settings.allowUnknownFileEnds = false; + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'}) + .expect(200) + .expect((res) => assert.doesNotMatch(res.text, /FrameCall\('undefined', 'ok'\);/)); }); - }); - it('exports Etherpad', function(done) { - request(host + '/p/'+testPadId+'/export/etherpad', function (err, res, body) { - // TODO: At some point checking that the contents is correct would be suitable - if(body.indexOf("hello") !== -1){ - done(); - }else{ - console.error("body"); - throw new Error("Etherpad Document does not include hello"); - } - }) - }) - - it('exports HTML for this Etherpad file', function(done) { - request(host + '/p/'+testPadId+'/export/html', function (err, res, body) { - - // broken pre fix export --
              - var expectedHTML = '
                • hello
              '; - // expect body to include - if(body.indexOf(expectedHTML) !== -1){ - done(); - }else{ - console.error(body); - throw new Error("Exported HTML nested list items is not right", body); - } - }) - }) - - it('tries to import Plain Text to a pad that does not exist', function(done) { - var req = request.post(host + '/p/'+testPadId+testPadId+testPadId+'/import', function (err, res, body) { - if (res.statusCode === 200) { - throw new Error("Was able to import to a pad that doesn't exist"); - }else{ - // Wasn't able to write to a pad that doesn't exist, this is expected behavior - api.get(endPoint('getText')+"&padID="+testPadId+testPadId+testPadId) - .expect(function(res){ - if(res.body.code !== 1) throw new Error("Pad Exists"); - }) - .expect(200, done) - } + describe('Import authorization checks', function () { + let authorize; - let form = req.form(); + const deleteTestPad = async () => { + if (await padManager.doesPadExist(testPadId)) { + const pad = await padManager.getPad(testPadId); + await pad.remove(); + } + }; + + const createTestPad = async (text) => { + const pad = await padManager.getPad(testPadId); + if (text) await pad.setText(text); + return pad; + }; + + beforeEach(async function () { + await deleteTestPad(); + settings.requireAuthorization = true; + authorize = () => true; + plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + }); - form.append('file', padText, { - filename: '/test.txt', - contentType: 'text/plain' + afterEach(async function () { + await deleteTestPad(); }); - }) - }); - it('Tries to import unsupported file type', function(done) { - if(settings.allowUnknownFileEnds === true){ - console.log("allowing unknown file ends so skipping this test"); - return done(); - } - - var req = request.post(host + '/p/'+testPadId+'/import', function (err, res, body) { - if (err) { - throw new Error("Failed to import", err); - } else { - if(res.body.indexOf("FrameCall('undefined', 'ok');") !== -1){ - console.log("worked"); - throw new Error("You shouldn't be able to import this file", testPadId); - } - return done(); - } - }); + it('!authn !exist -> create', async function () { + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + const pad = await padManager.getPad(testPadId); + assert.equal(pad.text(), padText.toString()); + }); - let form = req.form(); - form.append('file', padText, { - filename: '/test.xasdasdxx', - contentType: 'weirdness/jobby' - }); - }); + it('!authn exist -> replace', async function () { + const pad = await createTestPad('before import'); + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + assert.equal(pad.text(), padText.toString()); + }); -// end of tests -}) + it('authn anonymous !exist -> fail', async function () { + settings.requireAuthentication = true; + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(401); + assert(!(await padManager.doesPadExist(testPadId))); + }); + it('authn anonymous exist -> fail', async function () { + settings.requireAuthentication = true; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(401); + assert.equal(pad.text(), 'before import\n'); + }); + it('authn user create !exist -> create', async function () { + settings.requireAuthentication = true; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert(await padManager.doesPadExist(testPadId)); + const pad = await padManager.getPad(testPadId); + assert.equal(pad.text(), padText.toString()); + }); + it('authn user modify !exist -> fail', async function () { + settings.requireAuthentication = true; + authorize = () => 'modify'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert(!(await padManager.doesPadExist(testPadId))); + }); + it('authn user readonly !exist -> fail', async function () { + settings.requireAuthentication = true; + authorize = () => 'readOnly'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert(!(await padManager.doesPadExist(testPadId))); + }); -var endPoint = function(point, version){ - version = version || apiVersion; - return '/api/'+version+'/'+point+'?apikey='+apiKey; -} + it('authn user create exist -> replace', async function () { + settings.requireAuthentication = true; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert.equal(pad.text(), padText.toString()); + }); -function makeid() -{ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + it('authn user modify exist -> replace', async function () { + settings.requireAuthentication = true; + authorize = () => 'modify'; + const pad = await createTestPad('before import\n'); + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + assert.equal(pad.text(), padText.toString()); + }); + + it('authn user readonly exist -> fail', async function () { + const pad = await createTestPad('before import\n'); + settings.requireAuthentication = true; + authorize = () => 'readOnly'; + await agent.post(`/p/${testPadIdEnc}/import`) + .auth('user', 'user-password') + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(403); + assert.equal(pad.text(), 'before import\n'); + }); + }); + }); +}); // End of tests. - for( var i=0; i < 5; i++ ){ - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} -function generateLongText(){ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +var endPoint = function (point, version) { + version = version || apiVersion; + return `/api/${version}/${point}?apikey=${apiKey}`; +}; + +function makeid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 80000; i++ ){ + for (let i = 0; i < 5; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } - -// Need this to compare arrays (listSavedRevisions test) -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} diff --git a/tests/backend/specs/api/instance.js b/tests/backend/specs/api/instance.js index 4849c6507af..7da967ed261 100644 --- a/tests/backend/specs/api/instance.js +++ b/tests/backend/specs/api/instance.js @@ -3,52 +3,48 @@ * * Section "GLOBAL FUNCTIONS" in src/node/db/API.js */ -const assert = require('assert'); -const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const fs = require('fs'); -const settings = require(__dirname+'/../../../../src/node/utils/Settings'); -const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); - -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); - -var apiVersion = '1.2.14'; - -describe('Connectivity for instance-level API tests', function() { - it('can connect', function(done) { - api.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done) +const common = require('../../common'); +const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); +const settings = require(`${__dirname}/../../../../src/node/utils/Settings`); +const api = supertest(`http://${settings.ip}:${settings.port}`); + +const apiKey = common.apiKey; +const apiVersion = '1.2.14'; + +describe(__filename, function () { + describe('Connectivity for instance-level API tests', function () { + it('can connect', function (done) { + api.get('/api/') + .expect('Content-Type', /json/) + .expect(200, done); + }); }); -}); - -describe('getStats', function(){ - it('Gets the stats of a running instance', function(done) { - api.get(endPoint('getStats')) - .expect(function(res){ - if (res.body.code !== 0) throw new Error("getStats() failed"); - - if (!(('totalPads' in res.body.data) && (typeof res.body.data.totalPads === 'number'))) { - throw new Error(`Response to getStats() does not contain field totalPads, or it's not a number: ${JSON.stringify(res.body.data)}`); - } - - if (!(('totalSessions' in res.body.data) && (typeof res.body.data.totalSessions === 'number'))) { - throw new Error(`Response to getStats() does not contain field totalSessions, or it's not a number: ${JSON.stringify(res.body.data)}`); - } - if (!(('totalActivePads' in res.body.data) && (typeof res.body.data.totalActivePads === 'number'))) { - throw new Error(`Response to getStats() does not contain field totalActivePads, or it's not a number: ${JSON.stringify(res.body.data)}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + describe('getStats', function () { + it('Gets the stats of a running instance', function (done) { + api.get(endPoint('getStats')) + .expect((res) => { + if (res.body.code !== 0) throw new Error('getStats() failed'); + + if (!(('totalPads' in res.body.data) && (typeof res.body.data.totalPads === 'number'))) { + throw new Error(`Response to getStats() does not contain field totalPads, or it's not a number: ${JSON.stringify(res.body.data)}`); + } + + if (!(('totalSessions' in res.body.data) && (typeof res.body.data.totalSessions === 'number'))) { + throw new Error(`Response to getStats() does not contain field totalSessions, or it's not a number: ${JSON.stringify(res.body.data)}`); + } + + if (!(('totalActivePads' in res.body.data) && (typeof res.body.data.totalActivePads === 'number'))) { + throw new Error(`Response to getStats() does not contain field totalActivePads, or it's not a number: ${JSON.stringify(res.body.data)}`); + } + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); }); }); -var endPoint = function(point, version){ +var endPoint = function (point, version) { version = version || apiVersion; - return '/api/'+version+'/'+point+'?apikey='+apiKey; -} + return `/api/${version}/${point}?apikey=${apiKey}`; +}; diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js index 6de05c8b571..15ec4f0be54 100644 --- a/tests/backend/specs/api/pad.js +++ b/tests/backend/specs/api/pad.js @@ -5,618 +5,614 @@ * TODO: unify those two files, and merge in a single one. */ -const assert = require('assert'); -const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const fs = require('fs'); -const settings = require(__dirname + '/../../../../src/node/utils/Settings'); -const api = supertest('http://'+settings.ip+":"+settings.port); -const path = require('path'); -const async = require(__dirname+'/../../../../src/node_modules/async'); - -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); -var apiVersion = 1; -var testPadId = makeid(); -var lastEdited = ""; -var text = generateLongText(); +const common = require('../../common'); +const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); +const settings = require(`${__dirname}/../../../../src/node/utils/Settings`); +const api = supertest(`http://${settings.ip}:${settings.port}`); +const async = require(`${__dirname}/../../../../src/node_modules/async`); + +const apiKey = common.apiKey; +let apiVersion = 1; +const testPadId = makeid(); +let lastEdited = ''; +const text = generateLongText(); /* * Html document with nested lists of different types, to test its import and * verify it is exported back correctly */ -var ulHtml = '
              • one
              • two
              • 0
              • 1
              • 2
                • 3
                • 4
              1. item
                1. item1
                2. item2
              '; +const ulHtml = '
              • one
              • two
              • 0
              • 1
              • 2
                • 3
                • 4
              1. item
                1. item1
                2. item2
              '; /* * When exported back, Etherpad produces an html which is not exactly the same * textually, but at least it remains standard compliant and has an equal DOM * structure. */ -var expectedHtml = '
              • one
              • two
              • 0
              • 1
              • 2
                • 3
                • 4
              1. item
                1. item1
                2. item2
              '; +const expectedHtml = '
              • one
              • two
              • 0
              • 1
              • 2
                • 3
                • 4
              1. item
                1. item1
                2. item2
              '; /* * Html document with space between list items, to test its import and * verify it is exported back correctly */ -var ulSpaceHtml = '
              • one
              '; +const ulSpaceHtml = '
              • one
              '; /* * When exported back, Etherpad produces an html which is not exactly the same * textually, but at least it remains standard compliant and has an equal DOM * structure. */ -var expectedSpaceHtml = '
              • one
              '; - -describe('Connectivity', function(){ - it('can connect', function(done) { - api.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('API Versioning', function(){ - it('finds the version tag', function(done) { - api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) - }); -}) - -describe('Permission', function(){ - it('errors with invalid APIKey', function(done) { - // 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 - var permErrorURL = '/api/'+apiVersion+'/createPad?apikey=password&padID=test'; - api.get(permErrorURL) - .expect(401, done) - }); -}) - -/* Pad Tests Order of execution --> deletePad -- This gives us a guaranteed clear environment - -> createPad - -> getRevisions -- Should be 0 - -> getSavedRevisionsCount(padID) -- Should be 0 - -> listSavedRevisions(padID) -- Should be an empty array - -> getHTML -- Should be the default pad text in HTML format - -> deletePad -- Should just delete a pad - -> getHTML -- Should return an error - -> createPad(withText) - -> getText -- Should have the text specified above as the pad text - -> setText - -> getText -- Should be the text set before - -> getRevisions -- Should be 0 still? - -> saveRevision - -> getSavedRevisionsCount(padID) -- Should be 0 still? - -> listSavedRevisions(padID) -- Should be an empty array still ? - -> padUsersCount -- Should be 0 - -> getReadOnlyId -- Should be a value - -> listAuthorsOfPad(padID) -- should be empty array? - -> getLastEdited(padID) -- Should be when pad was made - -> setText(padId) - -> getLastEdited(padID) -- Should be when setText was performed - -> padUsers(padID) -- Should be when setText was performed - - -> setText(padId, "hello world") - -> getLastEdited(padID) -- Should be when pad was made - -> getText(padId) -- Should be "hello world" - -> movePad(padID, newPadId) -- Should provide consistant pad data - -> getText(newPadId) -- Should be "hello world" - -> movePad(newPadID, originalPadId) -- Should provide consistant pad data - -> getText(originalPadId) -- Should be "hello world" - -> getLastEdited(padID) -- Should not be 0 - -> appendText(padID, "hello") - -> getText(padID) -- Should be "hello worldhello" - -> setHTML(padID) -- Should fail on invalid HTML - -> setHTML(padID) *3 -- Should fail on invalid HTML - -> getHTML(padID) -- Should return HTML close to posted HTML - -> createPad -- Tries to create pads with bad url characters +const expectedSpaceHtml = '
              • one
              '; -*/ +describe(__filename, function () { + describe('Connectivity', function () { + it('can connect', function (done) { + api.get('/api/') + .expect('Content-Type', /json/) + .expect(200, done); + }); + }); -describe('deletePad', function(){ - it('deletes a Pad', function(done) { - api.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('createPad', function(){ - it('creates a new Pad', function(done) { - api.get(endPoint('createPad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create new Pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getRevisionsCount', function(){ - it('gets revision count of Pad', function(done) { - api.get(endPoint('getRevisionsCount')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('getSavedRevisionsCount', function(){ - it('gets saved revisions count of Pad', function(done) { - api.get(endPoint('getSavedRevisionsCount')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('listSavedRevisions', function(){ - it('gets saved revision list of Pad', function(done) { - api.get(endPoint('listSavedRevisions')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to get Saved Revisions List"); - if(!res.body.data.savedRevisions.equals([])) throw new Error("Incorrect Saved Revisions List"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getHTML', function(){ - it('get the HTML of Pad', function(done) { - api.get(endPoint('getHTML')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.html.length <= 1) throw new Error("Unable to get the HTML"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listAllPads', function () { - it('list all pads', function (done) { - api.get(endPoint('listAllPads')) - .expect(function (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) - }) -}) - -describe('deletePad', function(){ - it('deletes a Pad', function(done) { - api.get(endPoint('deletePad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Deletion failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listAllPads', function () { - it('list all pads', function (done) { - api.get(endPoint('listAllPads')) - .expect(function (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) - }) -}) - -describe('getHTML', function(){ - it('get the HTML of a Pad -- Should return a failure', function(done) { - api.get(endPoint('getHTML')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 1) throw new Error("Pad deletion failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createPad', function(){ - it('creates a new Pad with text', function(done) { - api.get(endPoint('createPad')+"&padID="+testPadId+"&text=testText") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Creation failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it('gets the Pad text and expect it to be testText with \n which is a line break', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== "testText\n") throw new Error("Pad Creation with text") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('setText', function(){ - it('creates a new Pad with text', function(done) { - api.post(endPoint('setText')) - .send({ - "padID": testPadId, - "text": "testTextTwo", - }) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad setting text failed"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it('gets the Pad text', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== "testTextTwo\n") throw new Error("Setting Text") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getRevisionsCount', function(){ - it('gets Revision Count of a Pad', function(done) { - api.get(endPoint('getRevisionsCount')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.revisions !== 1) throw new Error("Unable to get text revision count") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('saveRevision', function(){ - it('saves Revision', function(done) { - api.get(endPoint('saveRevision')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to save Revision"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getSavedRevisionsCount', function(){ - it('gets saved revisions count of Pad', function(done) { - api.get(endPoint('getSavedRevisionsCount')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('listSavedRevisions', function(){ - it('gets saved revision list of Pad', function(done) { - api.get(endPoint('listSavedRevisions')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to get Saved Revisions List"); - if(!res.body.data.savedRevisions.equals([1])) throw new Error("Incorrect Saved Revisions List"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) -describe('padUsersCount', function(){ - it('gets User Count of a Pad', function(done) { - api.get(endPoint('padUsersCount')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.padUsersCount !== 0) throw new Error("Incorrect Pad User count") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getReadOnlyID', function(){ - it('Gets the Read Only ID of a Pad', function(done) { - api.get(endPoint('getReadOnlyID')+"&padID="+testPadId) - .expect(function(res){ - if(!res.body.data.readOnlyID) throw new Error("No Read Only ID for Pad") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listAuthorsOfPad', function(){ - it('Get Authors of the Pad', function(done) { - api.get(endPoint('listAuthorsOfPad')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('getLastEdited', function(){ - it('Get When Pad was left Edited', function(done) { - api.get(endPoint('getLastEdited')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('setText', function(){ - it('creates a new Pad with text', function(done) { - api.post(endPoint('setText')) - .send({ - "padID": testPadId, - "text": "testTextTwo", - }) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad setting text failed"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getLastEdited', function(){ - it('Get When Pad was left Edited', function(done) { - api.get(endPoint('getLastEdited')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('padUsers', function(){ - it('gets User Count of a Pad', function(done) { - api.get(endPoint('padUsers')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.padUsers.length !== 0) throw new Error("Incorrect Pad Users") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('deletePad', function(){ - it('deletes a Pad', function(done) { - api.get(endPoint('deletePad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Deletion failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -var originalPadId = testPadId; -var newPadId = makeid(); -var copiedPadId = makeid(); - -describe('createPad', function(){ - it('creates a new Pad with text', function(done) { - api.get(endPoint('createPad')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Creation failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('setText', function(){ - it('Sets text on a pad Id', function(done) { - api.post(endPoint('setText')+"&padID="+testPadId) - .field({text: text}) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Set Text failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it('Gets text on a pad Id', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('setText', function(){ - it('Sets text on a pad Id including an explicit newline', function(done) { - api.post(endPoint('setText')+"&padID="+testPadId) - .field({text: text+'\n'}) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Set Text failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it("Gets text on a pad Id and doesn't have an excess newline", function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(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) - }); -}) - -describe('getLastEdited', function(){ - it('Gets when pad was last edited', function(done) { - api.get(endPoint('getLastEdited')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.lastEdited === 0) throw new Error("Get Last Edited Failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('movePad', function(){ - it('Move a Pad to a different Pad ID', function(done) { - api.get(endPoint('movePad')+"&sourceID="+testPadId+"&destinationID="+newPadId+"&force=true") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Moving Pad Failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it('Gets text on a pad Id', function(done) { - api.get(endPoint('getText')+"&padID="+newPadId) - .expect(function(res){ - if(res.body.data.text !== text+"\n") throw new Error("Pad Get Text failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('movePad', function(){ - it('Move a Pad to a different Pad ID', function(done) { - api.get(endPoint('movePad')+"&sourceID="+newPadId+"&destinationID="+testPadId+"&force=false") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Moving Pad Failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getText', function(){ - it('Gets text on a pad Id', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(res){ - if(res.body.data.text !== text+"\n") throw new Error("Pad Get Text failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getLastEdited', function(){ - it('Gets when pad was last edited', function(done) { - api.get(endPoint('getLastEdited')+"&padID="+testPadId) - .expect(function(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) { - api.get(endPoint('appendText', '1.2.13')+"&padID="+testPadId+"&text=hello") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Pad Append Text failed"); - }) - .expect('Content-Type', /json/) - .expect(200, done); + describe('API Versioning', function () { + it('finds the version tag', function (done) { + api.get('/api/') + .expect((res) => { + apiVersion = res.body.currentVersion; + if (!res.body.currentVersion) throw new Error('No version set in API'); + return; + }) + .expect(200, done); + }); + }); + + describe('Permission', function () { + it('errors with invalid APIKey', function (done) { + // 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`; + api.get(permErrorURL) + .expect(401, done); + }); }); -}); -describe('getText', function(){ - it('Gets text on a pad Id', function(done) { - api.get(endPoint('getText')+"&padID="+testPadId) - .expect(function(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); + /* Pad Tests Order of execution + -> deletePad -- This gives us a guaranteed clear environment + -> createPad + -> getRevisions -- Should be 0 + -> getSavedRevisionsCount(padID) -- Should be 0 + -> listSavedRevisions(padID) -- Should be an empty array + -> getHTML -- Should be the default pad text in HTML format + -> deletePad -- Should just delete a pad + -> getHTML -- Should return an error + -> createPad(withText) + -> getText -- Should have the text specified above as the pad text + -> setText + -> getText -- Should be the text set before + -> getRevisions -- Should be 0 still? + -> saveRevision + -> getSavedRevisionsCount(padID) -- Should be 0 still? + -> listSavedRevisions(padID) -- Should be an empty array still ? + -> padUsersCount -- Should be 0 + -> getReadOnlyId -- Should be a value + -> listAuthorsOfPad(padID) -- should be empty array? + -> getLastEdited(padID) -- Should be when pad was made + -> setText(padId) + -> getLastEdited(padID) -- Should be when setText was performed + -> padUsers(padID) -- Should be when setText was performed + + -> setText(padId, "hello world") + -> getLastEdited(padID) -- Should be when pad was made + -> getText(padId) -- Should be "hello world" + -> movePad(padID, newPadId) -- Should provide consistant pad data + -> getText(newPadId) -- Should be "hello world" + -> movePad(newPadID, originalPadId) -- Should provide consistant pad data + -> getText(originalPadId) -- Should be "hello world" + -> getLastEdited(padID) -- Should not be 0 + -> appendText(padID, "hello") + -> getText(padID) -- Should be "hello worldhello" + -> setHTML(padID) -- Should fail on invalid HTML + -> setHTML(padID) *3 -- Should fail on invalid HTML + -> getHTML(padID) -- Should return HTML close to posted HTML + -> createPad -- Tries to create pads with bad url characters + + */ + + describe('deletePad', function () { + it('deletes a Pad', function (done) { + api.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('createPad', function () { + it('creates a new Pad', function (done) { + api.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('getRevisionsCount', function () { + it('gets revision count of Pad', function (done) { + api.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); + }); + }); + + describe('getSavedRevisionsCount', function () { + it('gets saved revisions count of Pad', function (done) { + api.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); + }); + }); + + describe('listSavedRevisions', function () { + it('gets saved revision list of Pad', function (done) { + api.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect((res) => { + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + if (!res.body.data.savedRevisions.equals([])) throw new Error('Incorrect Saved Revisions List'); + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); + }); + + describe('getHTML', function () { + it('get the HTML of Pad', function (done) { + api.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); + }); + }); + + describe('listAllPads', function () { + it('list all pads', function (done) { + api.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); + }); + }); + + describe('deletePad', function () { + it('deletes a Pad', function (done) { + api.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); + }); + }); + + describe('listAllPads', function () { + it('list all pads', function (done) { + api.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); + }); + }); + + describe('getHTML', function () { + it('get the HTML of a Pad -- Should return a failure', function (done) { + api.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); + }); + }); + + describe('createPad', function () { + it('creates a new Pad with text', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it('gets the Pad text and expect it to be testText with \n which is a line break', function (done) { + api.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); + }); + }); + + describe('setText', function () { + it('creates a new Pad with text', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it('gets the Pad text', function (done) { + api.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); + }); + }); + + describe('getRevisionsCount', function () { + it('gets Revision Count of a Pad', function (done) { + api.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); + }); + }); + + describe('saveRevision', function () { + it('saves Revision', function (done) { + api.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); + }); + }); + + describe('getSavedRevisionsCount', function () { + it('gets saved revisions count of Pad', function (done) { + api.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); + }); + }); + + describe('listSavedRevisions', function () { + it('gets saved revision list of Pad', function (done) { + api.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect((res) => { + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + if (!res.body.data.savedRevisions.equals([1])) throw new Error('Incorrect Saved Revisions List'); + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); + }); + describe('padUsersCount', function () { + it('gets User Count of a Pad', function (done) { + api.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); + }); + }); + + describe('getReadOnlyID', function () { + it('Gets the Read Only ID of a Pad', function (done) { + api.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); + }); + }); + + describe('listAuthorsOfPad', function () { + it('Get Authors of the Pad', function (done) { + api.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); + }); + }); + + describe('getLastEdited', function () { + it('Get When Pad was left Edited', function (done) { + api.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); + }); + }); + + describe('setText', function () { + it('creates a new Pad with text', function (done) { + api.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); + }); + }); + + describe('getLastEdited', function () { + it('Get When Pad was left Edited', function (done) { + api.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); + }); + }); + + describe('padUsers', function () { + it('gets User Count of a Pad', function (done) { + api.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); + }); }); -}); + describe('deletePad', function () { + it('deletes a Pad', function (done) { + api.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); + }); + }); -describe('setHTML', function(){ - it('Sets the HTML of a Pad attempting to pass ugly HTML', function(done) { - var html = "
              Hello HTML
              "; - api.post(endPoint('setHTML')) - .send({ - "padID": testPadId, - "html": html, - }) - .expect(function(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) - }); -}) - -describe('setHTML', function(){ - it('Sets the HTML of a Pad with complex nested lists of different types', function(done) { - api.post(endPoint('setHTML')) - .send({ - "padID": testPadId, - "html": ulHtml, - }) - .expect(function(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) { - api.get(endPoint('getHTML')+"&padID="+testPadId) - .expect(function(res){ - var receivedHtml = res.body.data.html.replace("
              ", "").toLowerCase(); - - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + const originalPadId = testPadId; + const newPadId = makeid(); + const copiedPadId = makeid(); + + describe('createPad', function () { + it('creates a new Pad with text', function (done) { + api.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); + }); + }); + + describe('setText', function () { + it('Sets text on a pad Id', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it('Gets text on a pad Id', function (done) { + api.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); + }); + }); + + describe('setText', function () { + it('Sets text on a pad Id including an explicit newline', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it("Gets text on a pad Id and doesn't have an excess newline", function (done) { + api.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); + }); + }); + + describe('getLastEdited', function () { + it('Gets when pad was last edited', function (done) { + api.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('movePad', function () { + it('Move a Pad to a different Pad ID', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it('Gets text on a pad Id', function (done) { + api.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); + }); + }); + + describe('movePad', function () { + it('Move a Pad to a different Pad ID', function (done) { + api.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); + }); + }); + + describe('getText', function () { + it('Gets text on a pad Id', function (done) { + api.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); + }); + }); + + describe('getLastEdited', function () { + it('Gets when pad was last edited', function (done) { + api.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) { + api.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); + }); + }); + + describe('getText', function () { + it('Gets text on a pad Id', function (done) { + api.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); + }); + }); + + + describe('setHTML', function () { + it('Sets the HTML of a Pad attempting to pass ugly HTML', function (done) { + const html = '
              Hello HTML
              '; + api.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); + }); + }); + + describe('setHTML', function () { + it('Sets the HTML of a Pad with complex nested lists of different types', function (done) { + api.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) { + api.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} @@ -625,31 +621,31 @@ describe('getHTML', function(){ Which is a slightly modified version of the originally imported one: ${ulHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('setHTML', function(){ - it('Sets the HTML of a Pad with white space between list items', function(done) { - api.get(endPoint('setHTML')+"&padID="+testPadId+"&html="+ulSpaceHtml) - .expect(function(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) { - api.get(endPoint('getHTML')+"&padID="+testPadId) - .expect(function(res){ - var receivedHtml = res.body.data.html.replace("
              ", "").toLowerCase(); - if (receivedHtml !== expectedSpaceHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + } + }) + .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) { + api.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) { + api.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} @@ -658,75 +654,75 @@ describe('getHTML', function(){ Which is a slightly modified version of the originally imported one: ${ulSpaceHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createPad', function(){ - it('errors if pad can be created', function(done) { - var badUrlChars = ["/", "%23", "%3F", "%26"]; - async.map( - badUrlChars, - function (badUrlChar, cb) { - api.get(endPoint('createPad')+"&padID="+badUrlChar) - .expect(function(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) { - api.get(endPoint('copyPad')+"&sourceID="+testPadId+"&destinationID="+copiedPadId+"&force=true") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Copy Pad Failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('copyPadWithoutHistory', function(){ - var sourcePadId = makeid(); - var newPad; - - before(function(done) { - createNewPadWithHtml(sourcePadId, ulHtml, done); - }); - - beforeEach(function() { - newPad = makeid(); - }) - - it('returns a successful response', function(done) { - api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed") - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); - - // 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) { - api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed") - }) - .end(function() { - api.get(endPoint('getHTML')+"&padID="+newPad) - .expect(function(res){ - var receivedHtml = res.body.data.html.replace("

              ", "").toLowerCase(); + } + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); + }); - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + describe('createPad', function () { + it('errors if pad can be created', function (done) { + const badUrlChars = ['/', '%23', '%3F', '%26']; + async.map( + badUrlChars, + (badUrlChar, cb) => { + api.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) { + api.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); + }); + }); + + describe('copyPadWithoutHistory', function () { + const sourcePadId = makeid(); + let newPad; + + before(function (done) { + createNewPadWithHtml(sourcePadId, ulHtml, done); + }); + + beforeEach(function () { + newPad = makeid(); + }); + + it('returns a successful response', function (done) { + api.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); + }); + + // 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) { + api.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(() => { + api.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} @@ -735,94 +731,94 @@ describe('copyPadWithoutHistory', function(){ Which is a slightly modified version of the originally imported one: ${ulHtml}`); - } - }) - .expect(200, done); - }); - }); + } + }) + .expect(200, done); + }); + }); - context('when try copy a pad with a group that does not exist', function() { - var padId = makeid(); - var padWithNonExistentGroup = `notExistentGroup$${padId}` - it('throws an error', function(done) { - api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padWithNonExistentGroup+"&force=true") - .expect(function(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); - }) - }); + context('when try copy a pad with a group that does not exist', function () { + const padId = makeid(); + const padWithNonExistentGroup = `notExistentGroup$${padId}`; + it('throws an error', function (done) { + api.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); + }); + }); - context('when try copy a pad and destination pad already exist', function() { - var padIdExistent = makeid(); + context('when try copy a pad and destination pad already exist', function () { + const padIdExistent = makeid(); - before(function(done) { - createNewPadWithHtml(padIdExistent, ulHtml, done); - }); + before(function (done) { + createNewPadWithHtml(padIdExistent, ulHtml, done); + }); - context('and force is false', function() { - it('throws an error', function(done) { - api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padIdExistent+"&force=false") - .expect(function(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); + context('and force is false', function () { + it('throws an error', function (done) { + api.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); + }); }); - }); - context('and force is true', function() { - it('returns a successful response', function(done) { - api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padIdExistent+"&force=true") - .expect(function(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); + context('and force is true', function () { + it('returns a successful response', function (done) { + api.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); + }); }); }); - }) -}) + }); +}); /* -> movePadForce Test */ -var createNewPadWithHtml = function(padId, html, cb) { - api.get(endPoint('createPad')+"&padID="+padId) - .end(function() { - api.post(endPoint('setHTML')) - .send({ - "padID": padId, - "html": html, - }) - .end(cb); - }) -} +var createNewPadWithHtml = function (padId, html, cb) { + api.get(`${endPoint('createPad')}&padID=${padId}`) + .end(() => { + api.post(endPoint('setHTML')) + .send({ + padID: padId, + html, + }) + .end(cb); + }); +}; -var endPoint = function(point, version){ +var endPoint = function (point, version) { version = version || apiVersion; - return '/api/'+version+'/'+point+'?apikey='+apiKey; -} + return `/api/${version}/${point}?apikey=${apiKey}`; +}; -function makeid() -{ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function makeid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 5; i++ ){ + for (let i = 0; i < 5; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } -function generateLongText(){ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function generateLongText() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 80000; i++ ){ + for (let i = 0; i < 80000; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; @@ -830,22 +826,19 @@ function generateLongText(){ // Need this to compare arrays (listSavedRevisions test) Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } + // if the other array is a falsy value, return + if (!array) return false; + // compare lengths - can save a lot of time + if (this.length != array.length) return false; + for (let i = 0, l = this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) return false; + } else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; } - return true; -} + } + return true; +}; diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js index cbc6e0a1abf..3b5e1a91270 100644 --- a/tests/backend/specs/api/sessionsAndGroups.js +++ b/tests/backend/specs/api/sessionsAndGroups.js @@ -1,364 +1,298 @@ -var assert = require('assert') - supertest = require(__dirname+'/../../../../src/node_modules/supertest'), - fs = require('fs'), - settings = require(__dirname + '/../../../../src/node/utils/Settings'), - api = supertest('http://'+settings.ip+":"+settings.port), - path = require('path'); - -var filePath = path.join(__dirname, '../../../../APIKEY.txt'); - -var apiKey = fs.readFileSync(filePath, {encoding: 'utf-8'}); -apiKey = apiKey.replace(/\n$/, ""); -var apiVersion = 1; -var testPadId = makeid(); -var groupID = ""; -var authorID = ""; -var sessionID = ""; -var padID = makeid(); - -describe('API Versioning', function(){ - it('errors if can not connect', function(done) { - api.get('/api/') - .expect(function(res){ - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) +const assert = require('assert').strict; +const common = require('../../common'); +const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); +const settings = require(`${__dirname}/../../../../src/node/utils/Settings`); +const api = supertest(`http://${settings.ip}:${settings.port}`); + +const apiKey = common.apiKey; +let apiVersion = 1; +let groupID = ''; +let authorID = ''; +let sessionID = ''; +let padID = makeid(); + +describe(__filename, function () { + describe('API Versioning', function () { + it('errors if can not connect', async function () { + await api.get('/api/') + .expect(200) + .expect((res) => { + assert(res.body.currentVersion); + apiVersion = res.body.currentVersion; + }); + }); }); -}) - -// BEGIN GROUP AND AUTHOR TESTS -///////////////////////////////////// -///////////////////////////////////// - -/* Tests performed --> createGroup() -- should return a groupID - -> listSessionsOfGroup(groupID) -- should be 0 - -> deleteGroup(groupID) - -> createGroupIfNotExistsFor(groupMapper) -- should return a groupID - - -> createAuthor([name]) -- should return an authorID - -> createAuthorIfNotExistsFor(authorMapper [, name]) -- should return an authorID - -> getAuthorName(authorID) -- should return a name IE "john" - --> createSession(groupID, authorID, validUntil) - -> getSessionInfo(sessionID) - -> listSessionsOfGroup(groupID) -- should be 1 - -> deleteSession(sessionID) - -> getSessionInfo(sessionID) -- should have author id etc in - --> listPads(groupID) -- should be empty array - -> createGroupPad(groupID, padName [, text]) + + // BEGIN GROUP AND AUTHOR TESTS + // /////////////////////////////////// + // /////////////////////////////////// + + /* Tests performed + -> createGroup() -- should return a groupID + -> listSessionsOfGroup(groupID) -- should be 0 + -> deleteGroup(groupID) + -> createGroupIfNotExistsFor(groupMapper) -- should return a groupID + + -> createAuthor([name]) -- should return an authorID + -> createAuthorIfNotExistsFor(authorMapper [, name]) -- should return an authorID + -> getAuthorName(authorID) -- should return a name IE "john" + + -> createSession(groupID, authorID, validUntil) + -> getSessionInfo(sessionID) + -> listSessionsOfGroup(groupID) -- should be 1 + -> deleteSession(sessionID) + -> getSessionInfo(sessionID) -- should have author id etc in + -> listPads(groupID) -- should be empty array - -> getPublicStatus(padId) - -> setPublicStatus(padId, status) + -> createGroupPad(groupID, padName [, text]) + -> listPads(groupID) -- should be empty array -> getPublicStatus(padId) - -> isPasswordProtected(padID) -- should be false - -> setPassword(padID, password) - -> isPasswordProtected(padID) -- should be true - --> listPadsOfAuthor(authorID) -*/ - -describe('createGroup', function(){ - it('creates a new group', function(done) { - api.get(endPoint('createGroup')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); - groupID = res.body.data.groupID; - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listSessionsOfGroup', function(){ - it('Lists the session of a group', function(done) { - api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data !== null) throw new Error("Sessions show as existing for this group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('deleteGroup', function(){ - it('Deletes a group', function(done) { - api.get(endPoint('deleteGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Group failed to be deleted"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createGroupIfNotExistsFor', function(){ - it('Creates a group if one doesnt exist for mapper 0', function(done) { - api.get(endPoint('createGroupIfNotExistsFor')+"&groupMapper=management") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Sessions show as existing for this group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createGroup', function(){ - it('creates a new group', function(done) { - api.get(endPoint('createGroup')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID) throw new Error("Unable to create new Pad"); - groupID = res.body.data.groupID; - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createAuthor', function(){ - it('Creates an author with a name set', function(done) { - api.get(endPoint('createAuthor')) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createAuthor', function(){ - it('Creates an author with a name set', function(done) { - api.get(endPoint('createAuthor')+"&name=john") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create user with name set"); - authorID = res.body.data.authorID; // we will be this author for the rest of the tests - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createAuthorIfNotExistsFor', function(){ - it('Creates an author if it doesnt exist already and provides mapping', function(done) { - api.get(endPoint('createAuthorIfNotExistsFor')+"&authorMapper=chris") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.authorID) throw new Error("Unable to create author with mapper"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getAuthorName', function(){ - it('Gets the author name', function(done) { - api.get(endPoint('getAuthorName')+"&authorID="+authorID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data !== "john") throw new Error("Unable to get Author Name from Author ID"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -// BEGIN SESSION TESTS -/////////////////////////////////////// -/////////////////////////////////////// - -describe('createSession', function(){ - it('Creates a session for an Author', function(done) { - api.get(endPoint('createSession')+"&authorID="+authorID+"&groupID="+groupID+"&validUntil=999999999999") - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.sessionID) throw new Error("Unable to create Session"); - sessionID = res.body.data.sessionID; - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getSessionInfo', function(){ - it('Gets session inf', function(done) { - api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.groupID || !res.body.data.authorID || !res.body.data.validUntil) throw new Error("Unable to get Session info"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listSessionsOfGroup', function(){ - it('Gets sessions of a group', function(done) { - api.get(endPoint('listSessionsOfGroup')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || typeof res.body.data !== "object") throw new Error("Unable to get sessions of a group"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + -> setPublicStatus(padId, status) + -> getPublicStatus(padId) + + -> listPadsOfAuthor(authorID) + */ + + describe('API: Group creation and deletion', function () { + it('createGroup', async function () { + await api.get(endPoint('createGroup')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + groupID = res.body.data.groupID; + }); + }); + + it('listSessionsOfGroup for empty group', async function () { + await api.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data, null); + }); + }); + + it('deleteGroup', async function () { + await api.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); + }); + + it('createGroupIfNotExistsFor', async function () { + await api.get(`${endPoint('createGroupIfNotExistsFor')}&groupMapper=management`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + }); + }); }); -}) - -describe('deleteSession', function(){ - it('Deletes a session', function(done) { - api.get(endPoint('deleteSession')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to delete a session"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getSessionInfo', function(){ - it('Gets session info', function(done) { - api.get(endPoint('getSessionInfo')+"&sessionID="+sessionID) - .expect(function(res){ - if(res.body.code !== 1) throw new Error("Session was not properly deleted"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -// GROUP PAD MANAGEMENT -/////////////////////////////////////// -/////////////////////////////////////// - -describe('listPads', function(){ - it('Lists Pads of a Group', function(done) { - api.get(endPoint('listPads')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Group already had pads for some reason"+res.body.data.padIDs); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('createGroupPad', function(){ - it('Creates a Group Pad', function(done) { - api.get(endPoint('createGroupPad')+"&groupID="+groupID+"&padName="+padID) - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unable to create group pad"); - padID = res.body.data.padID; - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('listPads', function(){ - it('Lists Pads of a Group', function(done) { - api.get(endPoint('listPads')+"&groupID="+groupID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 1) throw new Error("Group isnt listing this pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -// PAD SECURITY /-_-\ -/////////////////////////////////////// -/////////////////////////////////////// - -describe('getPublicStatus', function(){ - it('Gets the public status of a pad', function(done) { - api.get(endPoint('getPublicStatus')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.publicstatus) throw new Error("Unable to get public status of this pad"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('setPublicStatus', function(){ - it('Sets the public status of a pad', function(done) { - api.get(endPoint('setPublicStatus')+"&padID="+padID+"&publicStatus=true") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Setting status did not work"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('getPublicStatus', function(){ - it('Gets the public status of a pad', function(done) { - api.get(endPoint('getPublicStatus')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.publicStatus) throw new Error("Setting public status of this pad did not work"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('isPasswordProtected', function(){ - it('Gets the public status of a pad', function(done) { - api.get(endPoint('isPasswordProtected')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.isPasswordProtected) throw new Error("Pad is password protected by default"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('setPassword', function(){ - it('Gets the public status of a pad', function(done) { - api.get(endPoint('setPassword')+"&padID="+padID+"&password=test") - .expect(function(res){ - if(res.body.code !== 0) throw new Error("Unabe to set password"); - }) - .expect('Content-Type', /json/) - .expect(200, done) - }); -}) - -describe('isPasswordProtected', function(){ - it('Gets the public status of a pad', function(done) { - api.get(endPoint('isPasswordProtected')+"&padID="+padID) - .expect(function(res){ - if(res.body.code !== 0 || !res.body.data.isPasswordProtected) throw new Error("Pad password protection has not applied"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + + describe('API: Author creation', function () { + it('createGroup', async function () { + await api.get(endPoint('createGroup')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + groupID = res.body.data.groupID; + }); + }); + + it('createAuthor', async function () { + await api.get(endPoint('createAuthor')) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + }); + }); + + it('createAuthor with name', async function () { + await api.get(`${endPoint('createAuthor')}&name=john`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + authorID = res.body.data.authorID; // we will be this author for the rest of the tests + }); + }); + + it('createAuthorIfNotExistsFor', async function () { + await api.get(`${endPoint('createAuthorIfNotExistsFor')}&authorMapper=chris`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.authorID); + }); + }); + + it('getAuthorName', async function () { + await api.get(`${endPoint('getAuthorName')}&authorID=${authorID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data, 'john'); + }); + }); }); -}) + describe('API: Sessions', function () { + it('createSession', async function () { + await api.get(`${endPoint('createSession') + }&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.sessionID); + sessionID = res.body.data.sessionID; + }); + }); + + it('getSessionInfo', async function () { + await api.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert(res.body.data.groupID); + assert(res.body.data.authorID); + assert(res.body.data.validUntil); + }); + }); + + it('listSessionsOfGroup', async function () { + await api.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(typeof res.body.data, 'object'); + }); + }); + + it('deleteSession', async function () { + await api.get(`${endPoint('deleteSession')}&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); + }); + + it('getSessionInfo of deleted session', async function () { + await api.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 1); + }); + }); + }); -// NOT SURE HOW TO POPULAT THIS /-_-\ -/////////////////////////////////////// -/////////////////////////////////////// + describe('API: Group pad management', function () { + it('listPads', async function () { + await api.get(`${endPoint('listPads')}&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 0); + }); + }); + + it('createGroupPad', async function () { + await api.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + padID = res.body.data.padID; + }); + }); + + it('listPads after creating a group pad', async function () { + await api.get(`${endPoint('listPads')}&groupID=${groupID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 1); + }); + }); + }); -describe('listPadsOfAuthor', function(){ - it('Gets the Pads of an Author', function(done) { - api.get(endPoint('listPadsOfAuthor')+"&authorID="+authorID) - .expect(function(res){ - if(res.body.code !== 0 || res.body.data.padIDs.length !== 0) throw new Error("Pad password protection has not applied"); - }) - .expect('Content-Type', /json/) - .expect(200, done) + describe('API: Pad security', function () { + it('getPublicStatus', async function () { + await api.get(`${endPoint('getPublicStatus')}&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.publicStatus, false); + }); + }); + + it('setPublicStatus', async function () { + await api.get(`${endPoint('setPublicStatus')}&padID=${padID}&publicStatus=true`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + }); + }); + + it('getPublicStatus after changing public status', async function () { + await api.get(`${endPoint('getPublicStatus')}&padID=${padID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.publicStatus, true); + }); + }); }); -}) + // NOT SURE HOW TO POPULAT THIS /-_-\ + // ///////////////////////////////////// + // ///////////////////////////////////// + + describe('API: Misc', function () { + it('listPadsOfAuthor', async function () { + await api.get(`${endPoint('listPadsOfAuthor')}&authorID=${authorID}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 0); + assert.equal(res.body.data.padIDs.length, 0); + }); + }); + }); +}); -var endPoint = function(point){ - return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; -} +const endPoint = function (point) { + return `/api/${apiVersion}/${point}?apikey=${apiKey}`; +}; -function makeid() -{ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function makeid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 5; i++ ){ + for (let i = 0; i < 5; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; diff --git a/tests/backend/specs/api/tidy.js b/tests/backend/specs/api/tidy.js index 3ef61931b2c..78784fff702 100644 --- a/tests/backend/specs/api/tidy.js +++ b/tests/backend/specs/api/tidy.js @@ -1,70 +1,72 @@ -var assert = require('assert') - os = require('os'), - fs = require('fs'), - path = require('path'), - TidyHtml = null, - Settings = null; +const assert = require('assert'); +os = require('os'), +fs = require('fs'), +path = require('path'), +TidyHtml = null, +Settings = null; -var npm = require("../../../../src/node_modules/npm/lib/npm.js"); -var nodeify = require('../../../../src/node_modules/nodeify'); +const npm = require('../../../../src/node_modules/npm/lib/npm.js'); +const nodeify = require('../../../../src/node_modules/nodeify'); -describe('tidyHtml', function() { - before(function(done) { - npm.load({}, function(err) { - assert.ok(!err); - TidyHtml = require('../../../../src/node/utils/TidyHtml'); - Settings = require('../../../../src/node/utils/Settings'); - return done() +describe(__filename, function () { + describe('tidyHtml', function () { + before(function (done) { + npm.load({}, (err) => { + assert.ok(!err); + TidyHtml = require('../../../../src/node/utils/TidyHtml'); + Settings = require('../../../../src/node/utils/Settings'); + return done(); + }); }); - }); - - function tidy(file, callback) { - return nodeify(TidyHtml.tidy(file), callback); - } - it('Tidies HTML', function(done) { - // If the user hasn't configured Tidy, we skip this tests as it's required for this test - if (!Settings.tidyHtml) { - this.skip(); + function tidy(file, callback) { + return nodeify(TidyHtml.tidy(file), callback); } - // Try to tidy up a bad HTML file - const tmpDir = os.tmpdir(); + it('Tidies HTML', function (done) { + // If the user hasn't configured Tidy, we skip this tests as it's required for this test + if (!Settings.tidyHtml) { + this.skip(); + } + + // Try to tidy up a bad HTML file + const tmpDir = os.tmpdir(); - var tmpFile = path.join(tmpDir, 'tmp_' + (Math.floor(Math.random() * 1000000)) + '.html') - fs.writeFileSync(tmpFile, '

              a paragraph

            • List without outer UL
            • trailing closing p

              '); - tidy(tmpFile, function(err){ - assert.ok(!err); + const tmpFile = path.join(tmpDir, `tmp_${Math.floor(Math.random() * 1000000)}.html`); + fs.writeFileSync(tmpFile, '

              a paragraph

            • List without outer UL
            • trailing closing p

              '); + tidy(tmpFile, (err) => { + assert.ok(!err); - // Read the file again - var cleanedHtml = fs.readFileSync(tmpFile).toString(); + // Read the file again + const cleanedHtml = fs.readFileSync(tmpFile).toString(); - var expectedHtml = [ - '', - '', - '', - '

              a paragraph

              ', - '
                ', - '
              • List without outer UL
              • ', - '
              • trailing closing p
              • ', - '
              ', - '', - '', - ].join('\n'); - assert.notStrictEqual(cleanedHtml.indexOf(expectedHtml), -1); - return done(); + const expectedHtml = [ + '', + '', + '', + '

              a paragraph

              ', + '
                ', + '
              • List without outer UL
              • ', + '
              • trailing closing p
              • ', + '
              ', + '', + '', + ].join('\n'); + assert.notStrictEqual(cleanedHtml.indexOf(expectedHtml), -1); + return done(); + }); }); - }); - it('can deal with errors', function(done) { - // If the user hasn't configured Tidy, we skip this tests as it's required for this test - if (!Settings.tidyHtml) { - this.skip(); - } + it('can deal with errors', function (done) { + // If the user hasn't configured Tidy, we skip this tests as it's required for this test + if (!Settings.tidyHtml) { + this.skip(); + } - tidy('/some/none/existing/file.html', function(err) { - assert.ok(err); - return done(); + tidy('/some/none/existing/file.html', (err) => { + assert.ok(err); + return done(); + }); }); }); }); diff --git a/tests/backend/specs/caching_middleware.js b/tests/backend/specs/caching_middleware.js new file mode 100644 index 00000000000..e8e14a5cf99 --- /dev/null +++ b/tests/backend/specs/caching_middleware.js @@ -0,0 +1,147 @@ +/** + * caching_middleware is responsible for serving everything under path `/javascripts/` + * That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code + * + */ + +const common = require('../common'); +const settings = require('../../../src/node/utils/Settings'); +const assert = require('assert').strict; +const url = require('url'); +const queryString = require('querystring'); + +let agent; + +/** + * Hack! Returns true if the resource is not plaintext + * The file should start with the callback method, so we need the + * URL. + * + * @param {string} fileContent the response body + * @param {URI} resource resource URI + * @returns {boolean} if it is plaintext + */ +function isPlaintextResponse(fileContent, resource) { + // callback=require.define&v=1234 + const query = url.parse(resource).query; + // require.define + const jsonp = queryString.parse(query).callback; + + // returns true if the first letters in fileContent equal the content of `jsonp` + return fileContent.substring(0, jsonp.length) === jsonp; +} + +/** + * A hack to disable `superagent`'s auto unzip functionality + * + * @param {Request} request + */ +function disableAutoDeflate(request) { + request._shouldUnzip = function () { + return false; + }; +} + +describe(__filename, function () { + const backups = {}; + const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved + const packages = [ + '/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define', + ]; + + before(async function () { + agent = await common.init(); + }); + beforeEach(async function () { + backups.settings = {}; + backups.settings.minify = settings.minify; + }); + afterEach(async function () { + Object.assign(settings, backups.settings); + }); + + context('when minify is false', function () { + before(async function () { + settings.minify = false; + }); + it('gets packages uncompressed without Accept-Encoding gzip', async function () { + await Promise.all(packages.map(async (resource) => agent.get(resource) + .set('Accept-Encoding', fantasyEncoding) + .use(disableAutoDeflate) + .then((res) => { + assert.match(res.header['content-type'], /application\/javascript/); + assert.equal(res.header['content-encoding'], undefined); + assert.equal(isPlaintextResponse(res.text, resource), true); + return; + }))); + }); + + it('gets packages compressed with Accept-Encoding gzip', async function () { + await Promise.all(packages.map(async (resource) => agent.get(resource) + .set('Accept-Encoding', 'gzip') + .use(disableAutoDeflate) + .then((res) => { + assert.match(res.header['content-type'], /application\/javascript/); + assert.equal(res.header['content-encoding'], 'gzip'); + assert.equal(isPlaintextResponse(res.text, resource), false); + return; + }))); + }); + + it('does not cache content-encoding headers', async function () { + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .then((res) => assert.equal(res.header['content-encoding'], undefined)); + await agent.get(packages[0]) + .set('Accept-Encoding', 'gzip') + .then((res) => assert.equal(res.header['content-encoding'], 'gzip')); + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .then((res) => assert.equal(res.header['content-encoding'], undefined)); + }); + }); + + context('when minify is true', function () { + before(async function () { + settings.minify = true; + }); + it('gets packages uncompressed without Accept-Encoding gzip', async function () { + await Promise.all(packages.map(async (resource) => agent.get(resource) + .set('Accept-Encoding', fantasyEncoding) + .use(disableAutoDeflate) + .then((res) => { + assert.match(res.header['content-type'], /application\/javascript/); + assert.equal(res.header['content-encoding'], undefined); + assert.equal(isPlaintextResponse(res.text, resource), true); + return; + }))); + }); + + it('gets packages compressed with Accept-Encoding gzip', async function () { + await Promise.all(packages.map(async (resource) => agent.get(resource) + .set('Accept-Encoding', 'gzip') + .use(disableAutoDeflate) + .then((res) => { + assert.match(res.header['content-type'], /application\/javascript/); + assert.equal(res.header['content-encoding'], 'gzip'); + assert.equal(isPlaintextResponse(res.text, resource), false); + return; + }))); + }); + + it('does not cache content-encoding headers', async function () { + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .then((res) => assert.equal(res.header['content-encoding'], undefined)); + await agent.get(packages[0]) + .set('Accept-Encoding', 'gzip') + .then((res) => assert.equal(res.header['content-encoding'], 'gzip')); + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .then((res) => assert.equal(res.header['content-encoding'], undefined)); + }); + }); +}); diff --git a/tests/backend/specs/contentcollector.js b/tests/backend/specs/contentcollector.js index 7a92c69b124..c859049179e 100644 --- a/tests/backend/specs/contentcollector.js +++ b/tests/backend/specs/contentcollector.js @@ -1,24 +1,34 @@ -const Changeset = require("../../../src/static/js/Changeset"); -const contentcollector = require("../../../src/static/js/contentcollector"); -const AttributePool = require("../../../src/static/js/AttributePool"); -const cheerio = require("../../../src/node_modules/cheerio"); -const util = require('util'); +'use strict'; + +/* eslint-disable max-len */ +/* + * While importexport tests target the `setHTML` API endpoint, which is nearly identical to what happens + * when a user manually imports a document via the UI, the contentcollector tests here don't use rehype to process + * the document. Rehype removes spaces and newĺines were applicable, so the expected results here can + * differ from importexport.js. + * + * If you add tests here, please also add them to importexport.js + */ + +const contentcollector = require('../../../src/static/js/contentcollector'); +const AttributePool = require('../../../src/static/js/AttributePool'); +const cheerio = require('../../../src/node_modules/cheerio'); const tests = { - nestedLi:{ - description: "Complex nested Li", + nestedLi: { + description: 'Complex nested Li', html: '
              1. one
                1. 1.1
              2. two
              ', - expectedLineAttribs : [ - '*0*1*2*3+1+3', '*0*4*2*5+1+3', '*0*1*2*5+1+3' + expectedLineAttribs: [ + '*0*1*2*3+1+3', '*0*4*2*5+1+3', '*0*1*2*5+1+3', ], expectedText: [ - '*one', '*1.1', '*two' - ] + '*one', '*1.1', '*two', + ], }, - complexNest:{ - description: "Complex list of different types", + complexNest: { + description: 'Complex list of different types', html: '
              • one
              • two
              • 0
              • 1
              • 2
                • 3
                • 4
              1. item
                1. item1
                2. item2
              ', - expectedLineAttribs : [ + expectedLineAttribs: [ '*0*1*2+1+3', '*0*1*2+1+3', '*0*1*2+1+1', @@ -28,134 +38,288 @@ const tests = { '*0*3*2+1+1', '*0*4*2*5+1+4', '*0*6*2*7+1+5', - '*0*6*2*7+1+5' + '*0*6*2*7+1+5', ], expectedText: [ - '*one', '*two', - '*0', '*1', - '*2', '*3', - '*4', '*item', - '*item1', '*item2' - ] + '*one', + '*two', + '*0', + '*1', + '*2', + '*3', + '*4', + '*item', + '*item1', + '*item2', + ], }, ul: { - description : "Tests if uls properly get attributes", - html : "
              • a
              • b
              div

              foo

              ", - expectedLineAttribs : [ '*0*1*2+1+1', '*0*1*2+1+1', '+3' , '+3'], - expectedText: ["*a","*b", "div", "foo"] - } - , + description: 'Tests if uls properly get attributes', + html: '
              • a
              • b
              div

              foo

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

              foo

              ", - expectedLineAttribs : [ '*0*1*2+1+1', '*0*3*2+1+1', '*0*1*2+1+1', '+3' ], - expectedText: ["*a","*b", "*a", "foo"] + description: 'Tests if indented uls properly get attributes', + html: '
              • a
                • b
              • a

              foo

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

              test

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

              hello

              1. should be 1
              2. should be 2

              ", - expectedLineAttribs : [ '*0*1*2*3+1+b', '+5', '*0*1*2*4+1+b', '*0*1*2*4+1+b', '' ], - expectedText: ["*should be 1","hello", "*should be 1","*should be 2", ""], - noteToSelf: "Shouldn't include attribute marker in the

              line" - }, - bulletListInOL:{ - description : "A bullet within an OL should not change numbering..", - html : "

              1. should be 1
                • should be a bullet
              2. should be 2

              ", - expectedLineAttribs : [ '*0*1*2*3+1+b', '*0*4*2*3+1+i', '*0*1*2*5+1+b', '' ], - expectedText: ["*should be 1","*should be a bullet","*should be 2", ""] - }, - testP:{ - description : "A single

              should create a new line", - html : "

              ", - expectedLineAttribs : [ '', ''], - expectedText: ["", ""], - noteToSelf: "

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

              test

              ', + expectedLineAttribs: ['*0*1*2*3+1+1', '*0*1*2*3+1+1', '*0*1*2*3+1+1', '+4'], + expectedText: ['*a', '*b', '*c', 'test'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', }, - nestedOl: { - description : "Tests if ols properly get line numbers when in a normal OL", - html : "a
              1. b
                1. c
              notlist

              foo

              ", - expectedLineAttribs : [ '+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1', '+7', '+3' ], - expectedText: ["a","*b","*c", "notlist", "foo"], - noteToSelf: "Ensure empty P does not induce line attribute marker, wont this break the editor?" + lineDoBreakInOl: { + description: 'A single completely empty line break within an ol should reset count if OL is closed off..', + html: '
              1. should be 1

              hello

              1. should be 1
              2. should be 2

              ', + expectedLineAttribs: ['*0*1*2*3+1+b', '+5', '*0*1*2*4+1+b', '*0*1*2*4+1+b', ''], + expectedText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''], + noteToSelf: "Shouldn't include attribute marker in the

              line", + }, + bulletListInOL: { + description: 'A bullet within an OL should not change numbering..', + html: '

              1. should be 1
                • should be a bullet
              2. should be 2

              ', + expectedLineAttribs: ['*0*1*2*3+1+b', '*0*4*2*3+1+i', '*0*1*2*5+1+b', ''], + expectedText: ['*should be 1', '*should be a bullet', '*should be 2', ''], + }, + testP: { + description: 'A single

              should create a new line', + html: '

              ', + expectedLineAttribs: ['', ''], + expectedText: ['', ''], + noteToSelf: '

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

              ", - expectedLineAttribs : [ ], - expectedText: ["*should be 1","*should be 2","*should be 3"], + description: 'Tests if ols properly get line numbers when in a normal OL', + html: 'a
              1. b
                1. c
              notlist

              foo

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

              ', + expectedLineAttribs: [], + expectedText: ['*should be 1', '*should be 2', '*should be 3'], noteToSelf: "

              should create a line break but not break numbering -- This is what I can't get working!", - disabled: true - } - -} - -// For each test.. -for (let test in tests){ - let testObj = tests[test]; - - describe(test, function() { - if(testObj.disabled){ - return xit("DISABLED:", test, function(done){ - done(); - }) - } - - it(testObj.description, function(done) { - var $ = cheerio.load(testObj.html); // Load HTML into Cheerio - var doc = $('html')[0]; // Creates a dom-like representation of HTML - // Create an empty attribute pool - var apool = new AttributePool(); - // Convert a dom tree into a list of lines and attribute liens - // using the content collector object - var cc = contentcollector.makeContentCollector(true, null, apool); - cc.collectContent(doc); - var result = cc.finish(); - var recievedAttributes = result.lineAttribs; - var expectedAttributes = testObj.expectedLineAttribs; - var recievedText = new Array(result.lines) - var expectedText = testObj.expectedText; - - // Check recieved text matches the expected text - if(arraysEqual(recievedText[0], expectedText)){ - // console.log("PASS: Recieved Text did match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText) - }else{ - console.error("FAIL: Recieved Text did not match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText) - throw new Error(); - } + disabled: true, + }, + ignoreAnyTagsOutsideBody: { + description: 'Content outside body should be ignored', + html: 'titleempty
              ', + expectedLineAttribs: ['+5'], + expectedText: ['empty'], + }, + lineWithMultipleSpaces: { + description: 'Multiple spaces should be preserved', + html: 'Text with more than one space.
              ', + expectedLineAttribs: [ '+10' ], + expectedText: ['Text with more than one space.'] + }, + lineWithMultipleNonBreakingAndNormalSpaces: { + description: 'non-breaking and normal space should be preserved', + html: 'Text with  more   than  one space.
              ', + expectedLineAttribs: [ '+10' ], + expectedText: ['Text with more than one space.'] + }, + multiplenbsp: { + description: 'Multiple nbsp should be preserved', + html: '  
              ', + expectedLineAttribs: [ '+2' ], + expectedText: [' '] + }, + multipleNonBreakingSpaceBetweenWords: { + description: 'Multiple nbsp between words ', + html: '  word1  word2   word3
              ', + expectedLineAttribs: [ '+m' ], + expectedText: [' word1 word2 word3'] + }, + nonBreakingSpacePreceededBySpaceBetweenWords: { + description: 'A non-breaking space preceeded by a normal space', + html: '  word1  word2  word3
              ', + expectedLineAttribs: [ '+l' ], + expectedText: [' word1 word2 word3'] + }, + nonBreakingSpaceFollowededBySpaceBetweenWords: { + description: 'A non-breaking space followed by a normal space', + html: '  word1  word2  word3
              ', + expectedLineAttribs: [ '+l' ], + expectedText: [' word1 word2 word3'] + }, + spacesAfterNewline: { + description: 'Don\'t collapse spaces that follow a newline', + html:'something
              something
              ', + expectedLineAttribs: ['+9', '+m'], + expectedText: ['something', ' something'] + }, + spacesAfterNewlineP: { + description: 'Don\'t collapse spaces that follow a empty paragraph', + html:'something

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

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

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

              first line

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

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

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

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

              multiple
              +lines
              +in
              +pre
              +

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

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

              + 1 +

              preline
              +

              `, + expectedLineAttribs: [ '+6', '+7' ], + expectedText: [' 1 ', 'preline'] + }, + dontDeleteSpaceInsideElements: { + description: 'Preserve spaces on the beginning and end of a element', + html: 'Need more space s !
              ', + expectedLineAttribs: ['+f*0+3+1'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceOutsideElements: { + description: 'Preserve spaces outside elements', + html: 'Need more space s !
              ', + expectedLineAttribs: ['+g*0+1+2'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceAtEndOfElement: { + description: 'Preserve spaces at the end of an element', + html: 'Need more space s !
              ', + expectedLineAttribs: ['+g*0+2+1'], + expectedText: ['Need more space s !'] + }, + dontDeleteSpaceAtBeginOfElements: { + description: 'Preserve spaces at the start of an element', + html: 'Need more space s !
              ', + expectedLineAttribs: ['+f*0+2+2'], + expectedText: ['Need more space s !'] + }, +}; - // Check recieved attributes matches the expected attributes - if(arraysEqual(recievedAttributes, expectedAttributes)){ - // console.log("PASS: Recieved Attributes matched Expected Attributes"); - done(); - }else{ - console.error("FAIL", test, testObj.description); - console.error("FAIL: Recieved Attributes did not match Expected Attributes\nRecieved: ", recievedAttributes, "\nExpected: ", expectedAttributes) - console.error("FAILING HTML", testObj.html); - throw new Error(); +describe(__filename, function () { + for (const test of Object.keys(tests)) { + const testObj = tests[test]; + describe(test, function () { + if (testObj.disabled) { + return xit('DISABLED:', test, function (done) { + done(); + }); } - }); - - }); -}; + it(testObj.description, function (done) { + const $ = cheerio.load(testObj.html); // Load HTML into Cheerio + const doc = $('body')[0]; // Creates a dom-like representation of 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); + const result = cc.finish(); + const recievedAttributes = result.lineAttribs; + const expectedAttributes = testObj.expectedLineAttribs; + const recievedText = new Array(result.lines); + const expectedText = testObj.expectedText; + // Check recieved text matches the expected text + if (arraysEqual(recievedText[0], expectedText)) { + // console.log("PASS: Recieved Text did match Expected Text\nRecieved:", recievedText[0], "\nExpected:", testObj.expectedText) + } else { + console.error('FAIL: Recieved Text did not match Expected Text\nRecieved:', recievedText[0], '\nExpected:', testObj.expectedText); + throw new Error(); + } + // Check recieved attributes matches the expected attributes + if (arraysEqual(recievedAttributes, expectedAttributes)) { + // console.log("PASS: Recieved Attributes matched Expected Attributes"); + done(); + } else { + console.error('FAIL', test, testObj.description); + console.error('FAIL: Recieved Attributes did not match Expected Attributes\nRecieved: ', recievedAttributes, '\nExpected: ', expectedAttributes); + console.error('FAILING HTML', testObj.html); + throw new Error(); + } + }); + }); + } +}); function arraysEqual(a, b) { @@ -168,7 +332,7 @@ function arraysEqual(a, b) { // Please note that calling sort on an array will modify that array. // you might want to clone your array first. - for (var i = 0; i < a.length; ++i) { + for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; diff --git a/tests/backend/specs/hooks.js b/tests/backend/specs/hooks.js new file mode 100644 index 00000000000..0e0f8075f75 --- /dev/null +++ b/tests/backend/specs/hooks.js @@ -0,0 +1,891 @@ +/* global __dirname, __filename, afterEach, beforeEach, describe, it, process, require */ + +function m(mod) { return `${__dirname}/../../../src/${mod}`; } + +const assert = require('assert').strict; +const common = require('../common'); +const hooks = require(m('static/js/pluginfw/hooks')); +const plugins = require(m('static/js/pluginfw/plugin_defs')); +const sinon = require(m('node_modules/sinon')); + +const logger = common.logger; + +describe(__filename, function () { + const hookName = 'testHook'; + const hookFnName = 'testPluginFileName:testHookFunctionName'; + let testHooks; // Convenience shorthand for plugins.hooks[hookName]. + let hook; // Convenience shorthand for plugins.hooks[hookName][0]. + + beforeEach(async function () { + // Make sure these are not already set so that we don't accidentally step on someone else's + // toes: + assert(plugins.hooks[hookName] == null); + assert(hooks.deprecationNotices[hookName] == null); + assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null); + + // Many of the tests only need a single registered hook function. Set that up here to reduce + // boilerplate. + hook = makeHook(); + plugins.hooks[hookName] = [hook]; + testHooks = plugins.hooks[hookName]; + }); + + afterEach(async function () { + sinon.restore(); + delete plugins.hooks[hookName]; + delete hooks.deprecationNotices[hookName]; + delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; + }); + + const makeHook = (ret) => ({ + hook_name: hookName, + // Many tests will likely want to change this. Unfortunately, we can't use a convenience + // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and + // change behavior depending on the number of parameters. + hook_fn: (hn, ctx, cb) => cb(ret), + hook_fn_name: hookFnName, + part: {plugin: 'testPluginName'}, + }); + + // Hook functions that should work for both synchronous and asynchronous hooks. + const supportedSyncHookFunctions = [ + { + name: 'return non-Promise value, with callback parameter', + fn: (hn, ctx, cb) => 'val', + want: 'val', + syncOk: true, + }, + { + name: 'return non-Promise value, without callback parameter', + fn: (hn, ctx) => 'val', + want: 'val', + syncOk: true, + }, + { + name: 'return undefined, without callback parameter', + fn: (hn, ctx) => {}, + want: undefined, + syncOk: true, + }, + { + name: 'pass non-Promise value to callback', + fn: (hn, ctx, cb) => { cb('val'); }, + want: 'val', + syncOk: true, + }, + { + name: 'pass undefined to callback', + fn: (hn, ctx, cb) => { cb(); }, + want: undefined, + syncOk: true, + }, + { + name: 'return the value returned from the callback', + fn: (hn, ctx, cb) => cb('val'), + want: 'val', + syncOk: true, + }, + { + name: 'throw', + fn: (hn, ctx, cb) => { throw new Error('test exception'); }, + wantErr: 'test exception', + syncOk: true, + }, + ]; + + describe('callHookFnSync', function () { + const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand. + + describe('basic behavior', function () { + it('passes hook name', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + callHookFnSync(hook); + }); + + it('passes context', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + callHookFnSync(hook, val); + } + }); + + it('returns the value provided to the callback', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + assert.equal(callHookFnSync(hook, val), val); + } + }); + + it('returns the value returned by the hook function', async function () { + for (const val of ['value', null, undefined]) { + // Must not have the cb parameter otherwise returning undefined will error. + hook.hook_fn = (hn, ctx) => ctx; + assert.equal(callHookFnSync(hook, val), val); + } + }); + + it('does not catch exceptions', async function () { + hook.hook_fn = () => { throw new Error('test exception'); }; + assert.throws(() => callHookFnSync(hook), {message: 'test exception'}); + }); + + it('callback returns undefined', async function () { + hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + callHookFnSync(hook); + }); + + it('checks for deprecation', async function () { + sinon.stub(console, 'warn'); + hooks.deprecationNotices[hookName] = 'test deprecation'; + callHookFnSync(hook); + assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + assert.equal(console.warn.callCount, 1); + assert.match(console.warn.getCall(0).args[0], /test deprecation/); + }); + }); + + describe('supported hook function styles', function () { + for (const tc of supportedSyncHookFunctions) { + it(tc.name, async function () { + sinon.stub(console, 'warn'); + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + const call = () => callHookFnSync(hook); + if (tc.wantErr) { + assert.throws(call, {message: tc.wantErr}); + } else { + assert.equal(call(), tc.want); + } + assert.equal(console.warn.callCount, 0); + assert.equal(console.error.callCount, 0); + }); + } + }); + + describe('bad hook function behavior (other than double settle)', function () { + const promise1 = Promise.resolve('val1'); + const promise2 = Promise.resolve('val2'); + + const testCases = [ + { + name: 'never settles -> buggy hook detected', + // Note that returning undefined without calling the callback is permitted if the function + // has 2 or fewer parameters, so this test function must have 3 parameters. + fn: (hn, ctx, cb) => {}, + wantVal: undefined, + wantError: /UNSETTLED FUNCTION BUG/, + }, + { + name: 'returns a Promise -> buggy hook detected', + fn: () => promise1, + wantVal: promise1, + wantError: /PROHIBITED PROMISE BUG/, + }, + { + name: 'passes a Promise to cb -> buggy hook detected', + fn: (hn, ctx, cb) => cb(promise2), + wantVal: promise2, + wantError: /PROHIBITED PROMISE BUG/, + }, + ]; + + for (const tc of testCases) { + it(tc.name, async function () { + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + assert.equal(callHookFnSync(hook), tc.wantVal); + assert.equal(console.error.callCount, tc.wantError ? 1 : 0); + if (tc.wantError) assert.match(console.error.getCall(0).args[0], tc.wantError); + }); + } + }); + + // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second + // time, or call the callback and then return a value.) + describe('bad hook function behavior (double settle)', function () { + beforeEach(function () { + sinon.stub(console, 'error'); + }); + + // Each item in this array codifies a way to settle a synchronous hook function. Each of the + // test cases below combines two of these behaviors in a single hook function and confirms + // that callHookFnSync both (1) returns the result of the first settle attempt, and + // (2) detects the second settle attempt. + const behaviors = [ + { + name: 'throw', + fn: (cb, err, val) => { throw err; }, + rejects: true, + }, + { + name: 'return value', + fn: (cb, err, val) => val, + }, + { + name: 'immediately call cb(value)', + fn: (cb, err, val) => cb(val), + }, + { + name: 'defer call to cb(value)', + fn: (cb, err, val) => { process.nextTick(cb, val); }, + async: true, + }, + ]; + + for (const step1 of behaviors) { + // There can't be a second step if the first step is to return or throw. + if (step1.name.startsWith('return ') || step1.name === 'throw') continue; + for (const step2 of behaviors) { + // If step1 and step2 are both async then there would be three settle attempts (first an + // erroneous unsettled return, then async step 1, then async step 2). Handling triple + // settle would complicate the tests, and it is sufficient to test only double settles. + if (step1.async && step2.async) continue; + + it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, new Error(ctx.ret1), ctx.ret1); + return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); + }; + + // Temporarily remove unhandled error listeners so that the errors we expect to see + // don't trigger a test failure (or terminate node). + const events = ['uncaughtException', 'unhandledRejection']; + const listenerBackups = {}; + for (const event of events) { + listenerBackups[event] = process.rawListeners(event); + process.removeAllListeners(event); + } + + // We should see an asynchronous error (either an unhandled Promise rejection or an + // uncaught exception) if and only if one of the two steps was asynchronous or there was + // a throw (in which case the double settle is deferred so that the caller sees the + // original error). + const wantAsyncErr = step1.async || step2.async || step2.rejects; + let tempListener; + let asyncErr; + try { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err) => { + assert.equal(asyncErr, undefined); + asyncErr = err; + resolve(); + }; + if (!wantAsyncErr) resolve(); + }); + events.forEach((event) => process.on(event, tempListener)); + const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'}); + if (step2.rejects) { + assert.throws(call, {message: 'val2'}); + } else if (!step1.async && !step2.async) { + assert.throws(call, {message: /DOUBLE SETTLE BUG/}); + } else { + assert.equal(call(), step1.async ? 'val2' : 'val1'); + } + await seenErrPromise; + } finally { + // Restore the original listeners. + for (const event of events) { + process.off(event, tempListener); + for (const listener of listenerBackups[event]) { + process.on(event, listener); + } + } + } + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + if (wantAsyncErr) { + assert(asyncErr instanceof Error); + assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); + } + }); + + // This next test is the same as the above test, except the second settle attempt is for + // the same outcome. The two outcomes can't be the same if one step throws and the other + // doesn't, so skip those cases. + if (step1.rejects !== step2.rejects) continue; + + it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { + const err = new Error('val'); + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, err, 'val'); + return step2.fn(cb, err, 'val'); + }; + + const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); + const call = () => callHookFnSync(hook); + if (step2.rejects) { + assert.throws(call, {message: 'val'}); + } else { + assert.equal(call(), 'val'); + } + await errorLogged; + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + }); + } + } + }); + }); + + describe('hooks.callAll', function () { + describe('basic behavior', function () { + it('calls all in order', async function () { + testHooks.length = 0; + testHooks.push(makeHook(1), makeHook(2), makeHook(3)); + assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]); + }); + + it('passes hook name', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hooks.callAll(hookName); + }); + + it('undefined context -> {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callAll(hookName); + }); + + it('null context -> {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callAll(hookName, null); + }); + + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hooks.callAll(hookName, wantContext); + }); + }); + + describe('result processing', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks.testHook; + assert.deepEqual(hooks.callAll(hookName), []); + }); + + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(hooks.callAll(hookName), []); + }); + + it('flattens one level', async function () { + testHooks.length = 0; + testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); + assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]); + }); + + it('filters out undefined', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); + assert.deepEqual(hooks.callAll(hookName), [2, [3]]); + }); + + it('preserves null', async function () { + testHooks.length = 0; + testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); + assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); + }); + + it('all undefined -> []', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook()); + assert.deepEqual(hooks.callAll(hookName), []); + }); + }); + }); + + describe('callHookFnAsync', function () { + const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. + + describe('basic behavior', function () { + it('passes hook name', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + await callHookFnAsync(hook); + }); + + it('passes context', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + await callHookFnAsync(hook, val); + } + }); + + it('returns the value provided to the callback', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + assert.equal(await callHookFnAsync(hook, val), val); + assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); + } + }); + + it('returns the value returned by the hook function', async function () { + for (const val of ['value', null, undefined]) { + // Must not have the cb parameter otherwise returning undefined will never resolve. + hook.hook_fn = (hn, ctx) => ctx; + assert.equal(await callHookFnAsync(hook, val), val); + assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); + } + }); + + it('rejects if it throws an exception', async function () { + hook.hook_fn = () => { throw new Error('test exception'); }; + await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); + }); + + it('rejects if rejected Promise passed to callback', async function () { + hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); + await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); + }); + + it('rejects if rejected Promise returned', async function () { + hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); + await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); + }); + + it('callback returns undefined', async function () { + hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + await callHookFnAsync(hook); + }); + + it('checks for deprecation', async function () { + sinon.stub(console, 'warn'); + hooks.deprecationNotices[hookName] = 'test deprecation'; + await callHookFnAsync(hook); + assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + assert.equal(console.warn.callCount, 1); + assert.match(console.warn.getCall(0).args[0], /test deprecation/); + }); + }); + + describe('supported hook function styles', function () { + const supportedHookFunctions = supportedSyncHookFunctions.concat([ + { + name: 'legacy async cb', + fn: (hn, ctx, cb) => { process.nextTick(cb, 'val'); }, + want: 'val', + }, + // Already resolved Promises: + { + name: 'return resolved Promise, with callback parameter', + fn: (hn, ctx, cb) => Promise.resolve('val'), + want: 'val', + }, + { + name: 'return resolved Promise, without callback parameter', + fn: (hn, ctx) => Promise.resolve('val'), + want: 'val', + }, + { + name: 'pass resolved Promise to callback', + fn: (hn, ctx, cb) => { cb(Promise.resolve('val')); }, + want: 'val', + }, + // Not yet resolved Promises: + { + name: 'return unresolved Promise, with callback parameter', + fn: (hn, ctx, cb) => new Promise((resolve) => process.nextTick(resolve, 'val')), + want: 'val', + }, + { + name: 'return unresolved Promise, without callback parameter', + fn: (hn, ctx) => new Promise((resolve) => process.nextTick(resolve, 'val')), + want: 'val', + }, + { + name: 'pass unresolved Promise to callback', + fn: (hn, ctx, cb) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, + want: 'val', + }, + // Already rejected Promises: + { + name: 'return rejected Promise, with callback parameter', + fn: (hn, ctx, cb) => Promise.reject(new Error('test rejection')), + wantErr: 'test rejection', + }, + { + name: 'return rejected Promise, without callback parameter', + fn: (hn, ctx) => Promise.reject(new Error('test rejection')), + wantErr: 'test rejection', + }, + { + name: 'pass rejected Promise to callback', + fn: (hn, ctx, cb) => { cb(Promise.reject(new Error('test rejection'))); }, + wantErr: 'test rejection', + }, + // Not yet rejected Promises: + { + name: 'return unrejected Promise, with callback parameter', + fn: (hn, ctx, cb) => new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + }), + wantErr: 'test rejection', + }, + { + name: 'return unrejected Promise, without callback parameter', + fn: (hn, ctx) => new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + }), + wantErr: 'test rejection', + }, + { + name: 'pass unrejected Promise to callback', + fn: (hn, ctx, cb) => { + cb(new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + })); + }, + wantErr: 'test rejection', + }, + ]); + + for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { + it(tc.name, async function () { + sinon.stub(console, 'warn'); + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + const p = callHookFnAsync(hook); + if (tc.wantErr) { + await assert.rejects(p, {message: tc.wantErr}); + } else { + assert.equal(await p, tc.want); + } + assert.equal(console.warn.callCount, 0); + assert.equal(console.error.callCount, 0); + }); + } + }); + + // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second + // time, or call the callback and then return a value.) + describe('bad hook function behavior (double settle)', function () { + beforeEach(function () { + sinon.stub(console, 'error'); + }); + + // Each item in this array codifies a way to settle an asynchronous hook function. Each of the + // test cases below combines two of these behaviors in a single hook function and confirms + // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2) + // detects the second settle attempt. + // + // The 'when' property specifies the relative time that two behaviors will cause the hook + // function to settle: + // * If behavior1.when <= behavior2.when and behavior1 is called before behavior2 then + // behavior1 will settle the hook function before behavior2. + // * Otherwise, behavior2 will settle the hook function before behavior1. + const behaviors = [ + { + name: 'throw', + fn: (cb, err, val) => { throw err; }, + rejects: true, + when: 0, + }, + { + name: 'return value', + fn: (cb, err, val) => val, + // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw' + // immediately settles the hook function, whereas the 'return value' case is settled by a + // .then() function attached to a Promise. EcmaScript guarantees that a .then() function + // attached to a Promise is enqueued on the event loop (not executed immediately) when the + // Promise settles. + when: 1, + }, + { + name: 'immediately call cb(value)', + fn: (cb, err, val) => cb(val), + // This behavior has the same relative time as the 'return value' case because it too is + // settled by a .then() function attached to a Promise. + when: 1, + }, + { + name: 'return resolvedPromise', + fn: (cb, err, val) => Promise.resolve(val), + // This behavior has the same relative time as the 'return value' case because the return + // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees + // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value), + // so returning an already resolved Promise vs. returning a non-Promise value are + // equivalent. + when: 1, + }, + { + name: 'immediately call cb(resolvedPromise)', + fn: (cb, err, val) => cb(Promise.resolve(val)), + when: 1, + }, + { + name: 'return rejectedPromise', + fn: (cb, err, val) => Promise.reject(err), + rejects: true, + when: 1, + }, + { + name: 'immediately call cb(rejectedPromise)', + fn: (cb, err, val) => cb(Promise.reject(err)), + rejects: true, + when: 1, + }, + { + name: 'return unresolvedPromise', + fn: (cb, err, val) => new Promise((resolve) => process.nextTick(resolve, val)), + when: 2, + }, + { + name: 'immediately call cb(unresolvedPromise)', + fn: (cb, err, val) => cb(new Promise((resolve) => process.nextTick(resolve, val))), + when: 2, + }, + { + name: 'return unrejectedPromise', + fn: (cb, err, val) => new Promise((resolve, reject) => process.nextTick(reject, err)), + rejects: true, + when: 2, + }, + { + name: 'immediately call cb(unrejectedPromise)', + fn: (cb, err, val) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), + rejects: true, + when: 2, + }, + { + name: 'defer call to cb(value)', + fn: (cb, err, val) => { process.nextTick(cb, val); }, + when: 2, + }, + { + name: 'defer call to cb(resolvedPromise)', + fn: (cb, err, val) => { process.nextTick(cb, Promise.resolve(val)); }, + when: 2, + }, + { + name: 'defer call to cb(rejectedPromise)', + fn: (cb, err, val) => { process.nextTick(cb, Promise.reject(err)); }, + rejects: true, + when: 2, + }, + { + name: 'defer call to cb(unresolvedPromise)', + fn: (cb, err, val) => { + process.nextTick(() => { + cb(new Promise((resolve) => process.nextTick(resolve, val))); + }); + }, + when: 3, + }, + { + name: 'defer call cb(unrejectedPromise)', + fn: (cb, err, val) => { + process.nextTick(() => { + cb(new Promise((resolve, reject) => process.nextTick(reject, err))); + }); + }, + rejects: true, + when: 3, + }, + ]; + + for (const step1 of behaviors) { + // There can't be a second step if the first step is to return or throw. + if (step1.name.startsWith('return ') || step1.name === 'throw') continue; + for (const step2 of behaviors) { + it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, new Error(ctx.ret1), ctx.ret1); + return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); + }; + + // Temporarily remove unhandled Promise rejection listeners so that the unhandled + // rejections we expect to see don't trigger a test failure (or terminate node). + const event = 'unhandledRejection'; + const listenersBackup = process.rawListeners(event); + process.removeAllListeners(event); + + let tempListener; + let asyncErr; + try { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err) => { + assert.equal(asyncErr, undefined); + asyncErr = err; + resolve(); + }; + }); + process.on(event, tempListener); + const step1Wins = step1.when <= step2.when; + const winningStep = step1Wins ? step1 : step2; + const winningVal = step1Wins ? 'val1' : 'val2'; + const p = callHookFnAsync(hook, {ret1: 'val1', ret2: 'val2'}); + if (winningStep.rejects) { + await assert.rejects(p, {message: winningVal}); + } else { + assert.equal(await p, winningVal); + } + await seenErrPromise; + } finally { + // Restore the original listeners. + process.off(event, tempListener); + for (const listener of listenersBackup) { + process.on(event, listener); + } + } + assert.equal(console.error.callCount, 1, + `Got errors:\n${ + console.error.getCalls().map((call) => call.args[0]).join('\n')}`); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + assert(asyncErr instanceof Error); + assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); + }); + + // This next test is the same as the above test, except the second settle attempt is for + // the same outcome. The two outcomes can't be the same if one step rejects and the other + // doesn't, so skip those cases. + if (step1.rejects !== step2.rejects) continue; + + it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { + const err = new Error('val'); + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, err, 'val'); + return step2.fn(cb, err, 'val'); + }; + const winningStep = (step1.when <= step2.when) ? step1 : step2; + const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); + const p = callHookFnAsync(hook); + if (winningStep.rejects) { + await assert.rejects(p, {message: 'val'}); + } else { + assert.equal(await p, 'val'); + } + await errorLogged; + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + }); + } + } + }); + }); + + describe('hooks.aCallAll', function () { + describe('basic behavior', function () { + it('calls all asynchronously, returns values in order', async function () { + testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. + let nextIndex = 0; + const hookPromises = []; + const hookStarted = []; + const hookFinished = []; + const makeHook = () => { + const i = nextIndex++; + const entry = {}; + hookStarted[i] = false; + hookFinished[i] = false; + hookPromises[i] = entry; + entry.promise = new Promise((resolve) => { + entry.resolve = () => { + hookFinished[i] = true; + resolve(i); + }; + }); + return {hook_fn: () => { + hookStarted[i] = true; + return entry.promise; + }}; + }; + testHooks.push(makeHook(), makeHook()); + const p = hooks.aCallAll(hookName); + assert.deepEqual(hookStarted, [true, true]); + assert.deepEqual(hookFinished, [false, false]); + hookPromises[1].resolve(); + await hookPromises[1].promise; + assert.deepEqual(hookFinished, [false, true]); + hookPromises[0].resolve(); + assert.deepEqual(await p, [0, 1]); + }); + + it('passes hook name', async function () { + hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + await hooks.aCallAll(hookName); + }); + + it('undefined context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallAll(hookName); + }); + + it('null context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallAll(hookName, null); + }); + + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + await hooks.aCallAll(hookName, wantContext); + }); + }); + + describe('aCallAll callback', function () { + it('exception in callback rejects', async function () { + const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); + await assert.rejects(p, {message: 'test exception'}); + }); + + it('propagates error on exception', async function () { + hook.hook_fn = () => { throw new Error('test exception'); }; + await hooks.aCallAll(hookName, {}, (err) => { + assert(err instanceof Error); + assert.equal(err.message, 'test exception'); + }); + }); + + it('propagages null error on success', async function () { + await hooks.aCallAll(hookName, {}, (err) => { + assert(err == null, `got non-null error: ${err}`); + }); + }); + + it('propagages results on success', async function () { + hook.hook_fn = () => 'val'; + await hooks.aCallAll(hookName, {}, (err, results) => { + assert.deepEqual(results, ['val']); + }); + }); + + it('returns callback return value', async function () { + assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); + }); + }); + + describe('result processing', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks[hookName]; + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); + + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); + + it('flattens one level', async function () { + testHooks.length = 0; + testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); + assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]); + }); + + it('filters out undefined', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); + assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]); + }); + + it('preserves null', async function () { + testHooks.length = 0; + testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); + assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]); + }); + + it('all undefined -> []', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook(Promise.resolve())); + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); + }); + }); +}); diff --git a/tests/backend/specs/promises.js b/tests/backend/specs/promises.js new file mode 100644 index 00000000000..02c656d4574 --- /dev/null +++ b/tests/backend/specs/promises.js @@ -0,0 +1,87 @@ +function m(mod) { return `${__dirname}/../../../src/${mod}`; } + +const assert = require('assert').strict; +const promises = require(m('node/utils/promises')); + +describe(__filename, function () { + describe('promises.timesLimit', function () { + let wantIndex = 0; + const testPromises = []; + const makePromise = (index) => { + // Make sure index increases by one each time. + assert.equal(index, wantIndex++); + // Save the resolve callback (so the test can trigger resolution) + // and the promise itself (to wait for resolve to take effect). + const p = {}; + const promise = new Promise((resolve) => { + p.resolve = resolve; + }); + p.promise = promise; + testPromises.push(p); + return p.promise; + }; + + const total = 11; + const concurrency = 7; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + + it('honors concurrency', async function () { + assert.equal(wantIndex, concurrency); + }); + + it('creates another when one completes', async function () { + const {promise, resolve} = testPromises.shift(); + resolve(); + await promise; + assert.equal(wantIndex, concurrency + 1); + }); + + it('creates the expected total number of promises', async function () { + while (testPromises.length > 0) { + // Resolve them in random order to ensure that the resolution order doesn't matter. + const i = Math.floor(Math.random() * Math.floor(testPromises.length)); + const {promise, resolve} = testPromises.splice(i, 1)[0]; + resolve(); + await promise; + } + assert.equal(wantIndex, total); + }); + + it('resolves', async function () { + await timesLimitPromise; + }); + + it('does not create too many promises if total < concurrency', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + const total = 7; + const concurrency = 11; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + while (testPromises.length > 0) { + const {promise, resolve} = testPromises.pop(); + resolve(); + await promise; + } + await timesLimitPromise; + assert.equal(wantIndex, total); + }); + + it('accepts total === 0, concurrency > 0', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, concurrency, makePromise); + assert.equal(wantIndex, 0); + }); + + it('accepts total === 0, concurrency === 0', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, 0, makePromise); + assert.equal(wantIndex, 0); + }); + + it('rejects total > 0, concurrency === 0', async function () { + await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); + }); + }); +}); diff --git a/tests/backend/specs/socketio.js b/tests/backend/specs/socketio.js index 9db6bdbeb98..6f3af2ee04a 100644 --- a/tests/backend/specs/socketio.js +++ b/tests/backend/specs/socketio.js @@ -1,52 +1,14 @@ -function m(mod) { return __dirname + '/../../../src/' + mod; } +'use strict'; const assert = require('assert').strict; -const db = require(m('node/db/DB')); -const express = require(m('node_modules/express')); -const http = require('http'); -const log4js = require(m('node_modules/log4js')); -let padManager; -const plugins = require(m('static/js/pluginfw/plugin_defs')); -const setCookieParser = require(m('node_modules/set-cookie-parser')); -const settings = require(m('node/utils/Settings')); -const io = require(m('node_modules/socket.io-client')); -const stats = require(m('node/stats')); -const supertest = require(m('node_modules/supertest')); -const util = require('util'); - -const logger = log4js.getLogger('test'); -const app = express(); -const server = http.createServer(app); -let client; -let baseUrl; - -before(async () => { - await util.promisify(server.listen).bind(server)(0, 'localhost'); - baseUrl = `http://localhost:${server.address().port}`; - logger.debug(`HTTP server at ${baseUrl}`); - client = supertest(baseUrl); - const npm = require(m('node_modules/npm/lib/npm.js')); - await util.promisify(npm.load)(); - settings.users = { - admin: {password: 'admin-password', is_admin: true}, - user: {password: 'user-password'}, - }; - await db.init(); - padManager = require(m('node/db/PadManager')); - const webaccess = require(m('node/hooks/express/webaccess')); - webaccess.expressConfigure('expressConfigure', {app}); - const socketio = require(m('node/hooks/express/socketio')); - socketio.expressCreateServer('expressCreateServer', {app, server}); - app.get(/./, (req, res) => { res.status(200).send('OK'); }); -}); +const common = require('../common'); +const io = require('ep_etherpad-lite/node_modules/socket.io-client'); +const padManager = require('ep_etherpad-lite/node/db/PadManager'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const setCookieParser = require('ep_etherpad-lite/node_modules/set-cookie-parser'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); -after(async () => { - stats.end(); - await Promise.all([ - db.doShutdown(), - util.promisify(server.close).bind(server)(), - ]); -}); +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). @@ -73,7 +35,7 @@ const getSocketEvent = async (socket, event) => { 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); @@ -86,12 +48,11 @@ const getSocketEvent = async (socket, event) => { 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]) => { - return `${name}=${encodeURIComponent(cookie.value)}`; - }).join('; '); + const reqCookieHdr = Object.entries(resCookies).map( + ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); logger.debug('socket.io connecting...'); - const socket = io(`${baseUrl}/`, { + 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 @@ -118,7 +79,6 @@ const handshake = async (socket, padID) => { type: 'CLIENT_READY', padId: padID, sessionID: null, - password: null, token: 't.12345', protocolVersion: 2, }); @@ -128,70 +88,270 @@ const handshake = async (socket, padID) => { return msg; }; -describe('socket.io access checks', () => { +describe(__filename, function () { + let agent; + let authorize; + const backups = {}; + const cleanUpPads = async () => { + const padIds = ['pad', 'other-pad', 'päd']; + await Promise.all(padIds.map(async (padId) => { + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + })); + }; let socket; - beforeEach(async () => { - assert(socket == null); + + before(async function () { agent = await common.init(); }); + beforeEach(async function () { + backups.hooks = {}; + for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) { + backups.hooks[hookName] = plugins.hooks[hookName]; + plugins.hooks[hookName] = []; + } + backups.settings = {}; + for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) { + backups.settings[setting] = settings[setting]; + } + settings.editOnly = false; settings.requireAuthentication = false; settings.requireAuthorization = false; - Promise.all(['pad', 'other-pad'].map(async (pad) => { - if (await padManager.doesPadExist(pad)) (await padManager.getPad(pad)).remove(); - })); + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; + assert(socket == null); + authorize = () => true; + plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + await cleanUpPads(); }); - afterEach(async () => { + afterEach(async function () { if (socket) socket.close(); socket = null; + await cleanUpPads(); + Object.assign(plugins.hooks, backups.hooks); + Object.assign(settings, backups.settings); }); - // Normal accesses. - it('!authn anonymous /p/pad -> 200, ok', async () => { - const res = await client.get('/p/pad').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('!authn user /p/pad -> 200, ok', async () => { - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); + describe('Normal accesses', function () { + it('!authn anonymous cookie /p/pad -> 200, ok', async function () { + const res = await agent.get('/p/pad').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('!authn !cookie -> ok', async function () { + socket = await connect(null); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('!authn user /p/pad -> 200, ok', async function () { + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('authn user /p/pad -> 200, ok', async function () { + 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'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('authz user /p/pad -> 200, ok', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('supports pad names with characters that must be percent-encoded', async function () { + settings.requireAuthentication = true; + // requireAuthorization is set to true here to guarantee that the user's padAuthorizations + // object is populated. Technically this isn't necessary because the user's padAuthorizations + // is currently populated even if requireAuthorization is false, but setting this to true + // ensures the test remains useful if the implementation ever changes. + settings.requireAuthorization = true; + const encodedPadId = encodeURIComponent('päd'); + const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'päd'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); }); - it('authn user /p/pad -> 200, ok', async () => { - settings.requireAuthentication = true; - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); - // Should not throw. - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); + + describe('Abnormal access attempts', function () { + it('authn anonymous /p/pad -> 401, error', async function () { + settings.requireAuthentication = true; + const res = await agent.get('/p/pad').expect(401); + // Despite the 401, try to create the pad via a socket.io connection anyway. + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('authn !cookie -> error', async function () { + settings.requireAuthentication = true; + socket = await connect(null); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('authorization bypass attempt -> error', async function () { + // 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); + // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. + const message = await handshake(socket, 'other-pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); - // Abnormal access attempts. - it('authn anonymous /p/pad -> 401, error', async () => { - settings.requireAuthentication = true; - const res = await client.get('/p/pad').expect(401); - // Despite the 401, try to create the pad via a socket.io connection anyway. - await assert.rejects(connect(res), {message: /authentication required/i}); + describe('Authorization levels via authorize hook', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it("level='create' -> can create", async function () { + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('level=true -> can create', async function () { + authorize = () => true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='modify' -> can modify", async function () { + await padManager.getPad('pad'); // Create the pad. + authorize = () => 'modify'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='create' settings.editOnly=true -> unable to create", async function () { + authorize = () => 'create'; + settings.editOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='modify' settings.editOnly=false -> unable to create", async function () { + authorize = () => 'modify'; + settings.editOnly = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='readOnly' -> unable to create", async function () { + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it("level='readOnly' -> unable to modify", async function () { + await padManager.getPad('pad'); // Create the pad. + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); }); - it('socket.io connection without express-session cookie -> error', async () => { - settings.requireAuthentication = true; - await assert.rejects(connect(null), {message: /signed express_sid cookie is required/i}); + + describe('Authorization levels via user settings', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + }); + + it('user.canCreate = true -> can create and modify', async function () { + settings.users.user.canCreate = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('user.canCreate = false -> unable to create', async function () { + settings.users.user.canCreate = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user.readOnly = true -> unable to create', async function () { + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user.readOnly = true -> unable to modify', async function () { + await padManager.getPad('pad'); // Create the pad. + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); + it('user.readOnly = false -> can create and modify', async function () { + settings.users.user.readOnly = false; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const clientVars = await handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('user.readOnly = true, user.canCreate = true -> unable to create', async function () { + settings.users.user.canCreate = true; + settings.users.user.readOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); - it('authorization bypass attempt -> error', async () => { - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => { - if (req.session.user == null) return cb([]); // Hasn't authenticated yet. - // Only allowed to access /p/pad. - return cb([req.path === '/p/pad']); - }}]; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - // First authenticate and establish a session. - const res = await client.get('/p/pad').auth('user', 'user-password').expect(200); - // Connecting should work because the user successfully authenticated. - socket = await connect(res); - // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. - const message = await handshake(socket, 'other-pad'); - assert.equal(message.accessStatus, 'deny'); + + describe('Authorization level interaction between authorize hook and user settings', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it('authorize hook does not elevate level from user settings', async function () { + settings.users.user.readOnly = true; + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('user settings does not elevate level from authorize hook', async function () { + settings.users.user.readOnly = false; + settings.users.user.canCreate = true; + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await connect(res); + const message = await handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); }); }); diff --git a/tests/backend/specs/specialpages.js b/tests/backend/specs/specialpages.js new file mode 100644 index 00000000000..3e385f6e9b8 --- /dev/null +++ b/tests/backend/specs/specialpages.js @@ -0,0 +1,25 @@ +const common = require('../common'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); + +describe(__filename, function () { + let agent; + const backups = {}; + before(async function () { agent = await common.init(); }); + beforeEach(async function () { + backups.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization']) { + backups.settings[setting] = settings[setting]; + } + settings.requireAuthentication = false; + settings.requireAuthorization = false; + }); + afterEach(async function () { + Object.assign(settings, backups.settings); + }); + + describe('/javascript', function () { + it('/javascript -> 200', async function () { + await agent.get('/javascript').expect(200); + }); + }); +}); diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js new file mode 100644 index 00000000000..a21cc73a8c4 --- /dev/null +++ b/tests/backend/specs/webaccess.js @@ -0,0 +1,473 @@ +/* global __dirname, __filename, Buffer, afterEach, before, beforeEach, describe, it, require */ + +function m(mod) { return `${__dirname}/../../../src/${mod}`; } + +const assert = require('assert').strict; +const common = require('../common'); +const plugins = require(m('static/js/pluginfw/plugin_defs')); +const settings = require(m('node/utils/Settings')); + +describe(__filename, function () { + let agent; + const backups = {}; + const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; + const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; + before(async function () { agent = await common.init(); }); + beforeEach(async function () { + backups.hooks = {}; + for (const hookName of authHookNames.concat(failHookNames)) { + backups.hooks[hookName] = plugins.hooks[hookName]; + plugins.hooks[hookName] = []; + } + backups.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) { + backups.settings[setting] = settings[setting]; + } + settings.requireAuthentication = false; + settings.requireAuthorization = false; + settings.users = { + admin: {password: 'admin-password', is_admin: true}, + user: {password: 'user-password'}, + }; + }); + afterEach(async function () { + Object.assign(plugins.hooks, backups.hooks); + Object.assign(settings, backups.settings); + }); + + describe('webaccess: without plugins', function () { + it('!authn !authz anonymous / -> 200', async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/').expect(200); + }); + it('!authn !authz anonymous /admin/ -> 401', async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/admin/').expect(401); + }); + it('authn !authz anonymous / -> 401', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').expect(401); + }); + it('authn !authz user / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + }); + it('authn !authz user /admin/ -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn !authz admin / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn !authz admin /admin/ -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz user / -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('user', 'user-password').expect(403); + }); + it('authn authz user /admin/ -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn authz admin / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz admin /admin/ -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); + + describe('login fails if password is nullish', function () { + for (const adminPassword of [undefined, null]) { + // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as + // base64(username + ':' + password), but there's nothing stopping a malicious user from + // sending just base64(username) (no colon). The lack of colon could throw off credential + // parsing, resulting in successful comparisons against a null or undefined password. + for (const creds of ['admin', 'admin:']) { + it(`admin password: ${adminPassword} credentials: ${creds}`, async function () { + settings.users.admin.password = adminPassword; + const encCreds = Buffer.from(creds).toString('base64'); + await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); + }); + } + } + }); + }); + + describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { + let callOrder; + const Handler = class { + constructor(hookName, suffix) { + this.called = false; + this.hookName = hookName; + this.innerHandle = () => []; + this.id = hookName + suffix; + this.checkContext = () => {}; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(context.next != null); + this.checkContext(context); + assert(!this.called); + this.called = true; + callOrder.push(this.id); + return cb(this.innerHandle(context.req)); + } + }; + const handlers = {}; + + beforeEach(async function () { + callOrder = []; + for (const hookName of authHookNames) { + // Create two handlers for each hook to test deferral to the next function. + const h0 = new Handler(hookName, '_0'); + const h1 = new Handler(hookName, '_1'); + handlers[hookName] = [h0, h1]; + plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}]; + } + }); + + describe('preAuthorize', function () { + beforeEach(async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + }); + + it('defers if it returns []', async function () { + await agent.get('/').expect(200); + // Note: The preAuthorize hook always runs even if requireAuthorization is false. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('bypasses authenticate and authorize hooks when true is returned', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks when false is returned', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [false]; + await agent.get('/').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks for static content, defers', async function () { + 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 () { + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/admin/').expect(401); + // Notes: + // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because + // 'true' entries are ignored for /admin/* requests. + // * The authenticate hook always runs for /admin/* requests even if + // settings.requireAuthentication is false. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('can deny access to /admin', async function () { + handlers.preAuthorize[0].innerHandle = () => [false]; + await agent.get('/admin/').auth('admin', 'admin-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('runs preAuthzFailure hook when access is denied', async function () { + handlers.preAuthorize[0].innerHandle = () => [false]; + let called = false; + plugins.hooks.preAuthzFailure = [{hook_fn: (hookName, {req, res}, cb) => { + assert.equal(hookName, 'preAuthzFailure'); + assert(req != null); + assert(res != null); + assert(!called); + called = true; + res.status(200).send('injected'); + return cb([true]); + }}]; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); + assert(called); + }); + it('returns 500 if an exception is thrown', async function () { + handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + }); + }); + + describe('authenticate', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + }); + + it('is not called if !requireAuthentication and not /admin/*', async function () { + settings.requireAuthentication = false; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('is called if !requireAuthentication and /admin/*', async function () { + settings.requireAuthentication = false; + await agent.get('/admin/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('defers if empty list returned', async function () { + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('does not defer if return [true], 200', async function () { + handlers.authenticate[0].innerHandle = (req) => { req.session.user = {}; return [true]; }; + await agent.get('/').expect(200); + // Note: authenticate_1 was not called because authenticate_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + it('does not defer if return [false], 401', async function () { + handlers.authenticate[0].innerHandle = (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 () { + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('passes settings.users in context', async function () { + handlers.authenticate[0].checkContext = ({users}) => { + assert.equal(users, settings.users); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('passes user, password in context if provided', async function () { + handlers.authenticate[0].checkContext = ({username, password}) => { + assert.equal(username, 'user'); + assert.equal(password, 'user-password'); + }; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('does not pass user, password in context if not provided', async function () { + handlers.authenticate[0].checkContext = ({username, password}) => { + assert(username == null); + assert(password == null); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('errors if req.session.user is not created', async function () { + handlers.authenticate[0].innerHandle = () => [true]; + await agent.get('/').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + it('returns 500 if an exception is thrown', async function () { + handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + }); + + describe('authorize', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + it('is not called if !requireAuthorization (non-/admin)', async function () { + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('is not called if !requireAuthorization (/admin)', async function () { + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('defers if empty list returned', async function () { + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0', + 'authorize_1']); + }); + it('does not defer if return [true], 200', async function () { + handlers.authorize[0].innerHandle = () => [true]; + await agent.get('/').auth('user', 'user-password').expect(200); + // Note: authorize_1 was not called because authorize_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + it('does not defer if return [false], 403', async function () { + handlers.authorize[0].innerHandle = (req) => [false]; + await agent.get('/').auth('user', 'user-password').expect(403); + // Note: authorize_1 was not called because authorize_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + it('passes req.path in context', async function () { + handlers.authorize[0].checkContext = ({resource}) => { + assert.equal(resource, '/'); + }; + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0', + 'authorize_1']); + }); + it('returns 500 if an exception is thrown', async function () { + handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').auth('user', 'user-password').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + }); + }); + + describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { + const Handler = class { + constructor(hookName) { + this.hookName = hookName; + this.shouldHandle = false; + this.called = false; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(!this.called); + this.called = true; + if (this.shouldHandle) { + context.res.status(200).send(this.hookName); + return cb([true]); + } + return cb([]); + } + }; + const handlers = {}; + + beforeEach(function () { + failHookNames.forEach((hookName) => { + const handler = new Handler(hookName); + handlers[hookName] = handler; + plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}]; + }); + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + + // authn failure tests + it('authn fail, no hooks handle -> 401', async function () { + await agent.get('/').expect(401); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authn fail, authnFailure handles', async function () { + handlers.authnFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); + it('authn fail, authFailure handles', async function () { + handlers.authFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authnFailure trumps authFailure', async function () { + handlers.authnFailure.shouldHandle = true; + handlers.authFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authFailure.called); + }); + + // authz failure tests + it('authz fail, no hooks handle -> 403', async function () { + await agent.get('/').auth('user', 'user-password').expect(403); + assert(!handlers.authnFailure.called); + assert(handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authz fail, authzFailure handles', async function () { + handlers.authzFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(!handlers.authnFailure.called); + assert(handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); + it('authz fail, authFailure handles', async function () { + handlers.authFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); + assert(!handlers.authnFailure.called); + assert(handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authzFailure trumps authFailure', async function () { + handlers.authzFailure.shouldHandle = true; + handlers.authFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); + }); +}); diff --git a/tests/container/loadSettings.js b/tests/container/loadSettings.js index 4a1c46021ef..7317f802269 100644 --- a/tests/container/loadSettings.js +++ b/tests/container/loadSettings.js @@ -12,16 +12,16 @@ * back to a default) */ -var jsonminify = require(__dirname+"/../../src/node_modules/jsonminify"); +const jsonminify = require(`${__dirname}/../../src/node_modules/jsonminify`); const fs = require('fs'); -function loadSettings(){ - var settingsStr = fs.readFileSync(__dirname+"/../../settings.json.docker").toString(); +function loadSettings() { + let settingsStr = fs.readFileSync(`${__dirname}/../../settings.json.docker`).toString(); // try to parse the settings try { - if(settingsStr) { - settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); - var settings = JSON.parse(settingsStr); + if (settingsStr) { + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); + const settings = JSON.parse(settingsStr); // custom settings for running in a container settings.ip = 'localhost'; @@ -29,8 +29,8 @@ function loadSettings(){ return settings; } - }catch(e){ - console.error("whoops something is bad with settings"); + } catch (e) { + console.error('whoops something is bad with settings'); } } diff --git a/tests/container/specs/api/pad.js b/tests/container/specs/api/pad.js index f50b7986468..6aeb8670811 100644 --- a/tests/container/specs/api/pad.js +++ b/tests/container/specs/api/pad.js @@ -5,34 +5,34 @@ * TODO: unify those two files, and merge in a single one. */ -const supertest = require(__dirname+'/../../../../src/node_modules/supertest'); -const settings = require(__dirname+'/../../loadSettings').loadSettings(); -const api = supertest('http://'+settings.ip+":"+settings.port); +const supertest = require(`${__dirname}/../../../../src/node_modules/supertest`); +const settings = require(`${__dirname}/../../loadSettings`).loadSettings(); +const api = supertest(`http://${settings.ip}:${settings.port}`); -var apiVersion = 1; +const apiVersion = 1; -describe('Connectivity', function(){ - it('can connect', function(done) { +describe('Connectivity', function () { + it('can connect', function (done) { api.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done) + .expect('Content-Type', /json/) + .expect(200, done); }); -}) +}); -describe('API Versioning', function(){ - it('finds the version tag', function(done) { +describe('API Versioning', function () { + it('finds the version tag', function (done) { api.get('/api/') - .expect(function(res){ - if (!res.body.currentVersion) throw new Error("No version set in API"); - return; - }) - .expect(200, done) + .expect((res) => { + if (!res.body.currentVersion) throw new Error('No version set in API'); + return; + }) + .expect(200, done); }); -}) +}); -describe('Permission', function(){ - it('errors with invalid APIKey', function(done) { - api.get('/api/'+apiVersion+'/createPad?apikey=wrong_password&padID=test') - .expect(401, done) +describe('Permission', function () { + it('errors with invalid APIKey', function (done) { + api.get(`/api/${apiVersion}/createPad?apikey=wrong_password&padID=test`) + .expect(401, done); }); -}) +}); diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js index e14169bb1dd..b49d32eb8a1 100644 --- a/tests/frontend/helper.js +++ b/tests/frontend/helper.js @@ -1,66 +1,65 @@ var helper = {}; -(function(){ - var $iframe, jsLibraries = {}; +(function () { + let $iframe; const + jsLibraries = {}; - helper.init = function(cb){ - $.get('/static/js/jquery.js').done(function(code){ + helper.init = function (cb) { + $.get('/static/js/jquery.js').done((code) => { // make sure we don't override existing jquery - jsLibraries["jquery"] = "if(typeof $ === 'undefined') {\n" + code + "\n}"; + jsLibraries.jquery = `if(typeof $ === 'undefined') {\n${code}\n}`; - $.get('/tests/frontend/lib/sendkeys.js').done(function(code){ - jsLibraries["sendkeys"] = code; + $.get('/tests/frontend/lib/sendkeys.js').done((code) => { + jsLibraries.sendkeys = code; cb(); }); }); - } + }; - helper.randomString = function randomString(len) - { - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var randomstring = ''; - for (var i = 0; i < len; i++) - { - var rnum = Math.floor(Math.random() * chars.length); + helper.randomString = function randomString(len) { + const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + let randomstring = ''; + for (let i = 0; i < len; i++) { + const rnum = Math.floor(Math.random() * chars.length); randomstring += chars.substring(rnum, rnum + 1); } return randomstring; - } + }; - var getFrameJQuery = function($iframe){ + const getFrameJQuery = function ($iframe) { /* I tried over 9000 ways to inject javascript into iframes. This is the only way I found that worked in IE 7+8+9, FF and Chrome */ - var win = $iframe[0].contentWindow; - var doc = win.document; + const win = $iframe[0].contentWindow; + const doc = win.document; - //IE 8+9 Hack to make eval appear - //http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function - win.execScript && win.execScript("null"); + // IE 8+9 Hack to make eval appear + // http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function + win.execScript && win.execScript('null'); - win.eval(jsLibraries["jquery"]); - win.eval(jsLibraries["sendkeys"]); + win.eval(jsLibraries.jquery); + win.eval(jsLibraries.sendkeys); win.$.window = win; win.$.document = doc; return win.$; - } + }; - helper.clearSessionCookies = function(){ + helper.clearSessionCookies = function () { // Expire cookies, so author and language are changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie window.document.cookie = 'token=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; window.document.cookie = 'language=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; - } + }; // Can only happen when the iframe exists, so we're doing it separately from other cookies - helper.clearPadPrefCookie = function(){ + helper.clearPadPrefCookie = function () { helper.padChrome$.document.cookie = 'prefsHttp=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; - } + }; // Overwrite all prefs in pad cookie. Assumes http, not https. // @@ -68,65 +67,64 @@ var helper = {}; // seem to have independent cookies, UNLESS we put path=/ here (which we don't). // I don't fully understand it, but this function seems to properly simulate // padCookie.setPref in the client code - helper.setPadPrefCookie = function(prefs){ - helper.padChrome$.document.cookie = ("prefsHttp=" + escape(JSON.stringify(prefs)) + ";expires=Thu, 01 Jan 3000 00:00:00 GMT"); - } + helper.setPadPrefCookie = function (prefs) { + helper.padChrome$.document.cookie = (`prefsHttp=${escape(JSON.stringify(prefs))};expires=Thu, 01 Jan 3000 00:00:00 GMT`); + }; // Functionality for knowing what key event type is required for tests - var evtType = "keydown"; + let evtType = 'keydown'; // if it's IE require keypress - if(window.navigator.userAgent.indexOf("MSIE") > -1){ - evtType = "keypress"; + if (window.navigator.userAgent.indexOf('MSIE') > -1) { + evtType = 'keypress'; } // Edge also requires keypress. - if(window.navigator.userAgent.indexOf("Edge") > -1){ - evtType = "keypress"; + if (window.navigator.userAgent.indexOf('Edge') > -1) { + evtType = 'keypress'; } // Opera also requires keypress. - if(window.navigator.userAgent.indexOf("OPR") > -1){ - evtType = "keypress"; + if (window.navigator.userAgent.indexOf('OPR') > -1) { + evtType = 'keypress'; } helper.evtType = evtType; // @todo needs fixing asap // newPad occasionally timeouts, might be a problem with ready/onload code during page setup // This ensures that tests run regardless of this problem - helper.retry = 0 + helper.retry = 0; - helper.newPad = function(cb, padName){ - //build opts object - var opts = {clearCookies: true} - if(typeof cb === 'function'){ - opts.cb = cb + helper.newPad = function (cb, padName) { + // build opts object + let opts = {clearCookies: true}; + if (typeof cb === 'function') { + opts.cb = cb; } else { opts = _.defaults(cb, opts); } // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. - if(opts.params){ - var encodedParams = "?" + $.param(opts.params); + if (opts.params) { + var encodedParams = `?${$.param(opts.params)}`; } - //clear cookies - if(opts.clearCookies){ + // clear cookies + if (opts.clearCookies) { helper.clearSessionCookies(); } - if(!padName) - padName = "FRONTEND_TEST_" + helper.randomString(20); - $iframe = $(""); + if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`; + $iframe = $(``); // needed for retry - let origPadName = padName; + const origPadName = padName; - //clean up inner iframe references + // clean up inner iframe references helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null; - //remove old iframe - $("#iframe-container iframe").remove(); - //set new iframe - $("#iframe-container").append($iframe); - $iframe.one('load', function(){ + // remove old iframe + $('#iframe-container iframe').remove(); + // set new iframe + $('#iframe-container').append($iframe); + $iframe.one('load', () => { helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); if (opts.clearCookies) { helper.clearPadPrefCookie(); @@ -134,112 +132,143 @@ var helper = {}; if (opts.padPrefs) { helper.setPadPrefCookie(opts.padPrefs); } - helper.waitFor(function(){ - return !$iframe.contents().find("#editorloadingbox").is(":visible"); - }, 10000).done(function(){ - helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); - helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe[name="ace_inner"]')); + helper.waitFor(() => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000).done(() => { + helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); + helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); - //disable all animations, this makes tests faster and easier + // 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(); opts.cb(); - }).fail(function(){ + }).fail(() => { if (helper.retry > 3) { - throw new Error("Pad never loaded"); + throw new Error('Pad never loaded'); } helper.retry++; - helper.newPad(cb,origPadName); + helper.newPad(cb, origPadName); }); }); return padName; - } + }; - helper.waitFor = function(conditionFunc, _timeoutTime, _intervalTime){ - var timeoutTime = _timeoutTime || 1900; - var intervalTime = _intervalTime || 10; + helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) { + const deferred = $.Deferred(); - var deferred = $.Deferred(); - - var _fail = deferred.fail; - var listenForFail = false; - deferred.fail = function(){ + const _fail = deferred.fail.bind(deferred); + let listenForFail = false; + deferred.fail = (...args) => { listenForFail = true; - _fail.apply(this, arguments); - } - - var intervalCheck = setInterval(function(){ - var passed = false; - - passed = conditionFunc(); - - if(passed){ - clearInterval(intervalCheck); - clearTimeout(timeout); + return _fail(...args); + }; + const check = () => { + try { + if (!conditionFunc()) return; deferred.resolve(); + } catch (err) { + deferred.reject(err); } - }, intervalTime); + clearInterval(intervalCheck); + clearTimeout(timeout); + }; + + const intervalCheck = setInterval(check, intervalTime); - var timeout = setTimeout(function(){ + const timeout = setTimeout(() => { clearInterval(intervalCheck); - var error = new Error("wait for condition never became true " + conditionFunc.toString()); + const error = new Error(`wait for condition never became true ${conditionFunc.toString()}`); deferred.reject(error); - if(!listenForFail){ + if (!listenForFail) { throw error; } }, timeoutTime); - return deferred; - } + // Check right away to avoid an unnecessary sleep if the condition is already true. + check(); - helper.selectLines = function($startLine, $endLine, startOffset, endOffset){ + return deferred; + }; + + /** + * Same as `waitFor` but using Promises + * + * @returns {Promise} + * + */ + helper.waitForPromise = async function (...args) { + // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable + // exception unless .fail() has been called. That uncatchable exception is disabled here by + // passing a no-op function to .fail(). + return await this.waitFor(...args).fail(() => {}); + }; + + helper.selectLines = function ($startLine, $endLine, startOffset, endOffset) { // if no offset is provided, use beginning of start line and end of end line startOffset = startOffset || 0; - endOffset = endOffset === undefined ? $endLine.text().length : endOffset; + endOffset = endOffset === undefined ? $endLine.text().length : endOffset; - var inner$ = helper.padInner$; - var selection = inner$.document.getSelection(); - var range = selection.getRangeAt(0); + const inner$ = helper.padInner$; + const selection = inner$.document.getSelection(); + const range = selection.getRangeAt(0); - var start = getTextNodeAndOffsetOf($startLine, startOffset); - var end = getTextNodeAndOffsetOf($endLine, endOffset); + const start = getTextNodeAndOffsetOf($startLine, startOffset); + const end = getTextNodeAndOffsetOf($endLine, endOffset); range.setStart(start.node, start.offset); range.setEnd(end.node, end.offset); selection.removeAllRanges(); selection.addRange(range); - } + }; - var getTextNodeAndOffsetOf = function($targetLine, targetOffsetAtLine){ - var $textNodes = $targetLine.find('*').contents().filter(function(){ + var getTextNodeAndOffsetOf = function ($targetLine, targetOffsetAtLine) { + const $textNodes = $targetLine.find('*').contents().filter(function () { return this.nodeType === Node.TEXT_NODE; }); // search node where targetOffsetAtLine is reached, and its 'inner offset' - var textNodeWhereOffsetIs = null; - var offsetBeforeTextNode = 0; - var offsetInsideTextNode = 0; - $textNodes.each(function(index, element){ - var elementTotalOffset = element.textContent.length; + let textNodeWhereOffsetIs = null; + let offsetBeforeTextNode = 0; + let offsetInsideTextNode = 0; + $textNodes.each((index, element) => { + const elementTotalOffset = element.textContent.length; textNodeWhereOffsetIs = element; offsetInsideTextNode = targetOffsetAtLine - offsetBeforeTextNode; - var foundTextNode = offsetBeforeTextNode + elementTotalOffset >= targetOffsetAtLine; - if (foundTextNode){ - return false; //stop .each by returning false + const foundTextNode = offsetBeforeTextNode + elementTotalOffset >= targetOffsetAtLine; + if (foundTextNode) { + return false; // stop .each by returning false } offsetBeforeTextNode += elementTotalOffset; }); // edge cases - if (textNodeWhereOffsetIs === null){ + if (textNodeWhereOffsetIs === null) { // there was no text node inside $targetLine, so it is an empty line (
              ). // Use beginning of line textNodeWhereOffsetIs = $targetLine.get(0); @@ -247,28 +276,16 @@ var helper = {}; } // avoid errors if provided targetOffsetAtLine is higher than line offset (maxOffset). // Use max allowed instead - var maxOffset = textNodeWhereOffsetIs.textContent.length; + const maxOffset = textNodeWhereOffsetIs.textContent.length; offsetInsideTextNode = Math.min(offsetInsideTextNode, maxOffset); return { node: textNodeWhereOffsetIs, offset: offsetInsideTextNode, }; - } + }; /* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/ window.console = window.console || {}; - window.console.log = window.console.log || function(){} - - //force usage of callbacks in it - var _it = it; - it = function(name, func){ - if(func && func.length !== 1){ - func = function(){ - throw new Error("Please use always a callback with it() - " + func.toString()); - } - } - - _it(name, func); - } -})() + window.console.log = window.console.log || function () {}; +})(); diff --git a/tests/frontend/helper/methods.js b/tests/frontend/helper/methods.js new file mode 100644 index 00000000000..4c7fe120474 --- /dev/null +++ b/tests/frontend/helper/methods.js @@ -0,0 +1,238 @@ +'use strict'; + +/** + * Spys on socket.io messages and saves them into several arrays + * that are visible in tests + */ +helper.spyOnSocketIO = function () { + helper.contentWindow().pad.socket.on('message', (msg) => { + if (msg.type == 'COLLABROOM') { + if (msg.data.type == 'ACCEPT_COMMIT') { + helper.commits.push(msg); + } else if (msg.data.type == 'USER_NEWINFO') { + helper.userInfos.push(msg); + } else if (msg.data.type == 'CHAT_MESSAGE') { + helper.chatMessages.push(msg); + } + } + }); +}; + +/** + * Makes an edit via `sendkeys` to the position of the caret and ensures ACCEPT_COMMIT + * is returned by the server + * It does not check if the ACCEPT_COMMIT is the edit sent, though + * If `line` is not given, the edit goes to line no. 1 + * + * @param {string} message The edit to make - can be anything supported by `sendkeys` + * @param {number} [line] the optional line to make the edit on starting from 1 + * @returns {Promise} + * @todo needs to support writing to a specified caret position + * + */ +helper.edit = async function (message, line) { + const editsNum = helper.commits.length; + line = line ? line - 1 : 0; + helper.linesDiv()[line].sendkeys(message); + return helper.waitForPromise(() => editsNum + 1 === helper.commits.length); +}; + +/** + * The pad text as an array of divs + * + * @example + * helper.linesDiv()[2].sendkeys('abc') // sends abc to the third line + * + * @returns {Array.} array of divs + */ +helper.linesDiv = function () { + return helper.padInner$('.ace-line').map(function () { + return $(this); + }).get(); +}; + +/** + * The pad text as an array of lines + * For lines in timeslider use `helper.timesliderTextLines()` + * + * @returns {Array.} lines of text + */ +helper.textLines = function () { + return helper.linesDiv().map((div) => div.text()); +}; + +/** + * The default pad text transmitted via `clientVars` + * + * @returns {string} + */ +helper.defaultText = function () { + return helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text; +}; + +/** + * Sends a chat `message` via `sendKeys` + * You *must* include `{enter}` at the end of the string or it will + * just fill the input field but not send the message. + * + * @todo Cannot send multiple messages at once + * + * @example + * + * `helper.sendChatMessage('hi{enter}')` + * + * @param {string} message the chat message to be sent + * @returns {Promise} + */ +helper.sendChatMessage = function (message) { + const noOfChatMessages = helper.chatMessages.length; + helper.padChrome$('#chatinput').sendkeys(message); + return helper.waitForPromise(() => noOfChatMessages + 1 === helper.chatMessages.length); +}; + +/** + * Opens the settings menu if its hidden via button + * + * @returns {Promise} + */ +helper.showSettings = function () { + if (!helper.isSettingsShown()) { + helper.settingsButton().click(); + return helper.waitForPromise(() => helper.isSettingsShown(), 2000); + } +}; + +/** + * Hide the settings menu if its open via button + * + * @returns {Promise} + * @todo untested + */ +helper.hideSettings = function () { + if (helper.isSettingsShown()) { + helper.settingsButton().click(); + return helper.waitForPromise(() => !helper.isSettingsShown(), 2000); + } +}; + +/** + * Makes the chat window sticky via settings menu if the settings menu is + * open and sticky button is not checked + * + * @returns {Promise} + */ +helper.enableStickyChatviaSettings = function () { + const stickyChat = helper.padChrome$('#options-stickychat'); + if (helper.isSettingsShown() && !stickyChat.is(':checked')) { + stickyChat.click(); + return helper.waitForPromise(() => helper.isChatboxSticky(), 2000); + } +}; + +/** + * Unsticks the chat window via settings menu if the settings menu is open + * and sticky button is checked + * + * @returns {Promise} + */ +helper.disableStickyChatviaSettings = function () { + const stickyChat = helper.padChrome$('#options-stickychat'); + if (helper.isSettingsShown() && stickyChat.is(':checked')) { + stickyChat.click(); + return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); + } +}; + +/** + * Makes the chat window sticky via an icon on the top right of the chat + * window + * + * @returns {Promise} + */ +helper.enableStickyChatviaIcon = function () { + const stickyChat = helper.padChrome$('#titlesticky'); + if (helper.isChatboxShown() && !helper.isChatboxSticky()) { + stickyChat.click(); + return helper.waitForPromise(() => helper.isChatboxSticky(), 2000); + } +}; + +/** + * Disables the stickyness of the chat window via an icon on the + * upper right + * + * @returns {Promise} + */ +helper.disableStickyChatviaIcon = function () { + if (helper.isChatboxShown() && helper.isChatboxSticky()) { + helper.titlecross().click(); + return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); + } +}; + +/** + * Sets the src-attribute of the main iframe to the timeslider + * In case a revision is given, sets the timeslider to this specific revision. + * Defaults to going to the last revision. + * It waits until the timer is filled with date and time, because it's one of the + * last things that happen during timeslider load + * + * @param {number} [revision] the optional revision + * @returns {Promise} + * @todo for some reason this does only work the first time, you cannot + * goto rev 0 and then via the same method to rev 5. Use buttons instead + */ +helper.gotoTimeslider = function (revision) { + revision = Number.isInteger(revision) ? `#${revision}` : ''; + const iframe = $('#iframe-container iframe'); + iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`); + + return helper.waitForPromise(() => helper.timesliderTimerTime() && + !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000); +}; + +/** + * Clicks in the timeslider at a specific offset + * It's used to navigate the timeslider + * + * @todo no mousemove test + * @param {number} X coordinate + */ +helper.sliderClick = function (X) { + const sliderBar = helper.sliderBar(); + const edown = new jQuery.Event('mousedown'); + const eup = new jQuery.Event('mouseup'); + edown.clientX = eup.clientX = X; + edown.clientY = eup.clientY = sliderBar.offset().top; + + sliderBar.trigger(edown); + sliderBar.trigger(eup); +}; + +/** + * The timeslider text as an array of lines + * + * @returns {Array.} lines of text + */ +helper.timesliderTextLines = function () { + return helper.contentWindow().$('.ace-line').map(function () { + return $(this).text(); + }).get(); +}; + +helper.padIsEmpty = () => ( + !helper.padInner$.document.getSelection().isCollapsed || + (helper.padInner$('div').length === 1 && helper.padInner$('div').first().html() === '
              ')); + +helper.clearPad = async () => { + if (helper.padIsEmpty()) return; + const commitsBefore = helper.commits.length; + const lines = helper.linesDiv(); + helper.selectLines(lines[0], lines[lines.length - 1]); + await helper.waitForPromise(() => !helper.padInner$.document.getSelection().isCollapsed); + const e = new helper.padInner$.Event(helper.evtType); + e.keyCode = 8; // delete key + helper.padInner$('#innerdocbody').trigger(e); + await helper.waitForPromise(helper.padIsEmpty); + await helper.waitForPromise(() => helper.commits.length > commitsBefore); +}; diff --git a/tests/frontend/helper/ui.js b/tests/frontend/helper/ui.js new file mode 100644 index 00000000000..d83cbee9709 --- /dev/null +++ b/tests/frontend/helper/ui.js @@ -0,0 +1,147 @@ +/** + * the contentWindow is either the normal pad or timeslider + * + * @returns {HTMLElement} contentWindow + */ +helper.contentWindow = function () { + return $('#iframe-container iframe')[0].contentWindow; +}; + +/** + * Opens the chat unless it is already open via an + * icon on the bottom right of the page + * + * @returns {Promise} + */ +helper.showChat = function () { + const chaticon = helper.chatIcon(); + if (chaticon.hasClass('visible')) { + chaticon.click(); + return helper.waitForPromise(() => !chaticon.hasClass('visible'), 2000); + } +}; + +/** + * Closes the chat window if it is shown and not sticky + * + * @returns {Promise} + */ +helper.hideChat = function () { + if (helper.isChatboxShown() && !helper.isChatboxSticky()) { + helper.titlecross().click(); + return helper.waitForPromise(() => !helper.isChatboxShown(), 2000); + } +}; + +/** + * Gets the chat icon from the bottom right of the page + * + * @returns {HTMLElement} the chat icon + */ +helper.chatIcon = function () { return helper.padChrome$('#chaticon'); }; + +/** + * The chat messages from the UI + * + * @returns {Array.} + */ +helper.chatTextParagraphs = function () { return helper.padChrome$('#chattext').children('p'); }; + +/** + * Returns true if the chat box is sticky + * + * @returns {boolean} stickyness of the chat box + */ +helper.isChatboxSticky = function () { + return helper.padChrome$('#chatbox').hasClass('stickyChat'); +}; + +/** + * Returns true if the chat box is shown + * + * @returns {boolean} visibility of the chat box + */ +helper.isChatboxShown = function () { + return helper.padChrome$('#chatbox').hasClass('visible'); +}; + +/** + * Gets the settings menu + * + * @returns {HTMLElement} the settings menu + */ +helper.settingsMenu = function () { return helper.padChrome$('#settings'); }; + +/** + * Gets the settings button + * + * @returns {HTMLElement} the settings button + */ +helper.settingsButton = function () { return helper.padChrome$("button[data-l10n-id='pad.toolbar.settings.title']"); }; + +/** + * Gets the titlecross icon + * + * @returns {HTMLElement} the titlecross icon + */ +helper.titlecross = function () { return helper.padChrome$('#titlecross'); }; + +/** + * Returns true if the settings menu is visible + * + * @returns {boolean} is the settings menu shown? + */ +helper.isSettingsShown = function () { + return helper.padChrome$('#settings').hasClass('popup-show'); +}; + +/** + * Gets the timer div of a timeslider that has the datetime of the revision + * + * @returns {HTMLElement} timer + */ +helper.timesliderTimer = function () { + if (typeof helper.contentWindow().$ === 'function') { + return helper.contentWindow().$('#timer'); + } +}; + +/** + * Gets the time of the revision on a timeslider + * + * @returns {HTMLElement} timer + */ +helper.timesliderTimerTime = function () { + if (helper.timesliderTimer()) { + return helper.timesliderTimer().text(); + } +}; + +/** + * The ui-slidar-bar element in the timeslider + * + * @returns {HTMLElement} + */ +helper.sliderBar = function () { + return helper.contentWindow().$('#ui-slider-bar'); +}; + +/** + * revision_date element + * like "Saved October 10, 2020" + * + * @returns {HTMLElement} + */ +helper.revisionDateElem = function () { + return helper.contentWindow().$('#revision_date').text(); +}; + +/** + * revision_label element + * like "Version 1" + * + * @returns {HTMLElement} + */ +helper.revisionLabelElem = function () { + return helper.contentWindow().$('#revision_label'); +}; diff --git a/tests/frontend/index.html b/tests/frontend/index.html index d828e851caf..8d31a8e6a55 100644 --- a/tests/frontend/index.html +++ b/tests/frontend/index.html @@ -14,11 +14,12 @@ - + - + + diff --git a/tests/frontend/runner.js b/tests/frontend/runner.js index 0ab380fb785..616c065b3c0 100644 --- a/tests/frontend/runner.js +++ b/tests/frontend/runner.js @@ -1,70 +1,74 @@ -$(function(){ +'use strict'; - function stringifyException(exception){ - var err = exception.stack || exception.toString(); +/* global specs_list */ + +$(() => { + const stringifyException = (exception) => { + let err = exception.stack || exception.toString(); // FF / Opera do not add the message if (!~err.indexOf(exception.message)) { - err = exception.message + '\n' + err; + err = `${exception.message}\n${err}`; } // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we // check for the result of the stringifying. - if ('[object Error]' == err) err = exception.message; + if (err === '[object Error]') err = exception.message; // Safari doesn't give you a stack. Let's at least provide a source line. if (!exception.stack && exception.sourceURL && exception.line !== undefined) { - err += "\n(" + exception.sourceURL + ":" + exception.line + ")"; + err += `\n(${exception.sourceURL}:${exception.line})`; } return err; - } + }; - function CustomRunner(runner) { - var stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }; + const customRunner = (runner) => { + const stats = {suites: 0, tests: 0, passes: 0, pending: 0, failures: 0}; + let level = 0; if (!runner) return; - runner.on('start', function(){ - stats.start = new Date; + runner.on('start', () => { + stats.start = new Date(); }); - runner.on('suite', function(suite){ + runner.on('suite', (suite) => { suite.root || stats.suites++; if (suite.root) return; append(suite.title); level++; }); - runner.on('suite end', function(suite){ + runner.on('suite end', (suite) => { if (suite.root) return; level--; - if(level == 0) { - append(""); + if (level === 0) { + append(''); } }); // Scroll down test display after each test - let mochaEl = $('#mocha')[0]; - runner.on('test', function(){ + const mochaEl = $('#mocha')[0]; + runner.on('test', () => { mochaEl.scrollTop = mochaEl.scrollHeight; }); // max time a test is allowed to run // TODO this should be lowered once timeslider_revision.js is faster - var killTimeout; - runner.on('test end', function(){ + let killTimeout; + runner.on('test end', () => { stats.tests++; }); - runner.on('pass', function(test){ - if(killTimeout) clearTimeout(killTimeout); - killTimeout = setTimeout(function(){ - append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); + runner.on('pass', (test) => { + if (killTimeout) clearTimeout(killTimeout); + killTimeout = setTimeout(() => { + append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); }, 60000 * 3); - var medium = test.slow() / 2; + const medium = test.slow() / 2; test.speed = test.duration > test.slow() ? 'slow' : test.duration > medium @@ -72,107 +76,109 @@ $(function(){ : 'fast'; stats.passes++; - append("->","[green]PASSED[clear] :", test.title," ",test.duration,"ms"); + append(`-> [green]PASSED[clear] : ${test.title} ${test.duration} ms`); }); - runner.on('fail', function(test, err){ - if(killTimeout) clearTimeout(killTimeout); - killTimeout = setTimeout(function(){ - append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); + runner.on('fail', (test, err) => { + if (killTimeout) clearTimeout(killTimeout); + killTimeout = setTimeout(() => { + append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); }, 60000 * 3); stats.failures++; test.err = err; - append("->","[red]FAILED[clear] :", test.title, stringifyException(test.err)); + append(`-> [red]FAILED[clear] : ${test.title} ${stringifyException(test.err)}`); }); - runner.on('pending', function(test){ - if(killTimeout) clearTimeout(killTimeout); - killTimeout = setTimeout(function(){ - append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); + runner.on('pending', (test) => { + if (killTimeout) clearTimeout(killTimeout); + killTimeout = setTimeout(() => { + append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]'); }, 60000 * 3); stats.pending++; - append("->","[yellow]PENDING[clear]:", test.title); + append(`-> [yellow]PENDING[clear]: ${test.title}`); }); - var $console = $("#console"); - var level = 0; - var append = function(){ - var text = Array.prototype.join.apply(arguments, [" "]); - var oldText = $console.text(); - - var space = ""; - for(var i=0;i { + const oldText = $console.text(); - var splitedText = ""; - _(text.split("\n")).each(function(line){ - while(line.length > 0){ - var split = line.substr(0,100); - line = line.substr(100); - if(splitedText.length > 0) splitedText+="\n"; - splitedText += split; - } - }); - - //indent all lines with the given amount of space - var newText = _(splitedText.split("\n")).map(function(line){ - return space + line; - }).join("\\n"); - - $console.text(oldText + newText + "\\n"); + let space = ''; + for (let i = 0; i < level * 2; i++) { + space += ' '; } - var total = runner.total; - runner.on('end', function(){ - stats.end = new Date; - stats.duration = stats.end - stats.start; - var minutes = Math.floor(stats.duration / 1000 / 60); - var seconds = Math.round((stats.duration / 1000) % 60) // chrome < 57 does not like this .toString().padStart("2","0"); - if(stats.tests === total){ - append("FINISHED -", stats.passes, "tests passed,", stats.failures, "tests failed,", stats.pending," pending, duration: " + minutes + ":" + seconds); - } else if (stats.tests > total) { - append("FINISHED - but more tests than planned returned", stats.passes, "tests passed,", stats.failures, "tests failed,", stats.pending," pending, duration: " + minutes + ":" + seconds); - append(total,"tests, but",stats.tests,"returned. There is probably a problem with your async code or error handling, see https://github.com/mochajs/mocha/issues/1327"); - } - else { - append("FINISHED - but not all tests returned", stats.passes, "tests passed,", stats.failures, "tests failed,", stats.pending, "tests pending, duration: " + minutes + ":" + seconds); - append(total,"tests, but only",stats.tests,"returned. Check for failed before/beforeEach-hooks (no `test end` is called for them and subsequent tests of the same suite are skipped), see https://github.com/mochajs/mocha/pull/1043"); + let splitedText = ''; + _(text.split('\n')).each((line) => { + while (line.length > 0) { + const split = line.substr(0, 100); + line = line.substr(100); + if (splitedText.length > 0) splitedText += '\n'; + splitedText += split; } }); - } - - //http://stackoverflow.com/questions/1403888/get-url-parameter-with-jquery - var getURLParameter = function (name) { - return decodeURI( - (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1] - ); - } - - //get the list of specs and filter it if requested - var specs = specs_list.slice(); - - //inject spec scripts into the dom - var $body = $('body'); - $.each(specs, function(i, spec){ - if(spec[0] != "/"){ // if the spec isn't a plugin spec which means the spec file might be in a different subfolder - $body.append('') - }else{ - $body.append('') + + // indent all lines with the given amount of space + const newText = _(splitedText.split('\n')).map((line) => space + line).join('\\n'); + + $console.text(`${oldText + newText}\\n`); + }; + + const total = runner.total; + runner.on('end', () => { + stats.end = new Date(); + stats.duration = stats.end - stats.start; + const minutes = Math.floor(stats.duration / 1000 / 60); + // chrome < 57 does not like this .toString().padStart('2', '0'); + const seconds = Math.round((stats.duration / 1000) % 60); + if (stats.tests === total) { + append(`FINISHED - ${stats.passes} tests passed, ${stats.failures} tests failed, ` + + `${stats.pending} pending, duration: ${minutes}:${seconds}`); + } else if (stats.tests > total) { + append(`FINISHED - but more tests than planned returned ${stats.passes} tests passed, ` + + `${stats.failures} tests failed, ${stats.pending} pending, ` + + `duration: ${minutes}:${seconds}`); + append(`${total} tests, but ${stats.tests} returned. ` + + 'There is probably a problem with your async code or error handling, ' + + 'see https://github.com/mochajs/mocha/issues/1327'); + } else { + append(`FINISHED - but not all tests returned ${stats.passes} tests passed, ` + + `${stats.failures} tests failed, ${stats.pending} tests pending, ` + + `duration: ${minutes}:${seconds}`); + append(`${total} tests, but only ${stats.tests} returned. ` + + 'Check for failed before/beforeEach-hooks (no `test end` is called for them ' + + 'and subsequent tests of the same suite are skipped), ' + + 'see https://github.com/mochajs/mocha/pull/1043'); + } + }); + }; + + const getURLParameter = (name) => (new URLSearchParams(location.search)).get(name); + + // get the list of specs and filter it if requested + const specs = specs_list.slice(); + + // inject spec scripts into the dom + const $body = $('body'); + $.each(specs, (i, spec) => { + // if the spec isn't a plugin spec which means the spec file might be in a different subfolder + if (!spec.startsWith('/')) { + $body.append(``); + } else { + $body.append(``); } }); - //initalize the test helper - helper.init(function(){ - //configure and start the test framework - var grep = getURLParameter("grep"); - if(grep != "null"){ + // initalize the test helper + helper.init(() => { + // configure and start the test framework + const grep = getURLParameter('grep'); + if (grep != null) { mocha.grep(grep); } - var runner = mocha.run(); - CustomRunner(runner) + const runner = mocha.run(); + customRunner(runner); }); }); diff --git a/tests/frontend/specs/alphabet.js b/tests/frontend/specs/alphabet.js index 5d16c983a30..a0ad61bdf74 100644 --- a/tests/frontend/specs/alphabet.js +++ b/tests/frontend/specs/alphabet.js @@ -1,27 +1,24 @@ -describe("All the alphabet works n stuff", function(){ - var expectedString = "abcdefghijklmnopqrstuvwxyz"; +describe('All the alphabet works n stuff', function () { + const expectedString = 'abcdefghijklmnopqrstuvwxyz'; - //create a new pad before each test run - beforeEach(function(cb){ + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("when you enter any char it appears right", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('when you enter any char it appears right', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const firstTextElement = inner$('div').first(); // simulate key presses to delete content firstTextElement.sendkeys('{selectall}'); // select all firstTextElement.sendkeys('{del}'); // clear the first line firstTextElement.sendkeys(expectedString); // insert the string - helper.waitFor(function(){ - return inner$("div").first().text() === expectedString; - }, 2000).done(done); + helper.waitFor(() => inner$('div').first().text() === expectedString, 2000).done(done); }); - }); diff --git a/tests/frontend/specs/authorship_of_editions.js b/tests/frontend/specs/authorship_of_editions.js index 0c173e9624b..6cf14b86922 100644 --- a/tests/frontend/specs/authorship_of_editions.js +++ b/tests/frontend/specs/authorship_of_editions.js @@ -1,46 +1,46 @@ -describe('author of pad edition', function() { - var REGULAR_LINE = 0; - var LINE_WITH_ORDERED_LIST = 1; - var LINE_WITH_UNORDERED_LIST = 2; +describe('author of pad edition', function () { + const REGULAR_LINE = 0; + const LINE_WITH_ORDERED_LIST = 1; + const LINE_WITH_UNORDERED_LIST = 2; // author 1 creates a new pad with some content (regular lines and lists) - before(function(done) { - var padId = helper.newPad(function() { + before(function (done) { + var padId = helper.newPad(() => { // make sure pad has at least 3 lines - var $firstLine = helper.padInner$('div').first(); - var threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
              '); + const $firstLine = helper.padInner$('div').first(); + const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
              '); $firstLine.html(threeLines); // wait for lines to be processed by Etherpad - helper.waitFor(function() { - var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + helper.waitFor(() => { + const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); return $lineWithUnorderedList.text() === 'line with unordered list'; - }).done(function() { + }).done(() => { // create the unordered list - var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); $lineWithUnorderedList.sendkeys('{selectall}'); - var $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist'); + const $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist'); $insertUnorderedListButton.click(); - helper.waitFor(function() { - var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + helper.waitFor(() => { + const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); return $lineWithUnorderedList.find('ul li').length === 1; - }).done(function() { + }).done(() => { // create the ordered list - var $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); + const $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); $lineWithOrderedList.sendkeys('{selectall}'); - var $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist'); + const $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist'); $insertOrderedListButton.click(); - helper.waitFor(function() { - var $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); + helper.waitFor(() => { + const $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); return $lineWithOrderedList.find('ol li').length === 1; - }).done(function() { + }).done(() => { // Reload pad, to make changes as a second user. Need a timeout here to make sure // all changes were saved before reloading - setTimeout(function() { + setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; @@ -55,55 +55,51 @@ describe('author of pad edition', function() { }); // author 2 makes some changes on the pad - it('marks only the new content as changes of the second user on a regular line', function(done) { + it('marks only the new content as changes of the second user on a regular line', function (done) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done); }); - it('marks only the new content as changes of the second user on a line with ordered list', function(done) { + it('marks only the new content as changes of the second user on a line with ordered list', function (done) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done); }); - it('marks only the new content as changes of the second user on a line with unordered list', function(done) { + it('marks only the new content as changes of the second user on a line with unordered list', function (done) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); }); /* ********************** Helper functions ************************ */ - var getLine = function(lineNumber) { + var getLine = function (lineNumber) { return helper.padInner$('div').eq(lineNumber); - } + }; - var getAuthorFromClassList = function(classes) { - return classes.find(function(cls) { - return cls.startsWith('author'); - }); - } + const getAuthorFromClassList = function (classes) { + return classes.find((cls) => cls.startsWith('author')); + }; - var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function(lineNumber, textChange, done) { + var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function (lineNumber, textChange, done) { // get original author class - var classes = getLine(lineNumber).find('span').first().attr('class').split(' '); - var originalAuthor = getAuthorFromClassList(classes); + const classes = getLine(lineNumber).find('span').first().attr('class').split(' '); + const originalAuthor = getAuthorFromClassList(classes); // make change on target line - var $regularLine = getLine(lineNumber); + const $regularLine = getLine(lineNumber); helper.selectLines($regularLine, $regularLine, 2, 2); // place caret after 2nd char of line $regularLine.sendkeys(textChange); // wait for change to be processed by Etherpad - var otherAuthorsOfLine; - helper.waitFor(function() { - var authorsOfLine = getLine(lineNumber).find('span').map(function() { + let otherAuthorsOfLine; + helper.waitFor(() => { + const authorsOfLine = getLine(lineNumber).find('span').map(function () { return getAuthorFromClassList($(this).attr('class').split(' ')); }).get(); - otherAuthorsOfLine = authorsOfLine.filter(function(author) { - return author !== originalAuthor; - }); - var lineHasChangeOfThisAuthor = otherAuthorsOfLine.length > 0; + otherAuthorsOfLine = authorsOfLine.filter((author) => author !== originalAuthor); + const lineHasChangeOfThisAuthor = otherAuthorsOfLine.length > 0; return lineHasChangeOfThisAuthor; - }).done(function() { - var thisAuthor = otherAuthorsOfLine[0]; - var $changeOfThisAuthor = getLine(lineNumber).find('span.' + thisAuthor); + }).done(() => { + const thisAuthor = otherAuthorsOfLine[0]; + const $changeOfThisAuthor = getLine(lineNumber).find(`span.${thisAuthor}`); expect($changeOfThisAuthor.text()).to.be(textChange); done(); }); - } + }; }); diff --git a/tests/frontend/specs/bold.js b/tests/frontend/specs/bold.js index 94e3a9b5b89..a7c46e1bc0e 100644 --- a/tests/frontend/specs/bold.js +++ b/tests/frontend/specs/bold.js @@ -1,64 +1,64 @@ -describe("bold button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('bold button', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text bold on click", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text bold on click', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - //get the bold button and click it - var $boldButton = chrome$(".buttonicon-bold"); + // get the bold button and click it + const $boldButton = chrome$('.buttonicon-bold'); $boldButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a button, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // is there a element now? - var isBold = $newFirstTextElement.find("b").length === 1; + const isBold = $newFirstTextElement.find('b').length === 1; - //expect it to be bold + // expect it to be bold expect(isBold).to.be(true); - //make sure the text hasn't changed + // make sure the text hasn't changed expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); done(); }); - it("makes text bold on keypress", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text bold on keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 66; // b - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a button, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // is there a element now? - var isBold = $newFirstTextElement.find("b").length === 1; + const isBold = $newFirstTextElement.find('b').length === 1; - //expect it to be bold + // expect it to be bold expect(isBold).to.be(true); - //make sure the text hasn't changed + // make sure the text hasn't changed expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); done(); diff --git a/tests/frontend/specs/caret.js b/tests/frontend/specs/caret.js index b15dd159716..1fb8d8aa79a 100644 --- a/tests/frontend/specs/caret.js +++ b/tests/frontend/specs/caret.js @@ -1,7 +1,7 @@ -describe("As the caret is moved is the UI properly updated?", function(){ - var padName; - var numberOfRows = 50; -/* +describe('As the caret is moved is the UI properly updated?', function () { + let padName; + const numberOfRows = 50; + /* //create a new pad before each test run beforeEach(function(cb){ @@ -28,7 +28,7 @@ describe("As the caret is moved is the UI properly updated?", function(){ * How do we keep the authors focus on a line if the lines above the author are modified? We should only redraw the user to a location if they are typing and make sure shift and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken * How can we simulate an edit event in the test framework? */ -/* + /* // THIS DOESNT WORK IN CHROME AS IT DOESNT MOVE THE CURSOR! it("down arrow", function(done){ var inner$ = helper.padInner$; @@ -194,7 +194,6 @@ console.log(inner$); }); - /* it("Creates N rows, changes height of rows, updates UI by caret key events", function(done){ var inner$ = helper.padInner$; @@ -264,7 +263,6 @@ console.log(inner$); }).done(function(){ // Once the DOM has registered the items - }); }); @@ -284,54 +282,51 @@ console.log(inner$); done(); }); */ - }); -function prepareDocument(n, target){ // generates a random document with random content on n lines - var i = 0; - while(i < n){ // for each line +function prepareDocument(n, target) { // generates a random document with random content on n lines + let i = 0; + while (i < n) { // for each line target.sendkeys(makeStr()); // generate a random string and send that to the editor target.sendkeys('{enter}'); // generator an enter keypress i++; // rinse n times } } -function keyEvent(target, charCode, ctrl, shift){ // sends a charCode to the window - - var e = target.Event(helper.evtType); - if(ctrl){ +function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the window + const e = target.Event(helper.evtType); + if (ctrl) { e.ctrlKey = true; // Control key } - if(shift){ + if (shift) { e.shiftKey = true; // Shift Key } e.which = charCode; e.keyCode = charCode; - target("#innerdocbody").trigger(e); + target('#innerdocbody').trigger(e); } -function makeStr(){ // from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function makeStr() { // from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for( var i=0; i < 5; i++ ) - text += possible.charAt(Math.floor(Math.random() * possible.length)); + for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } -function isScrolledIntoView(elem, $){ // from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling - var docViewTop = $(window).scrollTop(); - var docViewBottom = docViewTop + $(window).height(); - var elemTop = $(elem).offset().top; // how far the element is from the top of it's container - var elemBottom = elemTop + $(elem).height(); // how far plus the height of the elem.. IE is it all in? - elemBottom = elemBottom - 16; // don't ask, sorry but this is needed.. - return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); +function isScrolledIntoView(elem, $) { // from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling + const docViewTop = $(window).scrollTop(); + const docViewBottom = docViewTop + $(window).height(); + const elemTop = $(elem).offset().top; // how far the element is from the top of it's container + let elemBottom = elemTop + $(elem).height(); // how far plus the height of the elem.. IE is it all in? + elemBottom -= 16; // don't ask, sorry but this is needed.. + return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); } -function caretPosition($){ - var doc = $.window.document; - var pos = doc.getSelection(); +function caretPosition($) { + const doc = $.window.document; + const pos = doc.getSelection(); pos.y = pos.anchorNode.parentElement.offsetTop; pos.x = pos.anchorNode.parentElement.offsetLeft; return pos; diff --git a/tests/frontend/specs/change_user_color.js b/tests/frontend/specs/change_user_color.js index 5969eabe298..e8c16db375e 100644 --- a/tests/frontend/specs/change_user_color.js +++ b/tests/frontend/specs/change_user_color.js @@ -1,102 +1,101 @@ -describe("change user color", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('change user color', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("Color picker matches original color and remembers the user color after a refresh", function(done) { + it('Color picker matches original color and remembers the user color after a refresh', function (done) { this.timeout(60000); - var chrome$ = helper.padChrome$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $userSwatch = chrome$("#myswatch"); + const $userSwatch = chrome$('#myswatch'); $userSwatch.click(); - var fb = chrome$.farbtastic('#colorpicker') - var $colorPickerSave = chrome$("#mycolorpickersave"); - var $colorPickerPreview = chrome$("#mycolorpickerpreview"); + const fb = chrome$.farbtastic('#colorpicker'); + const $colorPickerSave = chrome$('#mycolorpickersave'); + const $colorPickerPreview = chrome$('#mycolorpickerpreview'); // Same color represented in two different ways - const testColorHash = '#abcdef' - const testColorRGB = 'rgb(171, 205, 239)' + const testColorHash = '#abcdef'; + const testColorRGB = 'rgb(171, 205, 239)'; // Check that the color picker matches the automatically assigned random color on the swatch. // NOTE: This has a tiny chance of creating a false positive for passing in the // off-chance the randomly assigned color is the same as the test color. - expect($colorPickerPreview.css('background-color')).to.be($userSwatch.css('background-color')) + expect($colorPickerPreview.css('background-color')).to.be($userSwatch.css('background-color')); // The swatch updates as the test color is picked. - fb.setColor(testColorHash) - expect($colorPickerPreview.css('background-color')).to.be(testColorRGB) + fb.setColor(testColorHash); + expect($colorPickerPreview.css('background-color')).to.be(testColorRGB); $colorPickerSave.click(); - expect($userSwatch.css('background-color')).to.be(testColorRGB) + expect($userSwatch.css('background-color')).to.be(testColorRGB); - setTimeout(function(){ //give it a second to save the color on the server side + setTimeout(() => { // give it a second to save the color on the server side helper.newPad({ // get a new pad, but don't clear the cookies - clearCookies: false - , cb: function(){ - var chrome$ = helper.padChrome$; + clearCookies: false, + cb() { + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $userSwatch = chrome$("#myswatch"); + const $userSwatch = chrome$('#myswatch'); $userSwatch.click(); - var $colorPickerPreview = chrome$("#mycolorpickerpreview"); + const $colorPickerPreview = chrome$('#mycolorpickerpreview'); - expect($colorPickerPreview.css('background-color')).to.be(testColorRGB) - expect($userSwatch.css('background-color')).to.be(testColorRGB) + expect($colorPickerPreview.css('background-color')).to.be(testColorRGB); + expect($userSwatch.css('background-color')).to.be(testColorRGB); done(); - } + }, }); }, 1000); }); - it("Own user color is shown when you enter a chat", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('Own user color is shown when you enter a chat', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $colorOption = helper.padChrome$('#options-colorscheck'); + const $colorOption = helper.padChrome$('#options-colorscheck'); if (!$colorOption.is(':checked')) { $colorOption.click(); } - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $userSwatch = chrome$("#myswatch"); + const $userSwatch = chrome$('#myswatch'); $userSwatch.click(); - var fb = chrome$.farbtastic('#colorpicker') - var $colorPickerSave = chrome$("#mycolorpickersave"); + const fb = chrome$.farbtastic('#colorpicker'); + const $colorPickerSave = chrome$('#mycolorpickersave'); // Same color represented in two different ways - const testColorHash = '#abcdef' - const testColorRGB = 'rgb(171, 205, 239)' + const testColorHash = '#abcdef'; + const testColorRGB = 'rgb(171, 205, 239)'; - fb.setColor(testColorHash) + fb.setColor(testColorHash); $colorPickerSave.click(); - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); + // click on the chat button to make chat visible + const $chatButton = chrome$('#chaticon'); $chatButton.click(); - var $chatInput = chrome$("#chatinput"); + const $chatInput = chrome$('#chatinput'); $chatInput.sendkeys('O hi'); // simulate a keypress of typing user $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - var $firstChatMessage = chrome$("#chattext").children("p"); + // check if chat shows up + helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 // wait until the chat message shows up + ).done(() => { + const $firstChatMessage = chrome$('#chattext').children('p'); expect($firstChatMessage.css('background-color')).to.be(testColorRGB); // expect the first chat message to be of the user's color done(); }); diff --git a/tests/frontend/specs/change_user_name.js b/tests/frontend/specs/change_user_name.js index b0a5df15f27..e144a23404e 100644 --- a/tests/frontend/specs/change_user_name.js +++ b/tests/frontend/specs/change_user_name.js @@ -1,70 +1,69 @@ -describe("change username value", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('change username value', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("Remembers the user name after a refresh", function(done) { + it('Remembers the user name after a refresh', function (done) { this.timeout(60000); - var chrome$ = helper.padChrome$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $usernameInput = chrome$("#myusernameedit"); + const $usernameInput = chrome$('#myusernameedit'); $usernameInput.click(); $usernameInput.val('John McLear'); $usernameInput.blur(); - setTimeout(function(){ //give it a second to save the username on the server side + setTimeout(() => { // give it a second to save the username on the server side helper.newPad({ // get a new pad, but don't clear the cookies - clearCookies: false - , cb: function(){ - var chrome$ = helper.padChrome$; + clearCookies: false, + cb() { + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $usernameInput = chrome$("#myusernameedit"); - expect($usernameInput.val()).to.be('John McLear') + const $usernameInput = chrome$('#myusernameedit'); + expect($usernameInput.val()).to.be('John McLear'); done(); - } + }, }); }, 1000); }); - it("Own user name is shown when you enter a chat", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('Own user name is shown when you enter a chat', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + // click on the settings button to make settings visible + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $usernameInput = chrome$("#myusernameedit"); + const $usernameInput = chrome$('#myusernameedit'); $usernameInput.click(); $usernameInput.val('John McLear'); $usernameInput.blur(); - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); + // click on the chat button to make chat visible + const $chatButton = chrome$('#chaticon'); $chatButton.click(); - var $chatInput = chrome$("#chatinput"); + const $chatInput = chrome$('#chatinput'); $chatInput.sendkeys('O hi'); // simulate a keypress of typing JohnMcLear $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - var $firstChatMessage = chrome$("#chattext").children("p"); - var containsJohnMcLear = $firstChatMessage.text().indexOf("John McLear") !== -1; // does the string contain John McLear + // check if chat shows up + helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 // wait until the chat message shows up + ).done(() => { + const $firstChatMessage = chrome$('#chattext').children('p'); + const containsJohnMcLear = $firstChatMessage.text().indexOf('John McLear') !== -1; // does the string contain John McLear expect(containsJohnMcLear).to.be(true); // expect the first chat message to contain JohnMcLear done(); }); diff --git a/tests/frontend/specs/chat.js b/tests/frontend/specs/chat.js index 4a88379df6e..d45988d6025 100644 --- a/tests/frontend/specs/chat.js +++ b/tests/frontend/specs/chat.js @@ -1,170 +1,111 @@ -describe("Chat messages and UI", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('Chat messages and UI', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); - this.timeout(60000); }); - it("opens chat, sends a message and makes sure it exists on the page", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var chatValue = "JohnMcLear"; - - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); - $chatButton.click(); - var $chatInput = chrome$("#chatinput"); - $chatInput.sendkeys('JohnMcLear'); // simulate a keypress of typing JohnMcLear - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 - - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - var $firstChatMessage = chrome$("#chattext").children("p"); - var containsMessage = $firstChatMessage.text().indexOf("JohnMcLear") !== -1; // does the string contain JohnMcLear? - expect(containsMessage).to.be(true); // expect the first chat message to contain JohnMcLear - - // do a slightly more thorough check - var username = $firstChatMessage.children("b"); - var usernameValue = username.text(); - var time = $firstChatMessage.children(".time"); - var timeValue = time.text(); - var discoveredValue = $firstChatMessage.text(); - var chatMsgExists = (discoveredValue.indexOf("JohnMcLear") !== -1); - expect(chatMsgExists).to.be(true); - done(); - }); + it('opens chat, sends a message, makes sure it exists on the page and hides chat', async function () { + const chatValue = 'JohnMcLear'; - }); + await helper.showChat(); + await helper.sendChatMessage(`${chatValue}{enter}`); - it("makes sure that an empty message can't be sent", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); - $chatButton.click(); - var $chatInput = chrome$("#chatinput"); - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter (to send an empty message) - $chatInput.sendkeys('mluto'); // simulate a keypress of typing mluto - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter (to send 'mluto') - - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - // check that the empty message is not there - expect(chrome$("#chattext").children("p").length).to.be(1); - // check that the received message is not the empty one - var $firstChatMessage = chrome$("#chattext").children("p"); - var containsMessage = $firstChatMessage.text().indexOf("mluto") !== -1; - expect(containsMessage).to.be(true); - done(); - }); - }); + expect(helper.chatTextParagraphs().length).to.be(1); - it("makes chat stick to right side of the screen", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + //

              + // unnamed: + // 12:38 + // JohnMcLear + //

              + const username = helper.chatTextParagraphs().children('b').text(); + const time = helper.chatTextParagraphs().children('.time').text(); - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); - $settingsButton.click(); + expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}`); - //get the chat selector - var $stickychatCheckbox = chrome$("#options-stickychat"); + await helper.hideChat(); + }); - //select chat always on screen - if (!$stickychatCheckbox.is(':checked')) { - $stickychatCheckbox.click(); - } + it("makes sure that an empty message can't be sent", async function () { + const chatValue = 'mluto'; - // due to animation, we need to make some timeout... - setTimeout(function() { - //check if chat changed to get the stickychat Class - var $chatbox = chrome$("#chatbox"); - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(true); + await helper.showChat(); - // select chat always on screen and fire change event - $stickychatCheckbox.click(); + await helper.sendChatMessage(`{enter}${chatValue}{enter}`); // simulate a keypress of typing enter, mluto and enter (to send 'mluto') - setTimeout(function() { - //check if chat changed to remove the stickychat Class - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(false); + const chat = helper.chatTextParagraphs(); - done(); - }, 10) - }, 10) + expect(chat.length).to.be(1); + // check that the received message is not the empty one + const username = chat.children('b').text(); + const time = chat.children('.time').text(); + expect(chat.text()).to.be(`${username}${time} ${chatValue}`); }); - it("makes chat stick to right side of the screen then makes it one step smaller", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async function () { + await helper.showSettings(); - // open chat - chrome$('#chaticon').click(); + await helper.enableStickyChatviaSettings(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(true); - // select chat always on screen from chatbox - chrome$('.stick-to-screen-btn').click(); + await helper.disableStickyChatviaSettings(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(true); - // due to animation, we need to make some timeout... - setTimeout(function() { - //check if chat changed to get the stickychat Class - var $chatbox = chrome$("#chatbox"); - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(true); + await helper.hideChat(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(false); + }); - // select chat always on screen and fire change event - chrome$('#titlecross').click(); + it('makes chat stick to right side of the screen via icon on the top right, remove sticky via icon, close it', async function () { + await helper.showChat(); - setTimeout(function() { - //check if chat changed to remove the stickychat Class - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(false); + await helper.enableStickyChatviaIcon(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(true); - done(); - }, 10) - }, 10) + await helper.disableStickyChatviaIcon(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(false); + + await helper.hideChat(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(false); }); - xit("Checks showChat=false URL Parameter hides chat then when removed it shows chat", function(done) { + xit('Checks showChat=false URL Parameter hides chat then when removed it shows chat', function (done) { this.timeout(60000); - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - setTimeout(function(){ //give it a second to save the username on the server side + setTimeout(() => { // give it a second to save the username on the server side helper.newPad({ // get a new pad, but don't clear the cookies clearCookies: false, - params:{ - showChat: "false" - }, cb: function(){ - var chrome$ = helper.padChrome$; - var chaticon = chrome$("#chaticon"); + params: { + showChat: 'false', + }, cb() { + const chrome$ = helper.padChrome$; + const chaticon = chrome$('#chaticon'); // chat should be hidden. - expect(chaticon.is(":visible")).to.be(false); + expect(chaticon.is(':visible')).to.be(false); - setTimeout(function(){ //give it a second to save the username on the server side + setTimeout(() => { // give it a second to save the username on the server side helper.newPad({ // get a new pad, but don't clear the cookies - clearCookies: false - , cb: function(){ - var chrome$ = helper.padChrome$; - var chaticon = chrome$("#chaticon"); + clearCookies: false, + cb() { + const chrome$ = helper.padChrome$; + const chaticon = chrome$('#chaticon'); // chat should be visible. - expect(chaticon.is(":visible")).to.be(true); + expect(chaticon.is(':visible')).to.be(true); done(); - } + }, }); }, 1000); - - } + }, }); }, 1000); - }); - }); diff --git a/tests/frontend/specs/chat_load_messages.js b/tests/frontend/specs/chat_load_messages.js index 29040798d34..29c1734ca05 100644 --- a/tests/frontend/specs/chat_load_messages.js +++ b/tests/frontend/specs/chat_load_messages.js @@ -1,85 +1,77 @@ -describe("chat-load-messages", function(){ - var padName; +describe('chat-load-messages', function () { + let padName; - it("creates a pad", function(done) { + it('creates a pad', function (done) { padName = helper.newPad(done); this.timeout(60000); }); - it("adds a lot of messages", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var chatButton = chrome$("#chaticon"); + it('adds a lot of messages', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const chatButton = chrome$('#chaticon'); chatButton.click(); - var chatInput = chrome$("#chatinput"); - var chatText = chrome$("#chattext"); + const chatInput = chrome$('#chatinput'); + const chatText = chrome$('#chattext'); this.timeout(60000); - var messages = 140; - for(var i=1; i <= messages; i++) { - var num = ''+i; - if(num.length == 1) - num = '00'+num; - if(num.length == 2) - num = '0'+num; - chatInput.sendkeys('msg' + num); + const messages = 140; + for (let i = 1; i <= messages; i++) { + let num = `${i}`; + if (num.length == 1) num = `00${num}`; + if (num.length == 2) num = `0${num}`; + chatInput.sendkeys(`msg${num}`); chatInput.sendkeys('{enter}'); } - helper.waitFor(function(){ - return chatText.children("p").length == messages; - }, 60000).always(function(){ - expect(chatText.children("p").length).to.be(messages); + helper.waitFor(() => chatText.children('p').length == messages, 60000).always(() => { + expect(chatText.children('p').length).to.be(messages); helper.newPad(done, padName); - }); + }); }); - it("checks initial message count", function(done) { - var chatText; - var expectedCount = 101; - var chrome$ = helper.padChrome$; - helper.waitFor(function(){ - var chatButton = chrome$("#chaticon"); + it('checks initial message count', function (done) { + let chatText; + const expectedCount = 101; + const chrome$ = helper.padChrome$; + helper.waitFor(() => { + const chatButton = chrome$('#chaticon'); chatButton.click(); - chatText = chrome$("#chattext"); - return chatText.children("p").length == expectedCount; - }).always(function(){ - expect(chatText.children("p").length).to.be(expectedCount); + chatText = chrome$('#chattext'); + return chatText.children('p').length == expectedCount; + }).always(() => { + expect(chatText.children('p').length).to.be(expectedCount); done(); }); }); - it("loads more messages", function(done) { - var expectedCount = 122; - var chrome$ = helper.padChrome$; - var chatButton = chrome$("#chaticon"); + it('loads more messages', function (done) { + const expectedCount = 122; + const chrome$ = helper.padChrome$; + const chatButton = chrome$('#chaticon'); chatButton.click(); - var chatText = chrome$("#chattext"); - var loadMsgBtn = chrome$("#chatloadmessagesbutton"); + const chatText = chrome$('#chattext'); + const loadMsgBtn = chrome$('#chatloadmessagesbutton'); loadMsgBtn.click(); - helper.waitFor(function(){ - return chatText.children("p").length == expectedCount; - }).always(function(){ - expect(chatText.children("p").length).to.be(expectedCount); + helper.waitFor(() => chatText.children('p').length == expectedCount).always(() => { + expect(chatText.children('p').length).to.be(expectedCount); done(); }); }); - it("checks for button vanishing", function(done) { - var expectedDisplay = 'none'; - var chrome$ = helper.padChrome$; - var chatButton = chrome$("#chaticon"); + it('checks for button vanishing', function (done) { + const expectedDisplay = 'none'; + const chrome$ = helper.padChrome$; + const chatButton = chrome$('#chaticon'); chatButton.click(); - var chatText = chrome$("#chattext"); - var loadMsgBtn = chrome$("#chatloadmessagesbutton"); - var loadMsgBall = chrome$("#chatloadmessagesball"); + const chatText = chrome$('#chattext'); + const loadMsgBtn = chrome$('#chatloadmessagesbutton'); + const loadMsgBall = chrome$('#chatloadmessagesball'); loadMsgBtn.click(); - helper.waitFor(function(){ - return loadMsgBtn.css('display') == expectedDisplay && - loadMsgBall.css('display') == expectedDisplay; - }).always(function(){ + helper.waitFor(() => loadMsgBtn.css('display') == expectedDisplay && + loadMsgBall.css('display') == expectedDisplay).always(() => { expect(loadMsgBtn.css('display')).to.be(expectedDisplay); expect(loadMsgBall.css('display')).to.be(expectedDisplay); done(); diff --git a/tests/frontend/specs/clear_authorship_colors.js b/tests/frontend/specs/clear_authorship_colors.js index 143a8612d24..f622e912a67 100644 --- a/tests/frontend/specs/clear_authorship_colors.js +++ b/tests/frontend/specs/clear_authorship_colors.js @@ -1,133 +1,128 @@ -describe("clear authorship colors button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('clear authorship colors button', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text clear authorship colors", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text clear authorship colors', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // override the confirm dialogue functioon - helper.padChrome$.window.confirm = function(){ + helper.padChrome$.window.confirm = function () { return true; - } + }; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); // Get the original text - var originalText = inner$("div").first().text(); + const originalText = inner$('div').first().text(); // Set some new text - var sentText = "Hello"; + const sentText = 'Hello'; - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(function(){ - return inner$("div span").first().attr("class").indexOf("author") !== -1; // wait until we have the full value available - }).done(function(){ - //IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$("div").first().focus(); + helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + ).done(() => { + // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship + inner$('div').first().focus(); - //get the clear authorship colors button and click it - var $clearauthorshipcolorsButton = chrome$(".buttonicon-clearauthorship"); + // get the clear authorship colors button and click it + const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); // does the first divs span include an author class? - var hasAuthorClass = inner$("div span").first().attr("class").indexOf("author") !== -1; - //expect(hasAuthorClass).to.be(false); + var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; + // expect(hasAuthorClass).to.be(false); // does the first div include an author class? - var hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1; + var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - helper.waitFor(function(){ - var disconnectVisible = chrome$("div.disconnected").attr("class").indexOf("visible") === -1 - return (disconnectVisible === true) + helper.waitFor(() => { + const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + return (disconnectVisible === true); }); - var disconnectVisible = chrome$("div.disconnected").attr("class").indexOf("visible") === -1 + const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; expect(disconnectVisible).to.be(true); done(); }); - }); - it("makes text clear authorship colors and checks it can't be undone", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("makes text clear authorship colors and checks it can't be undone", function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // override the confirm dialogue functioon - helper.padChrome$.window.confirm = function(){ + helper.padChrome$.window.confirm = function () { return true; - } + }; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); // Get the original text - var originalText = inner$("div").first().text(); + const originalText = inner$('div').first().text(); // Set some new text - var sentText = "Hello"; + const sentText = 'Hello'; - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(function(){ - return inner$("div span").first().attr("class").indexOf("author") !== -1; // wait until we have the full value available - }).done(function(){ - //IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$("div").first().focus(); + helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + ).done(() => { + // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship + inner$('div').first().focus(); - //get the clear authorship colors button and click it - var $clearauthorshipcolorsButton = chrome$(".buttonicon-clearauthorship"); + // get the clear authorship colors button and click it + const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); // does the first divs span include an author class? - var hasAuthorClass = inner$("div span").first().attr("class").indexOf("author") !== -1; - //expect(hasAuthorClass).to.be(false); + var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; + // expect(hasAuthorClass).to.be(false); // does the first div include an author class? - var hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1; + var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z - inner$("#innerdocbody").trigger(e); // shouldn't od anything + inner$('#innerdocbody').trigger(e); // shouldn't od anything // does the first div include an author class? - hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1; + hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); // get undo and redo buttons - var $undoButton = chrome$(".buttonicon-undo"); + const $undoButton = chrome$('.buttonicon-undo'); // click the button $undoButton.click(); // shouldn't do anything - hasAuthorClass = inner$("div").first().attr("class").indexOf("author") !== -1; + hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - helper.waitFor(function(){ - var disconnectVisible = chrome$("div.disconnected").attr("class").indexOf("visible") === -1 - return (disconnectVisible === true) + helper.waitFor(() => { + const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + return (disconnectVisible === true); }); - var disconnectVisible = chrome$("div.disconnected").attr("class").indexOf("visible") === -1 + const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; expect(disconnectVisible).to.be(true); done(); }); - }); }); - diff --git a/tests/frontend/specs/delete.js b/tests/frontend/specs/delete.js index 616cd4ddc51..4267aeec774 100644 --- a/tests/frontend/specs/delete.js +++ b/tests/frontend/specs/delete.js @@ -1,36 +1,36 @@ -describe("delete keystroke", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('delete keystroke', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text delete", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text delete', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); // get the original length of this element - var elementLength = $firstTextElement.text().length; + const elementLength = $firstTextElement.text().length; // get the original string value minus the last char - var originalTextValue = $firstTextElement.text(); - originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length ); + const originalTextValue = $firstTextElement.text(); + const originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length); // simulate key presses to delete content $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key $firstTextElement.sendkeys('{del}'); // simulate a keypress of delete - //ace creates a new dom element when you press a keystroke, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a keystroke, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // get the new length of this element - var newElementLength = $newFirstTextElement.text().length; + const newElementLength = $newFirstTextElement.text().length; - //expect it to be one char less in length - expect(newElementLength).to.be((elementLength-1)); + // expect it to be one char less in length + expect(newElementLength).to.be((elementLength - 1)); done(); }); diff --git a/tests/frontend/specs/drag_and_drop.js b/tests/frontend/specs/drag_and_drop.js index 821d3aacce2..a9726111c06 100644 --- a/tests/frontend/specs/drag_and_drop.js +++ b/tests/frontend/specs/drag_and_drop.js @@ -1,39 +1,39 @@ // WARNING: drag and drop is only simulated on these tests, so manual testing might also be necessary -describe('drag and drop', function() { - before(function(done) { - helper.newPad(function() { +describe('drag and drop', function () { + before(function (done) { + helper.newPad(() => { createScriptWithSeveralLines(done); }); this.timeout(60000); }); - context('when user drags part of one line and drops it far form its original place', function() { - before(function(done) { + context('when user drags part of one line and drops it far form its original place', function () { + before(function (done) { selectPartOfSourceLine(); dragSelectedTextAndDropItIntoMiddleOfLine(TARGET_LINE); // make sure DnD was correctly simulated - helper.waitFor(function() { - var $targetLine = getLine(TARGET_LINE); - var sourceWasMovedToTarget = $targetLine.text() === 'Target line [line 1]'; + helper.waitFor(() => { + const $targetLine = getLine(TARGET_LINE); + const sourceWasMovedToTarget = $targetLine.text() === 'Target line [line 1]'; return sourceWasMovedToTarget; }).done(done); }); - context('and user triggers UNDO', function() { - before(function() { - var $undoButton = helper.padChrome$(".buttonicon-undo"); + context('and user triggers UNDO', function () { + before(function () { + const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); }); - it('moves text back to its original place', function(done) { + it('moves text back to its original place', function (done) { // test text was removed from drop target - var $targetLine = getLine(TARGET_LINE); + const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); // test text was added back to original place - var $firstSourceLine = getLine(FIRST_SOURCE_LINE); - var $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); + const $firstSourceLine = getLine(FIRST_SOURCE_LINE); + const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); expect($firstSourceLine.text()).to.be('Source line 1.'); expect($lastSourceLine.text()).to.be('Source line 2.'); @@ -42,33 +42,33 @@ describe('drag and drop', function() { }); }); - context('when user drags some lines far form its original place', function() { - before(function(done) { + context('when user drags some lines far form its original place', function () { + before(function (done) { selectMultipleSourceLines(); dragSelectedTextAndDropItIntoMiddleOfLine(TARGET_LINE); // make sure DnD was correctly simulated - helper.waitFor(function() { - var $lineAfterTarget = getLine(TARGET_LINE + 1); - var sourceWasMovedToTarget = $lineAfterTarget.text() !== '...'; + helper.waitFor(() => { + const $lineAfterTarget = getLine(TARGET_LINE + 1); + const sourceWasMovedToTarget = $lineAfterTarget.text() !== '...'; return sourceWasMovedToTarget; }).done(done); }); - context('and user triggers UNDO', function() { - before(function() { - var $undoButton = helper.padChrome$(".buttonicon-undo"); + context('and user triggers UNDO', function () { + before(function () { + const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); }); - it('moves text back to its original place', function(done) { + it('moves text back to its original place', function (done) { // test text was removed from drop target - var $targetLine = getLine(TARGET_LINE); + const $targetLine = getLine(TARGET_LINE); expect($targetLine.text()).to.be('Target line []'); // test text was added back to original place - var $firstSourceLine = getLine(FIRST_SOURCE_LINE); - var $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); + const $firstSourceLine = getLine(FIRST_SOURCE_LINE); + const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); expect($firstSourceLine.text()).to.be('Source line 1.'); expect($lastSourceLine.text()).to.be('Source line 2.'); @@ -81,88 +81,88 @@ describe('drag and drop', function() { var TARGET_LINE = 2; var FIRST_SOURCE_LINE = 5; - var getLine = function(lineNumber) { - var $lines = helper.padInner$('div'); + var getLine = function (lineNumber) { + const $lines = helper.padInner$('div'); return $lines.slice(lineNumber, lineNumber + 1); - } + }; - var createScriptWithSeveralLines = function(done) { + var createScriptWithSeveralLines = function (done) { // create some lines to be used on the tests - var $firstLine = helper.padInner$('div').first(); + const $firstLine = helper.padInner$('div').first(); $firstLine.html('...
              ...
              Target line []
              ...
              ...
              Source line 1.
              Source line 2.
              '); // wait for lines to be split - helper.waitFor(function(){ - var $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); + helper.waitFor(() => { + const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); return $lastSourceLine.text() === 'Source line 2.'; }).done(done); - } + }; - var selectPartOfSourceLine = function() { - var $sourceLine = getLine(FIRST_SOURCE_LINE); + var selectPartOfSourceLine = function () { + const $sourceLine = getLine(FIRST_SOURCE_LINE); // select 'line 1' from 'Source line 1.' - var start = 'Source '.length; - var end = start + 'line 1'.length; + const start = 'Source '.length; + const end = start + 'line 1'.length; helper.selectLines($sourceLine, $sourceLine, start, end); - } - var selectMultipleSourceLines = function() { - var $firstSourceLine = getLine(FIRST_SOURCE_LINE); - var $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); + }; + var selectMultipleSourceLines = function () { + const $firstSourceLine = getLine(FIRST_SOURCE_LINE); + const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); helper.selectLines($firstSourceLine, $lastSourceLine); - } + }; - var dragSelectedTextAndDropItIntoMiddleOfLine = function(targetLineNumber) { + var dragSelectedTextAndDropItIntoMiddleOfLine = function (targetLineNumber) { // dragstart: start dragging content triggerEvent('dragstart'); // drop: get HTML data from selected text - var draggedHtml = getHtmlFromSelectedText(); + const draggedHtml = getHtmlFromSelectedText(); triggerEvent('drop'); // dragend: remove original content + insert HTML data into target moveSelectionIntoTarget(draggedHtml, targetLineNumber); triggerEvent('dragend'); - } + }; - var getHtmlFromSelectedText = function() { - var innerDocument = helper.padInner$.document; + var getHtmlFromSelectedText = function () { + const innerDocument = helper.padInner$.document; - var range = innerDocument.getSelection().getRangeAt(0); - var clonedSelection = range.cloneContents(); - var span = innerDocument.createElement('span'); + const range = innerDocument.getSelection().getRangeAt(0); + const clonedSelection = range.cloneContents(); + const span = innerDocument.createElement('span'); span.id = 'buffer'; span.appendChild(clonedSelection); - var draggedHtml = span.outerHTML; + const draggedHtml = span.outerHTML; return draggedHtml; - } + }; - var triggerEvent = function(eventName) { - var event = helper.padInner$.Event(eventName); + var triggerEvent = function (eventName) { + const event = helper.padInner$.Event(eventName); helper.padInner$('#innerdocbody').trigger(event); - } + }; - var moveSelectionIntoTarget = function(draggedHtml, targetLineNumber) { - var innerDocument = helper.padInner$.document; + var moveSelectionIntoTarget = function (draggedHtml, targetLineNumber) { + const innerDocument = helper.padInner$.document; // delete original content innerDocument.execCommand('delete'); // set position to insert content on target line - var $target = getLine(targetLineNumber); + const $target = getLine(targetLineNumber); $target.sendkeys('{selectall}{rightarrow}{leftarrow}'); // Insert content. // Based on http://stackoverflow.com/a/6691294, to be IE-compatible - var range = innerDocument.getSelection().getRangeAt(0); - var frag = innerDocument.createDocumentFragment(); - var el = innerDocument.createElement('div'); + const range = innerDocument.getSelection().getRangeAt(0); + const frag = innerDocument.createDocumentFragment(); + const el = innerDocument.createElement('div'); el.innerHTML = draggedHtml; while (el.firstChild) { frag.appendChild(el.firstChild); } range.insertNode(frag); - } + }; }); diff --git a/tests/frontend/specs/embed_value.js b/tests/frontend/specs/embed_value.js index e4cbcaaeb25..d6fb8c9772e 100644 --- a/tests/frontend/specs/embed_value.js +++ b/tests/frontend/specs/embed_value.js @@ -1,136 +1,133 @@ -describe("embed links", function(){ - var objectify = function (str) - { - var hash = {}; - var parts = str.split('&'); - for(var i = 0; i < parts.length; i++) - { - var keyValue = parts[i].split('='); +describe('embed links', function () { + const objectify = function (str) { + const hash = {}; + const parts = str.split('&'); + for (let i = 0; i < parts.length; i++) { + const keyValue = parts[i].split('='); hash[keyValue[0]] = keyValue[1]; } return hash; - } + }; - var checkiFrameCode = function(embedCode, readonly){ - //turn the code into an html element - var $embediFrame = $(embedCode); + const checkiFrameCode = function (embedCode, readonly) { + // turn the code into an html element + const $embediFrame = $(embedCode); - //read and check the frame attributes - var width = $embediFrame.attr("width"); - var height = $embediFrame.attr("height"); - var name = $embediFrame.attr("name"); + // read and check the frame attributes + const width = $embediFrame.attr('width'); + const height = $embediFrame.attr('height'); + const name = $embediFrame.attr('name'); expect(width).to.be('100%'); expect(height).to.be('600'); - expect(name).to.be(readonly ? "embed_readonly" : "embed_readwrite"); - - //parse the url - var src = $embediFrame.attr("src"); - var questionMark = src.indexOf("?"); - var url = src.substr(0,questionMark); - var paramsStr = src.substr(questionMark+1); - var params = objectify(paramsStr); - - var expectedParams = { - showControls: 'true' - , showChat: 'true' - , showLineNumbers: 'true' - , useMonospaceFont: 'false' - } - - //check the url - if(readonly){ - expect(url.indexOf("r.") > 0).to.be(true); + expect(name).to.be(readonly ? 'embed_readonly' : 'embed_readwrite'); + + // parse the url + const src = $embediFrame.attr('src'); + const questionMark = src.indexOf('?'); + const url = src.substr(0, questionMark); + const paramsStr = src.substr(questionMark + 1); + const params = objectify(paramsStr); + + const expectedParams = { + showControls: 'true', + showChat: 'true', + showLineNumbers: 'true', + useMonospaceFont: 'false', + }; + + // check the url + if (readonly) { + expect(url.indexOf('r.') > 0).to.be(true); } else { expect(url).to.be(helper.padChrome$.window.location.href); } - //check if all parts of the url are like expected + // check if all parts of the url are like expected expect(params).to.eql(expectedParams); - } + }; - describe("read and write", function(){ - //create a new pad before each test run - beforeEach(function(cb){ + describe('read and write', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - describe("the share link", function(){ - it("is the actual pad url", function(done){ - var chrome$ = helper.padChrome$; + describe('the share link', function () { + it('is the actual pad url', function (done) { + const chrome$ = helper.padChrome$; - //open share dropdown - chrome$(".buttonicon-embed").click(); + // open share dropdown + chrome$('.buttonicon-embed').click(); - //get the link of the share field + the actual pad url and compare them - var shareLink = chrome$("#linkinput").val(); - var padURL = chrome$.window.location.href; + // get the link of the share field + the actual pad url and compare them + const shareLink = chrome$('#linkinput').val(); + const padURL = chrome$.window.location.href; expect(shareLink).to.be(padURL); done(); }); }); - describe("the embed as iframe code", function(){ - it("is an iframe with the the correct url parameters and correct size", function(done){ - var chrome$ = helper.padChrome$; + describe('the embed as iframe code', function () { + it('is an iframe with the the correct url parameters and correct size', function (done) { + const chrome$ = helper.padChrome$; - //open share dropdown - chrome$(".buttonicon-embed").click(); + // open share dropdown + chrome$('.buttonicon-embed').click(); - //get the link of the share field + the actual pad url and compare them - var embedCode = chrome$("#embedinput").val(); + // get the link of the share field + the actual pad url and compare them + const embedCode = chrome$('#embedinput').val(); - checkiFrameCode(embedCode, false) + checkiFrameCode(embedCode, false); done(); }); }); }); - describe("when read only option is set", function(){ - beforeEach(function(cb){ + describe('when read only option is set', function () { + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - describe("the share link", function(){ - it("shows a read only url", function(done){ - var chrome$ = helper.padChrome$; + describe('the share link', function () { + it('shows a read only url', function (done) { + const chrome$ = helper.padChrome$; - //open share dropdown - chrome$(".buttonicon-embed").click(); + // open share dropdown + chrome$('.buttonicon-embed').click(); chrome$('#readonlyinput').click(); chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - //get the link of the share field + the actual pad url and compare them - var shareLink = chrome$("#linkinput").val(); - var containsReadOnlyLink = shareLink.indexOf("r.") > 0 + // get the link of the share field + the actual pad url and compare them + const shareLink = chrome$('#linkinput').val(); + const containsReadOnlyLink = shareLink.indexOf('r.') > 0; expect(containsReadOnlyLink).to.be(true); done(); }); }); - describe("the embed as iframe code", function(){ - it("is an iframe with the the correct url parameters and correct size", function(done){ - var chrome$ = helper.padChrome$; + describe('the embed as iframe code', function () { + it('is an iframe with the the correct url parameters and correct size', function (done) { + const chrome$ = helper.padChrome$; - //open share dropdown - chrome$(".buttonicon-embed").click(); - //check read only checkbox, a bit hacky + // open share dropdown + chrome$('.buttonicon-embed').click(); + // check read only checkbox, a bit hacky chrome$('#readonlyinput').click(); chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - //get the link of the share field + the actual pad url and compare them - var embedCode = chrome$("#embedinput").val(); + // get the link of the share field + the actual pad url and compare them + const embedCode = chrome$('#embedinput').val(); checkiFrameCode(embedCode, true); done(); }); }); - }); }); diff --git a/tests/frontend/specs/enter.js b/tests/frontend/specs/enter.js index 9c3457b02f9..6108d7f8282 100644 --- a/tests/frontend/specs/enter.js +++ b/tests/frontend/specs/enter.js @@ -1,32 +1,30 @@ -describe("enter keystroke", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('enter keystroke', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("creates a new line & puts cursor onto a new line", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('creates a new line & puts cursor onto a new line', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); // get the original string value minus the last char - var originalTextValue = $firstTextElement.text(); + const originalTextValue = $firstTextElement.text(); // simulate key presses to enter content $firstTextElement.sendkeys('{enter}'); - //ace creates a new dom element when you press a keystroke, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a keystroke, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); - helper.waitFor(function(){ - return inner$("div").first().text() === ""; - }).done(function(){ - var $newSecondLine = inner$("div").first().next(); - var newFirstTextElementValue = inner$("div").first().text(); - expect(newFirstTextElementValue).to.be(""); // expect the first line to be blank + helper.waitFor(() => inner$('div').first().text() === '').done(() => { + const $newSecondLine = inner$('div').first().next(); + const newFirstTextElementValue = inner$('div').first().text(); + expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank expect($newSecondLine.text()).to.be(originalTextValue); // expect the second line to be the same as the original first line. done(); }); diff --git a/tests/frontend/specs/font_type.js b/tests/frontend/specs/font_type.js index ad220d88271..51971da3994 100644 --- a/tests/frontend/specs/font_type.js +++ b/tests/frontend/specs/font_type.js @@ -1,31 +1,31 @@ -describe("font select", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('font select', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text RobotoMono", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text RobotoMono', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); + // click on the settings button to make settings visible + const $settingsButton = chrome$('.buttonicon-settings'); $settingsButton.click(); - //get the font menu and RobotoMono option - var $viewfontmenu = chrome$("#viewfontmenu"); - var $RobotoMonooption = $viewfontmenu.find("[value=RobotoMono]"); + // get the font menu and RobotoMono option + const $viewfontmenu = chrome$('#viewfontmenu'); + const $RobotoMonooption = $viewfontmenu.find('[value=RobotoMono]'); - //select RobotoMono and fire change event + // select RobotoMono and fire change event // $RobotoMonooption.attr('selected','selected'); // commenting out above will break safari test - $viewfontmenu.val("RobotoMono"); + $viewfontmenu.val('RobotoMono'); $viewfontmenu.change(); - //check if font changed to RobotoMono - var fontFamily = inner$("body").css("font-family").toLowerCase(); - var containsStr = fontFamily.indexOf("robotomono"); + // check if font changed to RobotoMono + const fontFamily = inner$('body').css('font-family').toLowerCase(); + const containsStr = fontFamily.indexOf('robotomono'); expect(containsStr).to.not.be(-1); done(); diff --git a/tests/frontend/specs/helper.js b/tests/frontend/specs/helper.js index 1cb712a009e..6bc6a364330 100644 --- a/tests/frontend/specs/helper.js +++ b/tests/frontend/specs/helper.js @@ -1,34 +1,34 @@ -describe("the test helper", function(){ - describe("the newPad method", function(){ - xit("doesn't leak memory if you creates iframes over and over again", function(done){ +describe('the test helper', function () { + describe('the newPad method', function () { + xit("doesn't leak memory if you creates iframes over and over again", function (done) { this.timeout(100000); - var times = 10; + let times = 10; - var loadPad = function(){ - helper.newPad(function(){ + var loadPad = function () { + helper.newPad(() => { times--; - if(times > 0){ + if (times > 0) { loadPad(); } else { done(); } - }) - } + }); + }; loadPad(); }); - it("gives me 3 jquery instances of chrome, outer and inner", function(done){ + it('gives me 3 jquery instances of chrome, outer and inner', function (done) { this.timeout(10000); - helper.newPad(function(){ - //check if the jquery selectors have the desired elements - expect(helper.padChrome$("#editbar").length).to.be(1); - expect(helper.padOuter$("#outerdocbody").length).to.be(1); - expect(helper.padInner$("#innerdocbody").length).to.be(1); + helper.newPad(() => { + // check if the jquery selectors have the desired elements + expect(helper.padChrome$('#editbar').length).to.be(1); + expect(helper.padOuter$('#outerdocbody').length).to.be(1); + expect(helper.padInner$('#innerdocbody').length).to.be(1); - //check if the document object was set correctly + // check if the document object was set correctly expect(helper.padChrome$.window.document).to.be(helper.padChrome$.document); expect(helper.padOuter$.window.document).to.be(helper.padOuter$.document); expect(helper.padInner$.window.document).to.be(helper.padInner$.document); @@ -43,7 +43,7 @@ describe("the test helper", function(){ // However this doesn't seem to always be easily replicated, so this // timeout may or may end up in the code. None the less, we test here // to catch it if the bug comes up again. - it("clears cookies", function(done) { + it('clears cookies', function (done) { this.timeout(60000); // set cookies far into the future to make sure they're not expired yet @@ -53,20 +53,20 @@ describe("the test helper", function(){ expect(window.document.cookie).to.contain('token=foo'); expect(window.document.cookie).to.contain('language=bar'); - helper.newPad(function(){ + helper.newPad(() => { // helper function seems to have cleared cookies // NOTE: this doesn't yet mean it's proven to have taken effect by this point in execution - var firstCookie = window.document.cookie + const firstCookie = window.document.cookie; expect(firstCookie).to.not.contain('token=foo'); expect(firstCookie).to.not.contain('language=bar'); - var chrome$ = helper.padChrome$; + const chrome$ = helper.padChrome$; // click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); - var $usernameInput = chrome$("#myusernameedit"); + const $usernameInput = chrome$('#myusernameedit'); $usernameInput.click(); $usernameInput.val('John McLear'); @@ -84,9 +84,9 @@ describe("the test helper", function(){ // be sure. expect(window.document.cookie).to.not.contain('prefsHtml=baz'); - setTimeout(function(){ //give it a second to save the username on the server side - helper.newPad(function(){ // get a new pad, let it clear the cookies - var chrome$ = helper.padChrome$; + setTimeout(() => { // give it a second to save the username on the server side + helper.newPad(() => { // get a new pad, let it clear the cookies + const chrome$ = helper.padChrome$; // helper function seems to have cleared cookies // NOTE: this doesn't yet mean cookies were cleared effectively. @@ -99,11 +99,11 @@ describe("the test helper", function(){ expect(window.document.cookie).to.not.be(firstCookie); // click on the settings button to make settings visible - var $userButton = chrome$(".buttonicon-showusers"); + const $userButton = chrome$('.buttonicon-showusers'); $userButton.click(); // confirm that the session was actually cleared - var $usernameInput = chrome$("#myusernameedit"); + const $usernameInput = chrome$('#myusernameedit'); expect($usernameInput.val()).to.be(''); done(); @@ -112,119 +112,173 @@ describe("the test helper", function(){ }); }); - it("sets pad prefs cookie", function(done) { + it('sets pad prefs cookie', function (done) { this.timeout(60000); helper.newPad({ - padPrefs: {foo:"bar"}, - cb: function(){ - var chrome$ = helper.padChrome$; + padPrefs: {foo: 'bar'}, + cb() { + const chrome$ = helper.padChrome$; expect(chrome$.document.cookie).to.contain('prefsHttp=%7B%22'); expect(chrome$.document.cookie).to.contain('foo%22%3A%22bar'); done(); - } + }, }); }); }); - describe("the waitFor method", function(){ - it("takes a timeout and waits long enough", function(done){ + describe('the waitFor method', function () { + it('takes a timeout and waits long enough', function (done) { this.timeout(2000); - var startTime = Date.now(); + const startTime = Date.now(); - helper.waitFor(function(){ - return false; - }, 1500).fail(function(){ - var duration = Date.now() - startTime; - expect(duration).to.be.greaterThan(1400); + helper.waitFor(() => false, 1500).fail(() => { + const duration = Date.now() - startTime; + expect(duration).to.be.greaterThan(1490); done(); }); }); - it("takes an interval and checks on every interval", function(done){ + it('takes an interval and checks on every interval', function (done) { this.timeout(4000); - var checks = 0; + let checks = 0; - helper.waitFor(function(){ + helper.waitFor(() => { checks++; return false; - }, 2000, 100).fail(function(){ - expect(checks).to.be.greaterThan(10); - expect(checks).to.be.lessThan(30); + }, 2000, 100).fail(() => { + // One at the beginning, and 19-20 more depending on whether it's the timeout or the final + // poll that wins at 2000ms. + expect(checks).to.be.greaterThan(15); + expect(checks).to.be.lessThan(24); done(); }); }); - describe("returns a deferred object", function(){ - it("it calls done after success", function(done){ - helper.waitFor(function(){ - return true; - }).done(function(){ + it('rejects if the predicate throws', async function () { + let err; + await helper.waitFor(() => { throw new Error('test exception'); }) + .fail(() => {}) // Suppress the redundant uncatchable exception. + .catch((e) => { err = e; }); + expect(err).to.be.an(Error); + expect(err.message).to.be('test exception'); + }); + + describe('returns a deferred object', function () { + it('it calls done after success', function (done) { + helper.waitFor(() => true).done(() => { done(); }); }); - it("calls fail after failure", function(done){ - helper.waitFor(function(){ - return false; - },0).fail(function(){ + it('calls fail after failure', function (done) { + helper.waitFor(() => false, 0).fail(() => { done(); }); }); - xit("throws if you don't listen for fails", function(done){ - var onerror = window.onerror; - window.onerror = function(){ + xit("throws if you don't listen for fails", function (done) { + const onerror = window.onerror; + window.onerror = function () { window.onerror = onerror; done(); - } + }; - helper.waitFor(function(){ - return false; - },100); + helper.waitFor(() => false, 100); + }); + }); + + describe('checks first then sleeps', function () { + it('resolves quickly if the predicate is immediately true', async function () { + const before = Date.now(); + await helper.waitFor(() => true, 1000, 900); + expect(Date.now() - before).to.be.lessThan(800); + }); + + it('polls exactly once if timeout < interval', async function () { + let calls = 0; + await helper.waitFor(() => { calls++; }, 1, 1000) + .fail(() => {}) // Suppress the redundant uncatchable exception. + .catch(() => {}); // Don't throw an exception -- we know it rejects. + expect(calls).to.be(1); + }); + + it('resolves if condition is immediately true even if timeout is 0', async function () { + await helper.waitFor(() => true, 0); }); }); }); - describe("the selectLines method", function(){ + describe('the waitForPromise method', function () { + it('returns a Promise', async function () { + expect(helper.waitForPromise(() => true)).to.be.a(Promise); + }); + + it('takes a timeout and waits long enough', async function () { + this.timeout(2000); + const startTime = Date.now(); + let rejected; + await helper.waitForPromise(() => false, 1500) + .catch(() => { rejected = true; }); + expect(rejected).to.be(true); + const duration = Date.now() - startTime; + expect(duration).to.be.greaterThan(1490); + }); + + it('takes an interval and checks on every interval', async function () { + this.timeout(4000); + let checks = 0; + let rejected; + await helper.waitForPromise(() => { checks++; return false; }, 2000, 100) + .catch(() => { rejected = true; }); + expect(rejected).to.be(true); + // `checks` is expected to be 20 or 21: one at the beginning, plus 19 or 20 more depending on + // whether it's the timeout or the final poll that wins at 2000ms. Margin is added to reduce + // flakiness on slow test machines. + expect(checks).to.be.greaterThan(15); + expect(checks).to.be.lessThan(24); + }); + }); + + describe('the selectLines method', function () { // function to support tests, use a single way to represent whitespaces - var cleanText = function(text){ + const cleanText = function (text) { return text // IE replaces line breaks with a whitespace, so we need to unify its behavior // for other browsers, to have all tests running for all browsers - .replace(/\n/gi, "") - .replace(/\s/gi, " "); - } + .replace(/\n/gi, '') + .replace(/\s/gi, ' '); + }; - before(function(done){ - helper.newPad(function() { + before(function (done) { + helper.newPad(() => { // create some lines to be used on the tests - var $firstLine = helper.padInner$("div").first(); - $firstLine.sendkeys("{selectall}some{enter}short{enter}lines{enter}to test{enter}{enter}"); + const $firstLine = helper.padInner$('div').first(); + $firstLine.sendkeys('{selectall}some{enter}short{enter}lines{enter}to test{enter}{enter}'); // wait for lines to be split - helper.waitFor(function(){ - var $fourthLine = helper.padInner$("div").eq(3); - return $fourthLine.text() === "to test"; + helper.waitFor(() => { + const $fourthLine = helper.padInner$('div').eq(3); + return $fourthLine.text() === 'to test'; }).done(done); }); this.timeout(60000); }); - it("changes editor selection to be between startOffset of $startLine and endOffset of $endLine", function(done){ - var inner$ = helper.padInner$; + it('changes editor selection to be between startOffset of $startLine and endOffset of $endLine', function (done) { + const inner$ = helper.padInner$; - var startOffset = 2; - var endOffset = 4; + const startOffset = 2; + const endOffset = 4; - var $lines = inner$("div"); - var $startLine = $lines.eq(1); - var $endLine = $lines.eq(3); + const $lines = inner$('div'); + const $startLine = $lines.eq(1); + const $endLine = $lines.eq(3); helper.selectLines($startLine, $endLine, startOffset, endOffset); - var selection = inner$.document.getSelection(); + const selection = inner$.document.getSelection(); /* * replace() is required here because Firefox keeps the line breaks. @@ -233,24 +287,24 @@ describe("the test helper", function(){ * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm,""))).to.be("ort lines to t"); + expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to t'); done(); }); - it("ends selection at beginning of $endLine when it is an empty line", function(done){ - var inner$ = helper.padInner$; + it('ends selection at beginning of $endLine when it is an empty line', function (done) { + const inner$ = helper.padInner$; - var startOffset = 2; - var endOffset = 1; + const startOffset = 2; + const endOffset = 1; - var $lines = inner$("div"); - var $startLine = $lines.eq(1); - var $endLine = $lines.eq(4); + const $lines = inner$('div'); + const $startLine = $lines.eq(1); + const $endLine = $lines.eq(4); helper.selectLines($startLine, $endLine, startOffset, endOffset); - var selection = inner$.document.getSelection(); + const selection = inner$.document.getSelection(); /* * replace() is required here because Firefox keeps the line breaks. @@ -259,24 +313,24 @@ describe("the test helper", function(){ * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm,""))).to.be("ort lines to test"); + expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); - it("ends selection at beginning of $endLine when its offset is zero", function(done){ - var inner$ = helper.padInner$; + it('ends selection at beginning of $endLine when its offset is zero', function (done) { + const inner$ = helper.padInner$; - var startOffset = 2; - var endOffset = 0; + const startOffset = 2; + const endOffset = 0; - var $lines = inner$("div"); - var $startLine = $lines.eq(1); - var $endLine = $lines.eq(3); + const $lines = inner$('div'); + const $startLine = $lines.eq(1); + const $endLine = $lines.eq(3); helper.selectLines($startLine, $endLine, startOffset, endOffset); - var selection = inner$.document.getSelection(); + const selection = inner$.document.getSelection(); /* * replace() is required here because Firefox keeps the line breaks. @@ -285,24 +339,24 @@ describe("the test helper", function(){ * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm,""))).to.be("ort lines "); + expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines '); done(); }); - it("selects full line when offset is longer than line content", function(done){ - var inner$ = helper.padInner$; + it('selects full line when offset is longer than line content', function (done) { + const inner$ = helper.padInner$; - var startOffset = 2; - var endOffset = 50; + const startOffset = 2; + const endOffset = 50; - var $lines = inner$("div"); - var $startLine = $lines.eq(1); - var $endLine = $lines.eq(3); + const $lines = inner$('div'); + const $startLine = $lines.eq(1); + const $endLine = $lines.eq(3); helper.selectLines($startLine, $endLine, startOffset, endOffset); - var selection = inner$.document.getSelection(); + const selection = inner$.document.getSelection(); /* * replace() is required here because Firefox keeps the line breaks. @@ -311,21 +365,21 @@ describe("the test helper", function(){ * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm,""))).to.be("ort lines to test"); + expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); - it("selects all text between beginning of $startLine and end of $endLine when no offset is provided", function(done){ - var inner$ = helper.padInner$; + it('selects all text between beginning of $startLine and end of $endLine when no offset is provided', function (done) { + const inner$ = helper.padInner$; - var $lines = inner$("div"); - var $startLine = $lines.eq(1); - var $endLine = $lines.eq(3); + const $lines = inner$('div'); + const $startLine = $lines.eq(1); + const $endLine = $lines.eq(3); helper.selectLines($startLine, $endLine); - var selection = inner$.document.getSelection(); + const selection = inner$.document.getSelection(); /* * replace() is required here because Firefox keeps the line breaks. @@ -334,9 +388,73 @@ describe("the test helper", function(){ * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm,""))).to.be("short lines to test"); + expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); done(); }); }); + + describe('helper', function () { + before(function (cb) { + helper.newPad(() => { + cb(); + }); + }); + + it('.textLines() returns the text of the pad as strings', async function () { + const lines = helper.textLines(); + const defaultText = helper.defaultText(); + expect(Array.isArray(lines)).to.be(true); + expect(lines[0]).to.be.an('string'); + // @todo + // final "\n" is added automatically, but my understanding is this should happen + // only when the default text does not end with "\n" already + expect(`${lines.join('\n')}\n`).to.equal(defaultText); + }); + + it('.linesDiv() returns the text of the pad as div elements', async function () { + const lines = helper.linesDiv(); + const defaultText = helper.defaultText(); + expect(Array.isArray(lines)).to.be(true); + expect(lines[0]).to.be.an('object'); + expect(lines[0].text()).to.be.an('string'); + _.each(defaultText.split('\n'), (line, index) => { + // last line of default text + if (index === lines.length) { + expect(line).to.equal(''); + } else { + expect(lines[index].text()).to.equal(line); + } + }); + }); + + it('.edit() defaults to send an edit to the first line', async function () { + const firstLine = helper.textLines()[0]; + await helper.edit('line'); + expect(helper.textLines()[0]).to.be(`line${firstLine}`); + }); + + it('.edit() to the line specified with parameter lineNo', async function () { + const firstLine = helper.textLines()[0]; + await helper.edit('second line', 2); + + const text = helper.textLines(); + expect(text[0]).to.equal(firstLine); + expect(text[1]).to.equal('second line'); + }); + + it('.edit() supports sendkeys syntax ({selectall},{del},{enter})', async function () { + expect(helper.textLines()[0]).to.not.equal(''); + + // select first line + helper.linesDiv()[0].sendkeys('{selectall}'); + // delete first line + await helper.edit('{del}'); + + expect(helper.textLines()[0]).to.be(''); + const noOfLines = helper.textLines().length; + await helper.edit('{enter}'); + expect(helper.textLines().length).to.be(noOfLines + 1); + }); + }); }); diff --git a/tests/frontend/specs/importexport.js b/tests/frontend/specs/importexport.js index 3466f7cfbc4..0be2a07440e 100644 --- a/tests/frontend/specs/importexport.js +++ b/tests/frontend/specs/importexport.js @@ -1,176 +1,162 @@ -describe("import functionality", function(){ - beforeEach(function(cb){ +describe('import functionality', function () { + beforeEach(function (cb) { helper.newPad(cb); // creates a new pad this.timeout(60000); }); - function getinnertext(){ - var inner = helper.padInner$ - if(!inner){ - return "" + function getinnertext() { + const inner = helper.padInner$; + if (!inner) { + return ''; } - var newtext = "" - inner("div").each(function(line,el){ - newtext += el.innerHTML+"\n" - }) - return newtext + let newtext = ''; + inner('div').each((line, el) => { + newtext += `${el.innerHTML}\n`; + }); + return newtext; } - function importrequest(data,importurl,type){ - var success; - var error; - var result = $.ajax({ + function importrequest(data, importurl, type) { + let success; + let error; + const result = $.ajax({ url: importurl, - type: "post", + type: 'post', processData: false, async: false, contentType: 'multipart/form-data; boundary=boundary', accepts: { - text: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: 'Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.'+type+'"\r\nContent-Type: text/plain\r\n\r\n' + data + '\r\n\r\n--boundary', - error: function(res){ - error = res - } - }) - expect(error).to.be(undefined) - return result + data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + error(res) { + error = res; + }, + }); + expect(error).to.be(undefined); + return result; } - function exportfunc(link){ - var exportresults = [] + function exportfunc(link) { + const exportresults = []; $.ajaxSetup({ - async:false - }) - $.get(link+"/export/html",function(data){ - var start = data.indexOf("") - var end = data.indexOf("") - var html = data.substr(start+6,end-start-6) - exportresults.push(["html",html]) - }) - $.get(link+"/export/txt",function(data){ - exportresults.push(["txt",data]) - }) - return exportresults + async: false, + }); + $.get(`${link}/export/html`, (data) => { + const start = data.indexOf(''); + const end = data.indexOf(''); + const html = data.substr(start + 6, end - start - 6); + exportresults.push(['html', html]); + }); + $.get(`${link}/export/txt`, (data) => { + exportresults.push(['txt', data]); + }); + return exportresults; } - xit("import a pad with newlines from txt", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var textWithNewLines = 'imported text\nnewline' - importrequest(textWithNewLines,importurl,"txt") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('imported text\nnewline\n
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be("imported text
              newline

              ") - expect(results[1][1]).to.be("imported text\nnewline\n\n") - done() - }) - xit("import a pad with newlines from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithNewLines = 'htmltext
              newline' - importrequest(htmlWithNewLines,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('htmltext\nnewline\n
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be("htmltext
              newline

              ") - expect(results[1][1]).to.be("htmltext\nnewline\n\n") - done() - }) - xit("import a pad with attributes from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithNewLines = 'htmltext
              newline' - importrequest(htmlWithNewLines,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('htmltext\nnewline\n
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('htmltext
              newline

              ') - expect(results[1][1]).to.be('htmltext\nnewline\n\n') - done() - }) - xit("import a pad with bullets from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                • bullet2 line 2
              ' - importrequest(htmlWithBullets,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ + xit('import a pad with newlines from txt', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const textWithNewLines = 'imported text\nnewline'; + importrequest(textWithNewLines, importurl, 'txt'); + helper.waitFor(() => expect(getinnertext()).to.be('imported text\nnewline\n
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('imported text
              newline

              '); + expect(results[1][1]).to.be('imported text\nnewline\n\n'); + done(); + }); + xit('import a pad with newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithNewLines = 'htmltext
              newline'; + importrequest(htmlWithNewLines, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('htmltext
              newline

              '); + expect(results[1][1]).to.be('htmltext\nnewline\n\n'); + done(); + }); + xit('import a pad with attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithNewLines = 'htmltext
              newline'; + importrequest(htmlWithNewLines, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('htmltext
              newline

              '); + expect(results[1][1]).to.be('htmltext\nnewline\n\n'); + done(); + }); + xit('import a pad with bullets from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                • bullet2 line 2
              '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
              • bullet line 1
              \n\
              • bullet line 2
              \n\
              • bullet2 line 1
              \n\
              • bullet2 line 2
              \n\ -
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                • bullet2 line 2

              ') - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n') - done() - }) - xit("import a pad with bullets and newlines from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                • bullet2 line 2
              ' - importrequest(htmlWithBullets,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ +
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                • bullet2 line 2

              '); + expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n'); + done(); + }); + xit('import a pad with bullets and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                • bullet2 line 2
              '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
              • bullet line 1
              \n\
              \n\
              • bullet line 2
              \n\
              • bullet2 line 1
              \n\
              \n\
              • bullet2 line 2
              \n\ -
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                • bullet2 line 2

              ') - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n') - done() - }) - xit("import a pad with bullets and newlines and attributes from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis
              ' - importrequest(htmlWithBullets,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ +
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                • bullet2 line 2

              '); + expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n'); + done(); + }); + xit('import a pad with bullets and newlines and attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis
              '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
              • bullet line 1
              \n\
              \n\
              • bullet line 2
              \n\
              • bullet2 line 1
              \n
              \n\
              • bullet4 line 2 bisu
              \n\
              • bullet4 line 2 bs
              \n\
              • bullet4 line 2 uuis
              \n\ -
              \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis

              ') - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n') - done() - }) - xit("import a pad with nested bullets from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                    • bullet4 line 2
                    • bullet4 line 2
                    • bullet4 line 2
                  • bullet3 line 1
              • bullet2 line 1
              ' - importrequest(htmlWithBullets,importurl,"html") - var oldtext=getinnertext() - helper.waitFor(function(){ - return oldtext != getinnertext() -// return expect(getinnertext()).to.be('\ -//
              • bullet line 1
              \n\ -//
              • bullet line 2
              \n\ -//
              • bullet2 line 1
              \n\ -//
              • bullet4 line 2
              \n\ -//
              • bullet4 line 2
              \n\ -//
              • bullet4 line 2
              \n\ -//
              \n') - }) +
              \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis

              '); + expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n'); + done(); + }); + xit('import a pad with nested bullets from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                    • bullet4 line 2
                    • bullet4 line 2
                    • bullet4 line 2
                  • bullet3 line 1
              • bullet2 line 1
              '; + importrequest(htmlWithBullets, importurl, 'html'); + const oldtext = getinnertext(); + helper.waitFor(() => oldtext != getinnertext() + // return expect(getinnertext()).to.be('\ + //
              • bullet line 1
              \n\ + //
              • bullet line 2
              \n\ + //
              • bullet2 line 1
              \n\ + //
              • bullet4 line 2
              \n\ + //
              • bullet4 line 2
              \n\ + //
              • bullet4 line 2
              \n\ + //
              \n') + ); - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
              • bullet line 1
              • bullet line 2
                • bullet2 line 1
                    • bullet4 line 2
                    • bullet4 line 2
                    • bullet4 line 2
                  • bullet3 line 1
              • bullet2 line 1

              ') - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1\n\t* bullet2 line 1\n\n') - done() - }) - xit("import a pad with 8 levels of bullets and newlines and attributes from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
              • bullet line 1

              • bullet line 2
                • bullet2 line 1

                    • bullet4 line 2 bisu
                    • bullet4 line 2 bs
                    • bullet4 line 2 uuis
                            • foo
                            • foobar bs
                      • foobar
                ' - importrequest(htmlWithBullets,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                • bullet line 1
                • bullet line 2
                  • bullet2 line 1
                      • bullet4 line 2
                      • bullet4 line 2
                      • bullet4 line 2
                    • bullet3 line 1
                • bullet2 line 1

                '); + expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1\n\t* bullet2 line 1\n\n'); + done(); + }); + xit('import a pad with 8 levels of bullets and newlines and attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
                • bullet line 1

                • bullet line 2
                  • bullet2 line 1

                      • bullet4 line 2 bisu
                      • bullet4 line 2 bs
                      • bullet4 line 2 uuis
                              • foo
                              • foobar bs
                        • foobar
                  '; + importrequest(htmlWithBullets, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
                  • bullet line 1
                  \n\
                  \n\
                  • bullet line 2
                  \n\
                  • bullet2 line 1
                  \n
                  \n\ @@ -180,32 +166,31 @@ describe("import functionality", function(){
                  • foo
                  \n\
                  • foobar bs
                  \n\
                  • foobar
                  \n\ -
                  \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
                  • bullet line 1

                  • bullet line 2
                    • bullet2 line 1

                        • bullet4 line 2 bisu
                        • bullet4 line 2 bs
                        • bullet4 line 2 uuis
                                • foo
                                • foobar bs
                          • foobar

                  ') - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* foobar bs\n\t\t\t\t\t* foobar\n\n') - done() - }) +
                  \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                  • bullet line 1

                  • bullet line 2
                    • bullet2 line 1

                        • bullet4 line 2 bisu
                        • bullet4 line 2 bs
                        • bullet4 line 2 uuis
                                • foo
                                • foobar bs
                          • foobar

                  '); + expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* foobar bs\n\t\t\t\t\t* foobar\n\n'); + done(); + }); - xit("import a pad with ordered lists from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
                  1. number 1 line 1
                  1. number 2 line 2
                  ' - importrequest(htmlWithBullets,importurl,"html") - -console.error(getinnertext()) + xit('import a pad with ordered lists from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
                  1. number 1 line 1
                  1. number 2 line 2
                  '; + importrequest(htmlWithBullets, importurl, 'html'); + console.error(getinnertext()); expect(getinnertext()).to.be('\
                  1. number 1 line 1
                  \n\
                  1. number 2 line 2
                  \n\ -
                  \n') - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
                  1. number 1 line 1
                  1. number 2 line 2
                  ') - expect(results[1][1]).to.be('') - done() - }) - xit("import a pad with ordered lists and newlines from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
                  1. number 9 line 1

                  1. number 10 line 2
                    1. number 2 times line 1

                    1. number 2 times line 2
                  ' - importrequest(htmlWithBullets,importurl,"html") +
                  \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                  1. number 1 line 1
                  1. number 2 line 2
                  '); + expect(results[1][1]).to.be(''); + done(); + }); + xit('import a pad with ordered lists and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
                  1. number 9 line 1

                  1. number 10 line 2
                    1. number 2 times line 1

                    1. number 2 times line 2
                  '; + importrequest(htmlWithBullets, importurl, 'html'); expect(getinnertext()).to.be('\
                  1. number 9 line 1
                  \n\
                  \n\ @@ -213,15 +198,15 @@ describe("import functionality", function(){
                  1. number 2 times line 1
                  \n\
                  \n\
                  1. number 2 times line 2
                  \n\ -
                  \n') - var results = exportfunc(helper.padChrome$.window.location.href) - console.error(results) - done() - }) - xit("import a pad with nested ordered lists and attributes and newlines from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithBullets = '
                  1. bold strikethrough italics underline line 1bold

                  1. number 10 line 2
                    1. number 2 times line 1

                    1. number 2 times line 2
                  ' - importrequest(htmlWithBullets,importurl,"html") +
                  \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + console.error(results); + done(); + }); + xit('import a pad with nested ordered lists and attributes and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithBullets = '
                  1. bold strikethrough italics underline line 1bold

                  1. number 10 line 2
                    1. number 2 times line 1

                    1. number 2 times line 2
                  '; + importrequest(htmlWithBullets, importurl, 'html'); expect(getinnertext()).to.be('\
                  1. bold strikethrough italics underline line 1bold
                  \n\
                  \n\ @@ -229,9 +214,9 @@ describe("import functionality", function(){
                  1. number 2 times line 1
                  \n\
                  \n\
                  1. number 2 times line 2
                  \n\ -
                  \n') - var results = exportfunc(helper.padChrome$.window.location.href) - console.error(results) - done() - }) -}) +
                  \n'); + const results = exportfunc(helper.padChrome$.window.location.href); + console.error(results); + done(); + }); +}); diff --git a/tests/frontend/specs/importindents.js b/tests/frontend/specs/importindents.js index 284a90f3281..6209236df1c 100644 --- a/tests/frontend/specs/importindents.js +++ b/tests/frontend/specs/importindents.js @@ -1,97 +1,92 @@ -describe("import indents functionality", function(){ - beforeEach(function(cb){ +describe('import indents functionality', function () { + beforeEach(function (cb) { helper.newPad(cb); // creates a new pad this.timeout(60000); }); - function getinnertext(){ - var inner = helper.padInner$ - var newtext = "" - inner("div").each(function(line,el){ - newtext += el.innerHTML+"\n" - }) - return newtext + function getinnertext() { + const inner = helper.padInner$; + let newtext = ''; + inner('div').each((line, el) => { + newtext += `${el.innerHTML}\n`; + }); + return newtext; } - function importrequest(data,importurl,type){ - var success; - var error; - var result = $.ajax({ + function importrequest(data, importurl, type) { + let success; + let error; + const result = $.ajax({ url: importurl, - type: "post", + type: 'post', processData: false, async: false, contentType: 'multipart/form-data; boundary=boundary', accepts: { - text: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: 'Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.'+type+'"\r\nContent-Type: text/plain\r\n\r\n' + data + '\r\n\r\n--boundary', - error: function(res){ - error = res - } - }) - expect(error).to.be(undefined) - return result + data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + error(res) { + error = res; + }, + }); + expect(error).to.be(undefined); + return result; } - function exportfunc(link){ - var exportresults = [] + function exportfunc(link) { + const exportresults = []; $.ajaxSetup({ - async:false - }) - $.get(link+"/export/html",function(data){ - var start = data.indexOf("") - var end = data.indexOf("") - var html = data.substr(start+6,end-start-6) - exportresults.push(["html",html]) - }) - $.get(link+"/export/txt",function(data){ - exportresults.push(["txt",data]) - }) - return exportresults + async: false, + }); + $.get(`${link}/export/html`, (data) => { + const start = data.indexOf(''); + const end = data.indexOf(''); + const html = data.substr(start + 6, end - start - 6); + exportresults.push(['html', html]); + }); + $.get(`${link}/export/txt`, (data) => { + exportresults.push(['txt', data]); + }); + return exportresults; } - xit("import a pad with indents from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithIndents = '
                  • indent line 1
                  • indent line 2
                    • indent2 line 1
                    • indent2 line 2
                  ' - importrequest(htmlWithIndents,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ + xit('import a pad with indents from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithIndents = '
                  • indent line 1
                  • indent line 2
                    • indent2 line 1
                    • indent2 line 2
                  '; + importrequest(htmlWithIndents, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
                  • indent line 1
                  \n\
                  • indent line 2
                  \n\
                  • indent2 line 1
                  \n\
                  • indent2 line 2
                  \n\ -
                  \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
                  • indent line 1
                  • indent line 2
                    • indent2 line 1
                    • indent2 line 2

                  ') - expect(results[1][1]).to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n') - done() - }) +
                  \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                  • indent line 1
                  • indent line 2
                    • indent2 line 1
                    • indent2 line 2

                  '); + expect(results[1][1]).to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n'); + done(); + }); - xit("import a pad with indented lists and newlines from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithIndents = '
                  • indent line 1

                  • indent 1 line 2
                    • indent 2 times line 1

                    • indent 2 times line 2
                  ' - importrequest(htmlWithIndents,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ + xit('import a pad with indented lists and newlines from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithIndents = '
                  • indent line 1

                  • indent 1 line 2
                    • indent 2 times line 1

                    • indent 2 times line 2
                  '; + importrequest(htmlWithIndents, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
                  • indent line 1
                  \n\
                  \n\
                  • indent 1 line 2
                  \n\
                  • indent 2 times line 1
                  \n\
                  \n\
                  • indent 2 times line 2
                  \n\ -
                  \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
                  • indent line 1

                  • indent 1 line 2
                    • indent 2 times line 1

                    • indent 2 times line 2

                  ') - expect(results[1][1]).to.be('\tindent line 1\n\n\tindent 1 line 2\n\t\tindent 2 times line 1\n\n\t\tindent 2 times line 2\n\n') - done() - }) - xit("import a pad with 8 levels of indents and newlines and attributes from html", function(done){ - var importurl = helper.padChrome$.window.location.href+'/import' - var htmlWithIndents = '
                  • indent line 1

                  • indent line 2
                    • indent2 line 1

                        • indent4 line 2 bisu
                        • indent4 line 2 bs
                        • indent4 line 2 uuis
                                • foo
                                • foobar bs
                          • foobar
                    ' - importrequest(htmlWithIndents,importurl,"html") - helper.waitFor(function(){ - return expect(getinnertext()).to.be('\ +
                    \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                    • indent line 1

                    • indent 1 line 2
                      • indent 2 times line 1

                      • indent 2 times line 2

                    '); + expect(results[1][1]).to.be('\tindent line 1\n\n\tindent 1 line 2\n\t\tindent 2 times line 1\n\n\t\tindent 2 times line 2\n\n'); + done(); + }); + xit('import a pad with 8 levels of indents and newlines and attributes from html', function (done) { + const importurl = `${helper.padChrome$.window.location.href}/import`; + const htmlWithIndents = '
                    • indent line 1

                    • indent line 2
                      • indent2 line 1

                          • indent4 line 2 bisu
                          • indent4 line 2 bs
                          • indent4 line 2 uuis
                                  • foo
                                  • foobar bs
                            • foobar
                      '; + importrequest(htmlWithIndents, importurl, 'html'); + helper.waitFor(() => expect(getinnertext()).to.be('\
                      • indent line 1
                      \n\
                      \n\
                      • indent line 2
                      \n\
                      • indent2 line 1
                      \n
                      \n\ @@ -101,11 +96,10 @@ describe("import indents functionality", function(){
                      • foo
                      \n\
                      • foobar bs
                      \n\
                      • foobar
                      \n\ -
                      \n') - }) - var results = exportfunc(helper.padChrome$.window.location.href) - expect(results[0][1]).to.be('
                      • indent line 1

                      • indent line 2
                        • indent2 line 1

                            • indent4 line 2 bisu
                            • indent4 line 2 bs
                            • indent4 line 2 uuis
                                    • foo
                                    • foobar bs
                              • foobar

                      ') - expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n') - done() - }) -}) +
                      \n')); + const results = exportfunc(helper.padChrome$.window.location.href); + expect(results[0][1]).to.be('
                      • indent line 1

                      • indent line 2
                        • indent2 line 1

                            • indent4 line 2 bisu
                            • indent4 line 2 bs
                            • indent4 line 2 uuis
                                    • foo
                                    • foobar bs
                              • foobar

                      '); + expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n'); + done(); + }); +}); diff --git a/tests/frontend/specs/indentation.js b/tests/frontend/specs/indentation.js index 2173f9e07ff..f35b7ca0036 100644 --- a/tests/frontend/specs/indentation.js +++ b/tests/frontend/specs/indentation.js @@ -1,178 +1,164 @@ -describe("indentation button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('indentation button', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("indent text with keypress", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent text with keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.keyCode = 9; // tab :| - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - helper.waitFor(function(){ - return inner$("div").first().find("ul li").length === 1; - }).done(done); + helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(done); }); - it("indent text with button", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent text with button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $indentButton = chrome$(".buttonicon-indent"); + const $indentButton = chrome$('.buttonicon-indent'); $indentButton.click(); - helper.waitFor(function(){ - return inner$("div").first().find("ul li").length === 1; - }).done(done); + helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(done); }); - it("keeps the indent on enter for the new line", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('keeps the indent on enter for the new line', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $indentButton = chrome$(".buttonicon-indent"); + const $indentButton = chrome$('.buttonicon-indent'); $indentButton.click(); - //type a bit, make a line break and type again - var $firstTextElement = inner$("div span").first(); + // type a bit, make a line break and type again + const $firstTextElement = inner$('div span').first(); $firstTextElement.sendkeys('line 1'); $firstTextElement.sendkeys('{enter}'); $firstTextElement.sendkeys('line 2'); $firstTextElement.sendkeys('{enter}'); - helper.waitFor(function(){ - return inner$("div span").first().text().indexOf("line 2") === -1; - }).done(function(){ - var $newSecondLine = inner$("div").first().next(); - var hasULElement = $newSecondLine.find("ul li").length === 1; + helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { + const $newSecondLine = inner$('div').first().next(); + const hasULElement = $newSecondLine.find('ul li').length === 1; expect(hasULElement).to.be(true); - expect($newSecondLine.text()).to.be("line 2"); + expect($newSecondLine.text()).to.be('line 2'); done(); }); }); - it("indents text with spaces on enter if previous line ends with ':', '[', '(', or '{'", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("indents text with spaces on enter if previous line ends with ':', '[', '(', or '{'", function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //type a bit, make a line break and type again - var $firstTextElement = inner$("div").first(); + // type a bit, make a line break and type again + const $firstTextElement = inner$('div').first(); $firstTextElement.sendkeys("line with ':'{enter}"); $firstTextElement.sendkeys("line with '['{enter}"); $firstTextElement.sendkeys("line with '('{enter}"); $firstTextElement.sendkeys("line with '{{}'{enter}"); - helper.waitFor(function(){ + helper.waitFor(() => { // wait for Etherpad to split four lines into separated divs - var $fourthLine = inner$("div").first().next().next().next(); + const $fourthLine = inner$('div').first().next().next().next(); return $fourthLine.text().indexOf("line with '{'") === 0; - }).done(function(){ + }).done(() => { // we validate bottom to top for easier implementation // curly braces - var $lineWithCurlyBraces = inner$("div").first().next().next().next(); + const $lineWithCurlyBraces = inner$('div').first().next().next().next(); $lineWithCurlyBraces.sendkeys('{{}'); pressEnter(); // cannot use sendkeys('{enter}') here, browser does not read the command properly - var $lineAfterCurlyBraces = inner$("div").first().next().next().next().next(); + const $lineAfterCurlyBraces = inner$('div').first().next().next().next().next(); expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces // parenthesis - var $lineWithParenthesis = inner$("div").first().next().next(); + const $lineWithParenthesis = inner$('div').first().next().next(); $lineWithParenthesis.sendkeys('('); pressEnter(); - var $lineAfterParenthesis = inner$("div").first().next().next().next(); + const $lineAfterParenthesis = inner$('div').first().next().next().next(); expect($lineAfterParenthesis.text()).to.match(/\s{4}/); // bracket - var $lineWithBracket = inner$("div").first().next(); + const $lineWithBracket = inner$('div').first().next(); $lineWithBracket.sendkeys('['); pressEnter(); - var $lineAfterBracket = inner$("div").first().next().next(); + const $lineAfterBracket = inner$('div').first().next().next(); expect($lineAfterBracket.text()).to.match(/\s{4}/); // colon - var $lineWithColon = inner$("div").first(); + const $lineWithColon = inner$('div').first(); $lineWithColon.sendkeys(':'); pressEnter(); - var $lineAfterColon = inner$("div").first().next(); + const $lineAfterColon = inner$('div').first().next(); expect($lineAfterColon.text()).to.match(/\s{4}/); done(); }); }); - it("appends indentation to the indent of previous line if previous line ends with ':', '[', '(', or '{'", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("appends indentation to the indent of previous line if previous line ends with ':', '[', '(', or '{'", function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //type a bit, make a line break and type again - var $firstTextElement = inner$("div").first(); + // type a bit, make a line break and type again + const $firstTextElement = inner$('div').first(); $firstTextElement.sendkeys(" line with some indentation and ':'{enter}"); - $firstTextElement.sendkeys("line 2{enter}"); + $firstTextElement.sendkeys('line 2{enter}'); - helper.waitFor(function(){ + helper.waitFor(() => { // wait for Etherpad to split two lines into separated divs - var $secondLine = inner$("div").first().next(); - return $secondLine.text().indexOf("line 2") === 0; - }).done(function(){ - var $lineWithColon = inner$("div").first(); + const $secondLine = inner$('div').first().next(); + return $secondLine.text().indexOf('line 2') === 0; + }).done(() => { + const $lineWithColon = inner$('div').first(); $lineWithColon.sendkeys(':'); pressEnter(); - var $lineAfterColon = inner$("div").first().next(); + const $lineAfterColon = inner$('div').first().next(); expect($lineAfterColon.text()).to.match(/\s{6}/); // previous line indentation + regular tab (4 spaces) done(); }); }); - it("issue #2772 shows '*' when multiple indented lines receive a style and are outdented", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("issue #2772 shows '*' when multiple indented lines receive a style and are outdented", function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // make sure pad has more than one line - inner$("div").first().sendkeys("First{enter}Second{enter}"); - helper.waitFor(function(){ - return inner$("div").first().text().trim() === "First"; - }).done(function(){ + inner$('div').first().sendkeys('First{enter}Second{enter}'); + helper.waitFor(() => inner$('div').first().text().trim() === 'First').done(() => { // indent first 2 lines - var $lines = inner$("div"); - var $firstLine = $lines.first(); - var $secondLine = $lines.slice(1,2); + const $lines = inner$('div'); + const $firstLine = $lines.first(); + const $secondLine = $lines.slice(1, 2); helper.selectLines($firstLine, $secondLine); - var $indentButton = chrome$(".buttonicon-indent"); + const $indentButton = chrome$('.buttonicon-indent'); $indentButton.click(); - helper.waitFor(function(){ - return inner$("div").first().find("ul li").length === 1; - }).done(function(){ + helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(() => { // apply bold - var $boldButton = chrome$(".buttonicon-bold"); + const $boldButton = chrome$('.buttonicon-bold'); $boldButton.click(); - helper.waitFor(function(){ - return inner$("div").first().find("b").length === 1; - }).done(function(){ + helper.waitFor(() => inner$('div').first().find('b').length === 1).done(() => { // outdent first 2 lines - var $outdentButton = chrome$(".buttonicon-outdent"); + const $outdentButton = chrome$('.buttonicon-outdent'); $outdentButton.click(); - helper.waitFor(function(){ - return inner$("div").first().find("ul li").length === 0; - }).done(function(){ + helper.waitFor(() => inner$('div').first().find('ul li').length === 0).done(() => { // check if '*' is displayed - var $secondLine = inner$("div").slice(1,2); - expect($secondLine.text().trim()).to.be("Second"); + const $secondLine = inner$('div').slice(1, 2); + expect($secondLine.text().trim()).to.be('Second'); done(); }); @@ -314,12 +300,11 @@ describe("indentation button", function(){ expect(isLI).to.be(true); },1000); });*/ - }); -function pressEnter(){ - var inner$ = helper.padInner$; - var e = inner$.Event(helper.evtType); +function pressEnter() { + const inner$ = helper.padInner$; + const e = inner$.Event(helper.evtType); e.keyCode = 13; // enter :| - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); } diff --git a/tests/frontend/specs/italic.js b/tests/frontend/specs/italic.js index 3c62e00e86b..3660f71f3a2 100644 --- a/tests/frontend/specs/italic.js +++ b/tests/frontend/specs/italic.js @@ -1,67 +1,66 @@ -describe("italic some text", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('italic some text', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text italic using button", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text italic using button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - //get the bold button and click it - var $boldButton = chrome$(".buttonicon-italic"); + // get the bold button and click it + const $boldButton = chrome$('.buttonicon-italic'); $boldButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a button, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // is there a element now? - var isItalic = $newFirstTextElement.find("i").length === 1; + const isItalic = $newFirstTextElement.find('i').length === 1; - //expect it to be bold + // expect it to be bold expect(isItalic).to.be(true); - //make sure the text hasn't changed + // make sure the text hasn't changed expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); done(); }); - it("makes text italic using keypress", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text italic using keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 105; // i - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a button, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // is there a element now? - var isItalic = $newFirstTextElement.find("i").length === 1; + const isItalic = $newFirstTextElement.find('i').length === 1; - //expect it to be bold + // expect it to be bold expect(isItalic).to.be(true); - //make sure the text hasn't changed + // make sure the text hasn't changed expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); done(); }); - }); diff --git a/tests/frontend/specs/language.js b/tests/frontend/specs/language.js index 41a19d11cc0..d29b2407e00 100644 --- a/tests/frontend/specs/language.js +++ b/tests/frontend/specs/language.js @@ -1,135 +1,127 @@ function deletecookie(name) { - document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; } -describe("Language select and change", function(){ +describe('Language select and change', function () { // Destroy language cookies - deletecookie("language", null); + deletecookie('language', null); - //create a new pad before each test run - beforeEach(function(cb){ + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); // Destroy language cookies - it("makes text german", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text german', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); + // click on the settings button to make settings visible + const $settingsButton = chrome$('.buttonicon-settings'); $settingsButton.click(); - //click the language button - var $language = chrome$("#languagemenu"); - var $languageoption = $language.find("[value=de]"); + // click the language button + const $language = chrome$('#languagemenu'); + const $languageoption = $language.find('[value=de]'); - //select german - $languageoption.attr('selected','selected'); + // select german + $languageoption.attr('selected', 'selected'); $language.change(); - helper.waitFor(function() { - return chrome$(".buttonicon-bold").parent()[0]["title"] == "Fett (Strg-B)"; - }) - .done(function(){ - //get the value of the bold button - var $boldButton = chrome$(".buttonicon-bold").parent(); + helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title == 'Fett (Strg-B)') + .done(() => { + // get the value of the bold button + const $boldButton = chrome$('.buttonicon-bold').parent(); - //get the title of the bold button - var boldButtonTitle = $boldButton[0]["title"]; + // get the title of the bold button + const boldButtonTitle = $boldButton[0].title; - //check if the language is now german - expect(boldButtonTitle).to.be("Fett (Strg-B)"); - done(); - }); + // check if the language is now german + expect(boldButtonTitle).to.be('Fett (Strg-B)'); + done(); + }); }); - it("makes text English", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text English', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); + // click on the settings button to make settings visible + const $settingsButton = chrome$('.buttonicon-settings'); $settingsButton.click(); - //click the language button - var $language = chrome$("#languagemenu"); - //select english - $language.val("en"); + // click the language button + const $language = chrome$('#languagemenu'); + // select english + $language.val('en'); $language.change(); - //get the value of the bold button - var $boldButton = chrome$(".buttonicon-bold").parent(); + // get the value of the bold button + const $boldButton = chrome$('.buttonicon-bold').parent(); - helper.waitFor(function() { return $boldButton[0]["title"] != "Fett (Strg+B)";}) - .done(function(){ + helper.waitFor(() => $boldButton[0].title != 'Fett (Strg+B)') + .done(() => { + // get the value of the bold button + const $boldButton = chrome$('.buttonicon-bold').parent(); - //get the value of the bold button - var $boldButton = chrome$(".buttonicon-bold").parent(); + // get the title of the bold button + const boldButtonTitle = $boldButton[0].title; - //get the title of the bold button - var boldButtonTitle = $boldButton[0]["title"]; - - //check if the language is now English - expect(boldButtonTitle).to.be("Bold (Ctrl+B)"); - done(); - - }); + // check if the language is now English + expect(boldButtonTitle).to.be('Bold (Ctrl+B)'); + done(); + }); }); - it("changes direction when picking an rtl lang", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('changes direction when picking an rtl lang', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); + // click on the settings button to make settings visible + const $settingsButton = chrome$('.buttonicon-settings'); $settingsButton.click(); - //click the language button - var $language = chrome$("#languagemenu"); - var $languageoption = $language.find("[value=ar]"); + // click the language button + const $language = chrome$('#languagemenu'); + const $languageoption = $language.find('[value=ar]'); - //select arabic + // select arabic // $languageoption.attr('selected','selected'); // Breaks the test.. - $language.val("ar"); + $language.val('ar'); $languageoption.change(); - helper.waitFor(function() { - return chrome$("html")[0]["dir"] != 'ltr'; - }) - .done(function(){ - // check if the document's direction was changed - expect(chrome$("html")[0]["dir"]).to.be("rtl"); - done(); - }); + helper.waitFor(() => chrome$('html')[0].dir != 'ltr') + .done(() => { + // check if the document's direction was changed + expect(chrome$('html')[0].dir).to.be('rtl'); + done(); + }); }); - it("changes direction when picking an ltr lang", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('changes direction when picking an ltr lang', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); + // click on the settings button to make settings visible + const $settingsButton = chrome$('.buttonicon-settings'); $settingsButton.click(); - //click the language button - var $language = chrome$("#languagemenu"); - var $languageoption = $language.find("[value=en]"); + // click the language button + const $language = chrome$('#languagemenu'); + const $languageoption = $language.find('[value=en]'); - //select english - //select arabic - $languageoption.attr('selected','selected'); - $language.val("en"); + // select english + // select arabic + $languageoption.attr('selected', 'selected'); + $language.val('en'); $languageoption.change(); - helper.waitFor(function() { - return chrome$("html")[0]["dir"] != 'rtl'; - }) - .done(function(){ - // check if the document's direction was changed - expect(chrome$("html")[0]["dir"]).to.be("ltr"); - done(); - }); + helper.waitFor(() => chrome$('html')[0].dir != 'rtl') + .done(() => { + // check if the document's direction was changed + expect(chrome$('html')[0].dir).to.be('ltr'); + done(); + }); }); }); diff --git a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index 58c93cf2f72..f532ea4be7d 100755 --- a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -1,51 +1,49 @@ -describe('author of pad edition', function() { - // author 1 creates a new pad with some content (regular lines and lists) - before(function(done) { - var padId = helper.newPad(function() { - // make sure pad has at least 3 lines - var $firstLine = helper.padInner$('div').first(); - $firstLine.html("Hello World"); - - // wait for lines to be processed by Etherpad - helper.waitFor(function() { - return $firstLine.text() === 'Hello World'; - }).done(function() { - // Reload pad, to make changes as a second user. Need a timeout here to make sure - // all changes were saved before reloading - setTimeout(function() { - // Expire cookie, so author is changed after reloading the pad. - // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; - - helper.newPad(done, padId); - }, 1000); - }); - }); - this.timeout(60000); - }); - - // author 2 makes some changes on the pad - it('Clears Authorship by second user', function(done) { - clearAuthorship(done); - }); - - var clearAuthorship = function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // override the confirm dialogue functioon - helper.padChrome$.window.confirm = function(){ - return true; - } - - //get the clear authorship colors button and click it - var $clearauthorshipcolorsButton = chrome$(".buttonicon-clearauthorship"); - $clearauthorshipcolorsButton.click(); - - // does the first divs span include an author class? - var hasAuthorClass = inner$("div span").first().attr("class").indexOf("author") !== -1; - - expect(hasAuthorClass).to.be(false) - done(); - } -}); +describe('author of pad edition', function () { + // author 1 creates a new pad with some content (regular lines and lists) + before(function (done) { + var padId = helper.newPad(() => { + // make sure pad has at least 3 lines + const $firstLine = helper.padInner$('div').first(); + $firstLine.html('Hello World'); + + // wait for lines to be processed by Etherpad + helper.waitFor(() => $firstLine.text() === 'Hello World').done(() => { + // Reload pad, to make changes as a second user. Need a timeout here to make sure + // all changes were saved before reloading + setTimeout(() => { + // Expire cookie, so author is changed after reloading the pad. + // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie + helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + + helper.newPad(done, padId); + }, 1000); + }); + }); + this.timeout(60000); + }); + + // author 2 makes some changes on the pad + it('Clears Authorship by second user', function (done) { + clearAuthorship(done); + }); + + var clearAuthorship = function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + + // override the confirm dialogue functioon + helper.padChrome$.window.confirm = function () { + return true; + }; + + // get the clear authorship colors button and click it + const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); + $clearauthorshipcolorsButton.click(); + + // does the first divs span include an author class? + const hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; + + expect(hasAuthorClass).to.be(false); + done(); + }; +}); diff --git a/tests/frontend/specs/ordered_list.js b/tests/frontend/specs/ordered_list.js index 4f81c456415..a932335e85c 100644 --- a/tests/frontend/specs/ordered_list.js +++ b/tests/frontend/specs/ordered_list.js @@ -1,204 +1,180 @@ -describe("assign ordered list", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('assign ordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("inserts ordered list text", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('inserts ordered list text', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - helper.waitFor(function(){ - return inner$("div").first().find("ol li").length === 1; - }).done(done); + helper.waitFor(() => inner$('div').first().find('ol li').length === 1).done(done); }); - context('when user presses Ctrl+Shift+N', function() { - context('and pad shortcut is enabled', function() { - beforeEach(function() { + context('when user presses Ctrl+Shift+N', function () { + context('and pad shortcut is enabled', function () { + beforeEach(function () { makeSureShortcutIsEnabled('cmdShiftN'); triggerCtrlShiftShortcut('N'); }); - it('inserts unordered list', function(done) { - helper.waitFor(function() { - return helper.padInner$('div').first().find('ol li').length === 1; - }).done(done); + it('inserts unordered list', function (done) { + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done); }); }); - context('and pad shortcut is disabled', function() { - beforeEach(function() { + context('and pad shortcut is disabled', function () { + beforeEach(function () { makeSureShortcutIsDisabled('cmdShiftN'); triggerCtrlShiftShortcut('N'); }); - it('does not insert unordered list', function(done) { - helper.waitFor(function() { - return helper.padInner$('div').first().find('ol li').length === 1; - }).done(function() { - expect().fail(function() { return 'Unordered list inserted, should ignore shortcut' }); - }).fail(function() { + it('does not insert unordered list', function (done) { + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + expect().fail(() => 'Unordered list inserted, should ignore shortcut'); + }).fail(() => { done(); }); }); }); }); - context('when user presses Ctrl+Shift+1', function() { - context('and pad shortcut is enabled', function() { - beforeEach(function() { + context('when user presses Ctrl+Shift+1', function () { + context('and pad shortcut is enabled', function () { + beforeEach(function () { makeSureShortcutIsEnabled('cmdShift1'); triggerCtrlShiftShortcut('1'); }); - it('inserts unordered list', function(done) { - helper.waitFor(function() { - return helper.padInner$('div').first().find('ol li').length === 1; - }).done(done); + it('inserts unordered list', function (done) { + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done); }); }); - context('and pad shortcut is disabled', function() { - beforeEach(function() { + context('and pad shortcut is disabled', function () { + beforeEach(function () { makeSureShortcutIsDisabled('cmdShift1'); triggerCtrlShiftShortcut('1'); }); - it('does not insert unordered list', function(done) { - helper.waitFor(function() { - return helper.padInner$('div').first().find('ol li').length === 1; - }).done(function() { - expect().fail(function() { return 'Unordered list inserted, should ignore shortcut' }); - }).fail(function() { + it('does not insert unordered list', function (done) { + helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + expect().fail(() => 'Unordered list inserted, should ignore shortcut'); + }).fail(() => { done(); }); }); }); }); - xit("issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + xit('issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - //type a bit, make a line break and type again - var $firstTextElement = inner$("div span").first(); + // type a bit, make a line break and type again + const $firstTextElement = inner$('div span').first(); $firstTextElement.sendkeys('line 1'); $firstTextElement.sendkeys('{enter}'); $firstTextElement.sendkeys('line 2'); $firstTextElement.sendkeys('{enter}'); - helper.waitFor(function(){ - return inner$("div span").first().text().indexOf("line 2") === -1; - }).done(function(){ - var $newSecondLine = inner$("div").first().next(); - var hasOLElement = $newSecondLine.find("ol li").length === 1; + helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { + const $newSecondLine = inner$('div').first().next(); + const hasOLElement = $newSecondLine.find('ol li').length === 1; expect(hasOLElement).to.be(true); - expect($newSecondLine.text()).to.be("line 2"); - var hasLineNumber = $newSecondLine.find("ol").attr("start") === 2; + expect($newSecondLine.text()).to.be('line 2'); + const hasLineNumber = $newSecondLine.find('ol').attr('start') === 2; expect(hasLineNumber).to.be(true); // This doesn't work because pasting in content doesn't work done(); }); }); - var triggerCtrlShiftShortcut = function(shortcutChar) { - var inner$ = helper.padInner$; - var e = inner$.Event(helper.evtType); + var triggerCtrlShiftShortcut = function (shortcutChar) { + const inner$ = helper.padInner$; + const e = inner$.Event(helper.evtType); e.ctrlKey = true; e.shiftKey = true; e.which = shortcutChar.toString().charCodeAt(0); - inner$("#innerdocbody").trigger(e); - } + inner$('#innerdocbody').trigger(e); + }; - var makeSureShortcutIsDisabled = function(shortcut) { + var makeSureShortcutIsDisabled = function (shortcut) { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; - } - var makeSureShortcutIsEnabled = function(shortcut) { + }; + var makeSureShortcutIsEnabled = function (shortcut) { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true; - } - + }; }); -describe("Pressing Tab in an OL increases and decreases indentation", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('Pressing Tab in an OL increases and decreases indentation', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("indent and de-indent list item with keypress", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent and de-indent list item with keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.keyCode = 9; // tab - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - expect(inner$("div").first().find(".list-number2").length === 1).to.be(true); + expect(inner$('div').first().find('.list-number2').length === 1).to.be(true); e.shiftKey = true; // shift e.keyCode = 9; // tab - inner$("#innerdocbody").trigger(e); - - helper.waitFor(function(){ - return inner$("div").first().find(".list-number1").length === 1; - }).done(done); + inner$('#innerdocbody').trigger(e); + helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); }); - - }); -describe("Pressing indent/outdent button in an OL increases and decreases indentation and bullet / ol formatting", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('Pressing indent/outdent button in an OL increases and decreases indentation and bullet / ol formatting', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("indent and de-indent list item with indent button", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent and de-indent list item with indent button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var $insertorderedlistButton = chrome$(".buttonicon-insertorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - var $indentButton = chrome$(".buttonicon-indent"); + const $indentButton = chrome$('.buttonicon-indent'); $indentButton.click(); // make it indented twice - expect(inner$("div").first().find(".list-number2").length === 1).to.be(true); + expect(inner$('div').first().find('.list-number2').length === 1).to.be(true); - var $outdentButton = chrome$(".buttonicon-outdent"); + const $outdentButton = chrome$('.buttonicon-outdent'); $outdentButton.click(); // make it deindented to 1 - helper.waitFor(function(){ - return inner$("div").first().find(".list-number1").length === 1; - }).done(done); - + helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done); }); - - }); - diff --git a/tests/frontend/specs/pad_modal.js b/tests/frontend/specs/pad_modal.js index 87738173791..1711e38b80c 100644 --- a/tests/frontend/specs/pad_modal.js +++ b/tests/frontend/specs/pad_modal.js @@ -1,36 +1,34 @@ -describe('Pad modal', function() { - context('when modal is a "force reconnect" message', function() { - var MODAL_SELECTOR = '#connectivity'; +describe('Pad modal', function () { + context('when modal is a "force reconnect" message', function () { + const MODAL_SELECTOR = '#connectivity'; - beforeEach(function(done) { - helper.newPad(function() { + beforeEach(function (done) { + helper.newPad(() => { // force a "slowcommit" error helper.padChrome$.window.pad.handleChannelStateChange('DISCONNECTED', 'slowcommit'); // wait for modal to be displayed - var $modal = helper.padChrome$(MODAL_SELECTOR); - helper.waitFor(function() { - return $modal.hasClass('popup-show'); - }, 50000).done(done); + const $modal = helper.padChrome$(MODAL_SELECTOR); + helper.waitFor(() => $modal.hasClass('popup-show'), 50000).done(done); }); this.timeout(60000); }); - it('disables editor', function(done) { + it('disables editor', function (done) { expect(isEditorDisabled()).to.be(true); done(); }); - context('and user clicks on editor', function() { - beforeEach(function() { + context('and user clicks on editor', function () { + beforeEach(function () { clickOnPadInner(); }); - it('does not close the modal', function(done) { - var $modal = helper.padChrome$(MODAL_SELECTOR); - var modalIsVisible = $modal.hasClass('popup-show'); + it('does not close the modal', function (done) { + const $modal = helper.padChrome$(MODAL_SELECTOR); + const modalIsVisible = $modal.hasClass('popup-show'); expect(modalIsVisible).to.be(true); @@ -38,14 +36,14 @@ describe('Pad modal', function() { }); }); - context('and user clicks on pad outer', function() { - beforeEach(function() { + context('and user clicks on pad outer', function () { + beforeEach(function () { clickOnPadOuter(); }); - it('does not close the modal', function(done) { - var $modal = helper.padChrome$(MODAL_SELECTOR); - var modalIsVisible = $modal.hasClass('popup-show'); + it('does not close the modal', function (done) { + const $modal = helper.padChrome$(MODAL_SELECTOR); + const modalIsVisible = $modal.hasClass('popup-show'); expect(modalIsVisible).to.be(true); @@ -55,79 +53,77 @@ describe('Pad modal', function() { }); // we use "settings" here, but other modals have the same behaviour - context('when modal is not an error message', function() { - var MODAL_SELECTOR = '#settings'; + context('when modal is not an error message', function () { + const MODAL_SELECTOR = '#settings'; - beforeEach(function(done) { - helper.newPad(function() { + beforeEach(function (done) { + helper.newPad(() => { openSettingsAndWaitForModalToBeVisible(done); }); this.timeout(60000); }); // This test breaks safari testing -/* + /* it('does not disable editor', function(done) { expect(isEditorDisabled()).to.be(false); done(); }); */ - context('and user clicks on editor', function() { - beforeEach(function() { + context('and user clicks on editor', function () { + beforeEach(function () { clickOnPadInner(); }); - it('closes the modal', function(done) { + it('closes the modal', function (done) { expect(isModalOpened(MODAL_SELECTOR)).to.be(false); done(); }); }); - context('and user clicks on pad outer', function() { - beforeEach(function() { + context('and user clicks on pad outer', function () { + beforeEach(function () { clickOnPadOuter(); }); - it('closes the modal', function(done) { + it('closes the modal', function (done) { expect(isModalOpened(MODAL_SELECTOR)).to.be(false); done(); }); }); }); - var clickOnPadInner = function() { - var $editor = helper.padInner$('#innerdocbody'); + var clickOnPadInner = function () { + const $editor = helper.padInner$('#innerdocbody'); $editor.click(); - } + }; - var clickOnPadOuter = function() { - var $lineNumbersColumn = helper.padOuter$('#sidedivinner'); + var clickOnPadOuter = function () { + const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); $lineNumbersColumn.click(); - } + }; - var openSettingsAndWaitForModalToBeVisible = function(done) { + var openSettingsAndWaitForModalToBeVisible = function (done) { helper.padChrome$('.buttonicon-settings').click(); // wait for modal to be displayed - var modalSelector = '#settings'; - helper.waitFor(function() { - return isModalOpened(modalSelector); - }, 10000).done(done); - } + const modalSelector = '#settings'; + helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done); + }; - var isEditorDisabled = function() { - var editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; - var editorBody = editorDocument.getElementById('innerdocbody'); + var isEditorDisabled = function () { + const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; + const editorBody = editorDocument.getElementById('innerdocbody'); - var editorIsDisabled = editorBody.contentEditable === 'false' // IE/Safari - || editorDocument.designMode === 'off'; // other browsers + const editorIsDisabled = editorBody.contentEditable === 'false' || // IE/Safari + editorDocument.designMode === 'off'; // other browsers return editorIsDisabled; - } + }; - var isModalOpened = function(modalSelector) { - var $modal = helper.padChrome$(modalSelector); + var isModalOpened = function (modalSelector) { + const $modal = helper.padChrome$(modalSelector); return $modal.hasClass('popup-show'); - } + }; }); diff --git a/tests/frontend/specs/redo.js b/tests/frontend/specs/redo.js index be85d44c351..58d5b6c1261 100644 --- a/tests/frontend/specs/redo.js +++ b/tests/frontend/specs/redo.js @@ -1,68 +1,63 @@ -describe("undo button then redo button", function(){ - beforeEach(function(cb){ +describe('undo button then redo button', function () { + beforeEach(function (cb) { helper.newPad(cb); // creates a new pad this.timeout(60000); }); - it("redo some typing with button", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('redo some typing with button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value - var newString = "Foo"; + const $firstTextElement = inner$('div span').first(); + const originalValue = $firstTextElement.text(); // get the original value + const newString = 'Foo'; $firstTextElement.sendkeys(newString); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value + const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change // get undo and redo buttons - var $undoButton = chrome$(".buttonicon-undo"); - var $redoButton = chrome$(".buttonicon-redo"); + const $undoButton = chrome$('.buttonicon-undo'); + const $redoButton = chrome$('.buttonicon-redo'); // click the buttons $undoButton.click(); // removes foo $redoButton.click(); // resends foo - helper.waitFor(function(){ - return inner$("div span").first().text() === newString; - }).done(function(){ - var finalValue = inner$("div").first().text(); + helper.waitFor(() => inner$('div span').first().text() === newString).done(() => { + const finalValue = inner$('div').first().text(); expect(finalValue).to.be(modifiedValue); // expect the value to change done(); }); }); - it("redo some typing with keypress", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('redo some typing with keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value - var newString = "Foo"; + const $firstTextElement = inner$('div span').first(); + const originalValue = $firstTextElement.text(); // get the original value + const newString = 'Foo'; $firstTextElement.sendkeys(newString); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value + const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change var e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); var e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 121; // y - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - helper.waitFor(function(){ - return inner$("div span").first().text() === newString; - }).done(function(){ - var finalValue = inner$("div").first().text(); + helper.waitFor(() => inner$('div span').first().text() === newString).done(() => { + const finalValue = inner$('div').first().text(); expect(finalValue).to.be(modifiedValue); // expect the value to change done(); }); }); }); - diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index e6552642fe0..63803f641ab 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -13,9 +13,9 @@ // Adapted from John McLear's original test case. -describe('Responsiveness of Editor', function() { +xdescribe('Responsiveness of Editor', function () { // create a new pad before each test run - beforeEach(function(cb) { + beforeEach(function (cb) { helper.newPad(cb); this.timeout(6000); }); @@ -23,66 +23,62 @@ describe('Responsiveness of Editor', function() { // And the test needs to be fixed to work in Firefox 52 on Windows 7. I am not sure why it fails on this specific platform // The errors show this.timeout... then crash the browser but I am sure something is actually causing the stack trace and // I just need to narrow down what, offers to help accepted. - it('Fast response to keypress in pad with large amount of contents', function(done) { - - //skip on Windows Firefox 52.0 - if(window.bowser && window.bowser.windows && window.bowser.firefox && window.bowser.version == "52.0") { + it('Fast response to keypress in pad with large amount of contents', function (done) { + // skip on Windows Firefox 52.0 + if (window.bowser && window.bowser.windows && window.bowser.firefox && window.bowser.version == '52.0') { this.skip(); } - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var chars = '0000000000'; // row of placeholder chars - var amount = 200000; //number of blocks of chars we will insert - var length = (amount * (chars.length) +1); // include a counter for each space - var text = ''; // the text we're gonna insert - this.timeout(amount * 100); + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const chars = '0000000000'; // row of placeholder chars + const amount = 200000; // number of blocks of chars we will insert + const length = (amount * (chars.length) + 1); // include a counter for each space + let text = ''; // the text we're gonna insert + this.timeout(amount * 150); // Changed from 100 to 150 to allow Mac OSX Safari to be slow. // get keys to send - var keyMultiplier = 10; // multiplier * 10 == total number of key events - var keysToSend = ''; - for(var i=0; i <= keyMultiplier; i++) { + const keyMultiplier = 10; // multiplier * 10 == total number of key events + let keysToSend = ''; + for (var i = 0; i <= keyMultiplier; i++) { keysToSend += chars; } - var textElement = inner$('div'); + const textElement = inner$('div'); textElement.sendkeys('{selectall}'); // select all textElement.sendkeys('{del}'); // clear the pad text - for(var i=0; i <= amount; i++) { - text = text + chars + ' '; // add the chars and space to the text contents + for (var i = 0; i <= amount; i++) { + text = `${text + chars} `; // add the chars and space to the text contents } inner$('div').first().text(text); // Put the text contents into the pad - helper.waitFor(function(){ // Wait for the new contents to be on the pad - return inner$('div').text().length > length; - }).done(function(){ - - expect( inner$('div').text().length ).to.be.greaterThan( length ); // has the text changed? - var start = Date.now(); // get the start time + helper.waitFor(() => // Wait for the new contents to be on the pad + inner$('div').text().length > length + ).done(() => { + expect(inner$('div').text().length).to.be.greaterThan(length); // has the text changed? + const start = Date.now(); // get the start time // send some new text to the screen (ensure all 3 key events are sent) - var el = inner$('div').first(); - for(var i = 0; i < keysToSend.length; ++i) { + const el = inner$('div').first(); + for (let i = 0; i < keysToSend.length; ++i) { var x = keysToSend.charCodeAt(i); - ['keyup', 'keypress', 'keydown'].forEach(function(type) { - var e = $.Event(type); + ['keyup', 'keypress', 'keydown'].forEach((type) => { + const e = $.Event(type); e.keyCode = x; el.trigger(e); }); } - helper.waitFor(function(){ // Wait for the ability to process - return true; // Ghetto but works for now - }).done(function(){ - var end = Date.now(); // get the current time - var delay = end - start; // get the delay as the current time minus the start time + helper.waitFor(() => { // Wait for the ability to process + const el = inner$('body'); + if (el[0].textContent.length > amount) return true; + }).done(() => { + const end = Date.now(); // get the current time + const delay = end - start; // get the delay as the current time minus the start time - expect(delay).to.be.below(400); + expect(delay).to.be.below(600); done(); - }, 1000); - + }, 5000); }, 10000); }); - }); - diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js index 9bc6f10177a..52595a04476 100644 --- a/tests/frontend/specs/select_formatting_buttons.js +++ b/tests/frontend/specs/select_formatting_buttons.js @@ -1,105 +1,101 @@ -describe("select formatting buttons when selection has style applied", function(){ - var STYLES = ['italic', 'bold', 'underline', 'strikethrough']; - var SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough - var FIRST_LINE = 0; +describe('select formatting buttons when selection has style applied', function () { + const STYLES = ['italic', 'bold', 'underline', 'strikethrough']; + const SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough + const FIRST_LINE = 0; - before(function(cb){ + before(function (cb) { helper.newPad(cb); this.timeout(60000); }); - var applyStyleOnLine = function(style, line) { - var chrome$ = helper.padChrome$; + const applyStyleOnLine = function (style, line) { + const chrome$ = helper.padChrome$; selectLine(line); - var $formattingButton = chrome$('.buttonicon-' + style); + const $formattingButton = chrome$(`.buttonicon-${style}`); $formattingButton.click(); - } + }; - var isButtonSelected = function(style) { - var chrome$ = helper.padChrome$; - var $formattingButton = chrome$('.buttonicon-' + style); - return $formattingButton.parent().hasClass('selected'); - } + const isButtonSelected = function (style) { + const chrome$ = helper.padChrome$; + const $formattingButton = chrome$(`.buttonicon-${style}`); + return $formattingButton.parent().hasClass('selected'); + }; - var selectLine = function(lineNumber, offsetStart, offsetEnd) { - var inner$ = helper.padInner$; - var $line = inner$("div").eq(lineNumber); + var selectLine = function (lineNumber, offsetStart, offsetEnd) { + const inner$ = helper.padInner$; + const $line = inner$('div').eq(lineNumber); helper.selectLines($line, $line, offsetStart, offsetEnd); - } + }; - var placeCaretOnLine = function(lineNumber) { - var inner$ = helper.padInner$; - var $line = inner$("div").eq(lineNumber); + const placeCaretOnLine = function (lineNumber) { + const inner$ = helper.padInner$; + const $line = inner$('div').eq(lineNumber); $line.sendkeys('{leftarrow}'); - } + }; - var undo = function() { - var $undoButton = helper.padChrome$(".buttonicon-undo"); + const undo = function () { + const $undoButton = helper.padChrome$('.buttonicon-undo'); $undoButton.click(); - } + }; - var testIfFormattingButtonIsDeselected = function(style) { - it('deselects the ' + style + ' button', function(done) { - helper.waitFor(function(){ - return isButtonSelected(style) === false; - }).done(done) + const testIfFormattingButtonIsDeselected = function (style) { + it(`deselects the ${style} button`, function (done) { + helper.waitFor(() => isButtonSelected(style) === false).done(done); }); - } + }; - var testIfFormattingButtonIsSelected = function(style) { - it('selects the ' + style + ' button', function(done) { - helper.waitFor(function(){ - return isButtonSelected(style); - }).done(done) + const testIfFormattingButtonIsSelected = function (style) { + it(`selects the ${style} button`, function (done) { + helper.waitFor(() => isButtonSelected(style)).done(done); }); - } + }; - var applyStyleOnLineAndSelectIt = function(line, style, cb) { + const applyStyleOnLineAndSelectIt = function (line, style, cb) { applyStyleOnLineOnFullLineAndRemoveSelection(line, style, selectLine, cb); - } + }; - var applyStyleOnLineAndPlaceCaretOnit = function(line, style, cb) { + const applyStyleOnLineAndPlaceCaretOnit = function (line, style, cb) { applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb); - } + }; - var applyStyleOnLineOnFullLineAndRemoveSelection = function(line, style, selectTarget, cb) { + var applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) { // see if line html has changed - var inner$ = helper.padInner$; - var oldLineHTML = inner$.find("div")[line]; + const inner$ = helper.padInner$; + const oldLineHTML = inner$.find('div')[line]; applyStyleOnLine(style, line); - helper.waitFor(function(){ - var lineHTML = inner$.find("div")[line]; + helper.waitFor(() => { + const lineHTML = inner$.find('div')[line]; return lineHTML !== oldLineHTML; }); - // remove selection from previous line + // remove selection from previous line selectLine(line + 1); - //setTimeout(function() { - // select the text or place the caret on a position that - // has the formatting text applied previously - selectTarget(line); - cb(); - //}, 1000); - } - - var pressFormattingShortcutOnSelection = function(key) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); - - //select this text element + // setTimeout(function() { + // select the text or place the caret on a position that + // has the formatting text applied previously + selectTarget(line); + cb(); + // }, 1000); + }; + + const pressFormattingShortcutOnSelection = function (key) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); + + // select this text element $firstTextElement.sendkeys('{selectall}'); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = key.charCodeAt(0); // I, U, B, 5 - inner$("#innerdocbody").trigger(e); - } + inner$('#innerdocbody').trigger(e); + }; - STYLES.forEach(function(style){ - context('when selection is in a text with ' + style + ' applied', function(){ + STYLES.forEach((style) => { + context(`when selection is in a text with ${style} applied`, function () { before(function (done) { this.timeout(4000); applyStyleOnLineAndSelectIt(FIRST_LINE, style, done); @@ -112,7 +108,7 @@ describe("select formatting buttons when selection has style applied", function( testIfFormattingButtonIsSelected(style); }); - context('when caret is in a position with ' + style + ' applied', function(){ + context(`when caret is in a position with ${style} applied`, function () { before(function (done) { this.timeout(4000); applyStyleOnLineAndPlaceCaretOnit(FIRST_LINE, style, done); @@ -122,12 +118,12 @@ describe("select formatting buttons when selection has style applied", function( undo(); }); - testIfFormattingButtonIsSelected(style) + testIfFormattingButtonIsSelected(style); }); }); - context('when user applies a style and the selection does not change', function() { - var style = STYLES[0]; // italic + context('when user applies a style and the selection does not change', function () { + const style = STYLES[0]; // italic before(function () { applyStyleOnLine(style, FIRST_LINE); }); @@ -143,16 +139,16 @@ describe("select formatting buttons when selection has style applied", function( }); }); - SHORTCUT_KEYS.forEach(function(key, index){ - var styleOfTheShortcut = STYLES[index]; // italic, bold, ... - context('when user presses CMD + ' + key, function() { + SHORTCUT_KEYS.forEach((key, index) => { + const styleOfTheShortcut = STYLES[index]; // italic, bold, ... + context(`when user presses CMD + ${key}`, function () { before(function () { pressFormattingShortcutOnSelection(key); }); testIfFormattingButtonIsSelected(styleOfTheShortcut); - context('and user presses CMD + ' + key + ' again', function() { + context(`and user presses CMD + ${key} again`, function () { before(function () { pressFormattingShortcutOnSelection(key); }); diff --git a/tests/frontend/specs/strikethrough.js b/tests/frontend/specs/strikethrough.js index dc37b36f451..d8feae3be03 100644 --- a/tests/frontend/specs/strikethrough.js +++ b/tests/frontend/specs/strikethrough.js @@ -1,34 +1,34 @@ -describe("strikethrough button", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('strikethrough button', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("makes text strikethrough", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('makes text strikethrough', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - //get the strikethrough button and click it - var $strikethroughButton = chrome$(".buttonicon-strikethrough"); + // get the strikethrough button and click it + const $strikethroughButton = chrome$('.buttonicon-strikethrough'); $strikethroughButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again - var $newFirstTextElement = inner$("div").first(); + // ace creates a new dom element when you press a button, so just get the first text element again + const $newFirstTextElement = inner$('div').first(); // is there a element now? - var isstrikethrough = $newFirstTextElement.find("s").length === 1; + const isstrikethrough = $newFirstTextElement.find('s').length === 1; - //expect it to be strikethrough + // expect it to be strikethrough expect(isstrikethrough).to.be(true); - //make sure the text hasn't changed + // make sure the text hasn't changed expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); done(); diff --git a/tests/frontend/specs/timeslider.js b/tests/frontend/specs/timeslider.js index bca80ba495c..bea7932dff4 100644 --- a/tests/frontend/specs/timeslider.js +++ b/tests/frontend/specs/timeslider.js @@ -1,47 +1,42 @@ -//deactivated, we need a nice way to get the timeslider, this is ugly -xdescribe("timeslider button takes you to the timeslider of a pad", function(){ - beforeEach(function(cb){ +// deactivated, we need a nice way to get the timeslider, this is ugly +xdescribe('timeslider button takes you to the timeslider of a pad', function () { + beforeEach(function (cb) { helper.newPad(cb); // creates a new pad this.timeout(60000); }); - it("timeslider contained in URL", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('timeslider contained in URL', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value - var newValue = "Testing"+originalValue; - $firstTextElement.sendkeys("Testing"); // send line 1 to the pad + const $firstTextElement = inner$('div span').first(); + const originalValue = $firstTextElement.text(); // get the original value + const newValue = `Testing${originalValue}`; + $firstTextElement.sendkeys('Testing'); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value + const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - helper.waitFor(function(){ - return modifiedValue !== originalValue; // The value has changed so we can.. - }).done(function(){ - - var $timesliderButton = chrome$("#timesliderlink"); + helper.waitFor(() => modifiedValue !== originalValue // The value has changed so we can.. + ).done(() => { + const $timesliderButton = chrome$('#timesliderlink'); $timesliderButton.click(); // So click the timeslider link - helper.waitFor(function(){ - var iFrameURL = chrome$.window.location.href; - if(iFrameURL){ - return iFrameURL.indexOf("timeslider") !== -1; - }else{ + helper.waitFor(() => { + const iFrameURL = chrome$.window.location.href; + if (iFrameURL) { + return iFrameURL.indexOf('timeslider') !== -1; + } else { return false; // the URL hasnt been set yet } - }).done(function(){ + }).done(() => { // click the buttons - var iFrameURL = chrome$.window.location.href; // get the url - var inTimeslider = iFrameURL.indexOf("timeslider") !== -1; + const iFrameURL = chrome$.window.location.href; // get the url + const inTimeslider = iFrameURL.indexOf('timeslider') !== -1; expect(inTimeslider).to.be(true); // expect the value to change done(); }); - - }); }); }); - diff --git a/tests/frontend/specs/timeslider_follow.js b/tests/frontend/specs/timeslider_follow.js index 258abc3eccb..9f86ddc7700 100644 --- a/tests/frontend/specs/timeslider_follow.js +++ b/tests/frontend/specs/timeslider_follow.js @@ -1,55 +1,100 @@ -describe("timeslider", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +'use strict'; + +describe('timeslider follow', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); - this.timeout(6000); }); - it("follow content as it's added to timeslider", function(done) { // passes - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // make some changes to produce 100 revisions - var timePerRev = 900 - , revs = 10; - this.timeout(revs*timePerRev+10000); - for(var i=0; i < revs; i++) { - setTimeout(function() { - // enter 'a' in the first text element - inner$("div").last().sendkeys('a\n'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - }, timePerRev*i); + // TODO needs test if content is also followed, when user a makes edits + // while user b is in the timeslider + it("content as it's added to timeslider", async function () { + // send 6 revisions + const revs = 6; + const message = 'a\n\n\n\n\n\n\n\n\n\n'; + const newLines = message.split('\n').length; + for (let i = 0; i < revs; i++) { + await helper.edit(message, newLines * i + 1); } - setTimeout(function() { - // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); + await helper.gotoTimeslider(0); + await helper.waitForPromise(() => helper.contentWindow().location.hash === '#0'); - setTimeout(function() { - var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - var $sliderBar = timeslider$('#ui-slider-bar'); + const originalTop = helper.contentWindow().$('#innerdocbody').offset(); - var latestContents = timeslider$('#innerdocbody').text(); + // set to follow contents as it arrives + helper.contentWindow().$('#options-followContents').prop('checked', true); + helper.contentWindow().$('#playpause_button_icon').click(); - // set to follow contents as it arrives - timeslider$('#options-followContents').prop("checked", true); + let newTop; + await helper.waitForPromise(() => { + newTop = helper.contentWindow().$('#innerdocbody').offset(); + return newTop.top < originalTop.top; + }); + }); - var originalTop = timeslider$('#innerdocbody').offset(); - timeslider$('#playpause_button_icon').click(); + /** + * Tests for bug described in #4389 + * The goal is to scroll to the first line that contains a change right before + * the change is applied. + */ + it('only to lines that exist in the pad view, regression test for #4389', async function () { + await helper.clearPad(); + await helper.edit('Test line\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' + + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'); + await helper.edit('Another test line', 40); - setTimeout(function() { - //make sure the text has changed - var newTop = timeslider$('#innerdocbody').offset(); - expect( originalTop ).not.to.eql( newTop ); - done(); - }, 1000); - }, 2000); - }, revs*timePerRev); - }); + await helper.gotoTimeslider(); + + // set to follow contents as it arrives + helper.contentWindow().$('#options-followContents').prop('checked', true); + + const oldYPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop; + expect(oldYPosition).to.be(0); + + /** + * pad content rev 0 [default Pad text] + * pad content rev 1 [''] + * pad content rev 2 ['Test line','','', ..., ''] + * pad content rev 3 ['Test line','',..., 'Another test line', ..., ''] + */ + // line 40 changed + helper.contentWindow().$('#leftstep').click(); + await helper.waitForPromise(() => hasFollowedToLine(40)); + + // line 1 is the first line that changed + helper.contentWindow().$('#leftstep').click(); + await helper.waitForPromise(() => hasFollowedToLine(1)); + + // line 1 changed + helper.contentWindow().$('#leftstep').click(); + await helper.waitForPromise(() => hasFollowedToLine(1)); + + // line 1 changed + helper.contentWindow().$('#rightstep').click(); + await helper.waitForPromise(() => hasFollowedToLine(1)); + + // line 1 is the first line that changed + helper.contentWindow().$('#rightstep').click(); + await helper.waitForPromise(() => hasFollowedToLine(1)); + + // line 40 changed + helper.contentWindow().$('#rightstep').click(); + helper.waitForPromise(() => hasFollowedToLine(40)); + }); }); +/** + * @param {number} lineNum + * @returns {boolean} scrolled to the lineOffset? + */ +const hasFollowedToLine = (lineNum) => { + const scrollPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop; + const lineOffset = + helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop; + return Math.abs(scrollPosition - lineOffset) < 1; +}; diff --git a/tests/frontend/specs/timeslider_labels.js b/tests/frontend/specs/timeslider_labels.js index 09213a45227..c7a4aca5aff 100644 --- a/tests/frontend/specs/timeslider_labels.js +++ b/tests/frontend/specs/timeslider_labels.js @@ -1,64 +1,62 @@ -describe("timeslider", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('timeslider', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); - this.timeout(60000); }); - it("Shows a date and time in the timeslider and make sure it doesn't include NaN", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // make some changes to produce 100 revisions - var revs = 10; - this.timeout(60000); - for(var i=0; i < revs; i++) { - setTimeout(function() { - // enter 'a' in the first text element - inner$("div").first().sendkeys('a'); - }, 200); + /** + * @todo test authorsList + */ + it("Shows a date and time in the timeslider and make sure it doesn't include NaN", async function () { + // make some changes to produce 3 revisions + const revs = 3; + + for (let i = 0; i < revs; i++) { + await helper.edit('a\n'); } - setTimeout(function() { - // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider'); - - setTimeout(function() { - var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - var $sliderBar = timeslider$('#ui-slider-bar'); - - var latestContents = timeslider$('#padcontent').text(); - - // Expect the date and time to be shown - - // Click somewhere on the timeslider - var e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 45; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 40; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 50; - $sliderBar.trigger(e); - - $sliderBar.trigger('mouseup') - - setTimeout(function() { - //make sure the text has changed - expect( timeslider$('#timer').text() ).not.to.eql( "" ); - expect( timeslider$('#revision_date').text() ).not.to.eql( "" ); - expect( timeslider$('#revision_label').text() ).not.to.eql( "" ); - var includesNaN = timeslider$('#revision_label').text().indexOf("NaN"); // NaN is bad. Naan ist gut - expect( includesNaN ).to.eql( -1 ); // not quite so tasty, I like curry. - done(); - }, 400); - }, 2000); - }, 2000); + await helper.gotoTimeslider(revs); + await helper.waitForPromise(() => helper.contentWindow().location.hash === `#${revs}`); + + // the datetime of last edit + const timerTimeLast = new Date(helper.timesliderTimerTime()).getTime(); + + // the day of this revision, e.g. August 12, 2020 (stripped the string "Saved") + const dateLast = new Date(helper.revisionDateElem().substr(6)).getTime(); + + // the label/revision, ie Version 3 + const labelLast = helper.revisionLabelElem().text(); + + // the datetime should be a date + expect(Number.isNaN(timerTimeLast)).to.eql(false); + + // the Date object of the day should not be NaN + expect(Number.isNaN(dateLast)).to.eql(false); + + // the label should be Version `Number` + expect(labelLast).to.be(`Version ${revs}`); + + // Click somewhere left on the timeslider to go to revision 0 + helper.sliderClick(1); + + // the datetime of last edit + const timerTime = new Date(helper.timesliderTimerTime()).getTime(); + + // the day of this revision, e.g. August 12, 2020 + const date = new Date(helper.revisionDateElem().substr(6)).getTime(); + + // the label/revision, e.g. Version 0 + const label = helper.revisionLabelElem().text(); + + // the datetime should be a date + expect(Number.isNaN(timerTime)).to.eql(false); + // the last revision should be newer or have the same time + expect(timerTimeLast).to.not.be.lessThan(timerTime); + + // the Date object of the day should not be NaN + expect(Number.isNaN(date)).to.eql(false); + + // the label should be Version 0 + expect(label).to.be('Version 0'); }); }); diff --git a/tests/frontend/specs/timeslider_numeric_padID.js b/tests/frontend/specs/timeslider_numeric_padID.js index e6071f09152..53eb4a29c8b 100644 --- a/tests/frontend/specs/timeslider_numeric_padID.js +++ b/tests/frontend/specs/timeslider_numeric_padID.js @@ -1,67 +1,29 @@ -describe("timeslider", function(){ - var padId = 735773577357+(Math.round(Math.random()*1000)); +describe('timeslider', function () { + const padId = 735773577357 + (Math.round(Math.random() * 1000)); - //create a new pad before each test run - beforeEach(function(cb){ + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb, padId); - this.timeout(60000); }); - it("Makes sure the export URIs are as expected when the padID is numeric", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('Makes sure the export URIs are as expected when the padID is numeric', async function () { + await helper.edit('a\n'); - // make some changes to produce 100 revisions - var revs = 10; - this.timeout(60000); - for(var i=0; i < revs; i++) { - setTimeout(function() { - // enter 'a' in the first text element - inner$("div").first().sendkeys('a'); - }, 100); - } + await helper.gotoTimeslider(1); - setTimeout(function() { - // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider'); + // ensure we are on revision 1 + await helper.waitForPromise(() => helper.contentWindow().location.hash === '#1'); - setTimeout(function() { - var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - var $sliderBar = timeslider$('#ui-slider-bar'); + // expect URI to be similar to + // http://192.168.1.48:9001/p/2/1/export/html + // http://192.168.1.48:9001/p/735773577399/1/export/html + const rev1ExportLink = helper.contentWindow().$('#exporthtmla').attr('href'); + expect(rev1ExportLink).to.contain('/1/export/html'); - var latestContents = timeslider$('#padcontent').text(); + // Click somewhere left on the timeslider to go to revision 0 + helper.sliderClick(30); - // Expect the date and time to be shown - - // Click somewhere on the timeslider - var e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 45; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 40; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 50; - $sliderBar.trigger(e); - - $sliderBar.trigger('mouseup') - - setTimeout(function() { - // expect URI to be similar to - // http://192.168.1.48:9001/p/2/2/export/html - // http://192.168.1.48:9001/p/735773577399/0/export/html - var exportLink = timeslider$('#exporthtmla').attr('href'); - var checkVal = padId + "/0/export/html"; - var includesCorrectURI = exportLink.indexOf(checkVal); - expect(includesCorrectURI).to.not.be(-1); - done(); - }, 400); - }, 2000); - }, 2000); + const rev0ExportLink = helper.contentWindow().$('#exporthtmla').attr('href'); + expect(rev0ExportLink).to.contain('/0/export/html'); }); }); diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js index 67123344bba..fbfbb36152a 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/tests/frontend/specs/timeslider_revisions.js @@ -1,42 +1,42 @@ -describe("timeslider", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('timeslider', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); - this.timeout(6000); + this.timeout(60000); }); - it("loads adds a hundred revisions", function(done) { // passes - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('loads adds a hundred revisions', function (done) { // passes + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // make some changes to produce 100 revisions - var timePerRev = 900 - , revs = 99; - this.timeout(revs*timePerRev+10000); - for(var i=0; i < revs; i++) { - setTimeout(function() { + const timePerRev = 900; + const revs = 99; + this.timeout(revs * timePerRev + 10000); + for (let i = 0; i < revs; i++) { + setTimeout(() => { // enter 'a' in the first text element - inner$("div").first().sendkeys('a'); - }, timePerRev*i); + inner$('div').first().sendkeys('a'); + }, timePerRev * i); } chrome$('.buttonicon-savedRevision').click(); - setTimeout(function() { + setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider'); + $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); - setTimeout(function() { - var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - var $sliderBar = timeslider$('#ui-slider-bar'); + setTimeout(() => { + const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; + const $sliderBar = timeslider$('#ui-slider-bar'); - var latestContents = timeslider$('#innerdocbody').text(); + const latestContents = timeslider$('#innerdocbody').text(); // Click somewhere on the timeslider - var e = new jQuery.Event('mousedown'); + let e = new jQuery.Event('mousedown'); // sets y co-ordinate of the pad slider modal. - var base = (timeslider$('#ui-slider-bar').offset().top - 24) + const base = (timeslider$('#ui-slider-bar').offset().top - 24); e.clientX = e.pageX = 150; - e.clientY = e.pageY = base+5; + e.clientY = e.pageY = base + 5; $sliderBar.trigger(e); e = new jQuery.Event('mousedown'); @@ -46,134 +46,127 @@ describe("timeslider", function(){ e = new jQuery.Event('mousedown'); e.clientX = e.pageX = 150; - e.clientY = e.pageY = base-5; + e.clientY = e.pageY = base - 5; $sliderBar.trigger(e); - $sliderBar.trigger('mouseup') + $sliderBar.trigger('mouseup'); - setTimeout(function() { - //make sure the text has changed - expect( timeslider$('#innerdocbody').text() ).not.to.eql( latestContents ); - var starIsVisible = timeslider$('.star').is(":visible"); - expect( starIsVisible ).to.eql( true ); + setTimeout(() => { + // make sure the text has changed + expect(timeslider$('#innerdocbody').text()).not.to.eql(latestContents); + const starIsVisible = timeslider$('.star').is(':visible'); + expect(starIsVisible).to.eql(true); done(); }, 1000); - }, 6000); - }, revs*timePerRev); + }, revs * timePerRev); }); // Disabled as jquery trigger no longer works properly - xit("changes the url when clicking on the timeslider", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + xit('changes the url when clicking on the timeslider', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // make some changes to produce 7 revisions - var timePerRev = 1000 - , revs = 20; - this.timeout(revs*timePerRev+10000); - for(var i=0; i < revs; i++) { - setTimeout(function() { + const timePerRev = 1000; + const revs = 20; + this.timeout(revs * timePerRev + 10000); + for (let i = 0; i < revs; i++) { + setTimeout(() => { // enter 'a' in the first text element - inner$("div").first().sendkeys('a'); - }, timePerRev*i); + inner$('div').first().sendkeys('a'); + }, timePerRev * i); } - setTimeout(function() { + setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider'); + $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); - setTimeout(function() { - var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - var $sliderBar = timeslider$('#ui-slider-bar'); + setTimeout(() => { + const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; + const $sliderBar = timeslider$('#ui-slider-bar'); - var latestContents = timeslider$('#innerdocbody').text(); - var oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; + const latestContents = timeslider$('#innerdocbody').text(); + const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; // Click somewhere on the timeslider - var e = new jQuery.Event('mousedown'); + const e = new jQuery.Event('mousedown'); e.clientX = e.pageX = 150; e.clientY = e.pageY = 60; $sliderBar.trigger(e); - helper.waitFor(function(){ - return $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl; - }, 6000).always(function(){ - expect( $('#iframe-container iframe')[0].contentWindow.location.hash ).not.to.eql( oldUrl ); + helper.waitFor(() => $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl, 6000).always(() => { + expect($('#iframe-container iframe')[0].contentWindow.location.hash).not.to.eql(oldUrl); done(); }); }, 6000); - }, revs*timePerRev); + }, revs * timePerRev); }); - it("jumps to a revision given in the url", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - this.timeout(20000); + it('jumps to a revision given in the url', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + this.timeout(40000); // wait for the text to be loaded - helper.waitFor(function(){ - return inner$('body').text().length != 0; - }, 6000).always(function() { - var newLines = inner$('body div').length; - var oldLength = inner$('body').text().length + newLines / 2; - expect( oldLength ).to.not.eql( 0 ); - inner$("div").first().sendkeys('a'); - var timeslider$; + helper.waitFor(() => inner$('body').text().length != 0, 10000).always(() => { + const newLines = inner$('body div').length; + const oldLength = inner$('body').text().length + newLines / 2; + expect(oldLength).to.not.eql(0); + inner$('div').first().sendkeys('a'); + let timeslider$; // wait for our additional revision to be added - helper.waitFor(function(){ + helper.waitFor(() => { // newLines takes the new lines into account which are strippen when using // inner$('body').text(), one
                      is used for one line in ACE. - var lenOkay = inner$('body').text().length + newLines / 2 != oldLength; + const lenOkay = inner$('body').text().length + newLines / 2 != oldLength; // this waits for the color to be added to our , which means that the revision // was accepted by the server. - var colorOkay = inner$('span').first().attr('class').indexOf("author-") == 0; + const colorOkay = inner$('span').first().attr('class').indexOf('author-') == 0; return lenOkay && colorOkay; - }, 6000).always(function() { + }, 10000).always(() => { // go to timeslider with a specific revision set - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); + $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); // wait for the timeslider to be loaded - helper.waitFor(function(){ + helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch(e){} - if(timeslider$){ + } catch (e) {} + if (timeslider$) { return timeslider$('#innerdocbody').text().length == oldLength; } - }, 6000).always(function(){ - expect( timeslider$('#innerdocbody').text().length ).to.eql( oldLength ); + }, 10000).always(() => { + expect(timeslider$('#innerdocbody').text().length).to.eql(oldLength); done(); }); }); }); }); - it("checks the export url", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('checks the export url', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; this.timeout(11000); - inner$("div").first().sendkeys('a'); + inner$('div').first().sendkeys('a'); - setTimeout(function() { + setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider#0'); - var timeslider$; - var exportLink; + $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + let timeslider$; + let exportLink; - helper.waitFor(function(){ - try{ + helper.waitFor(() => { + try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - }catch(e){} - if(!timeslider$) - return false; + } catch (e) {} + if (!timeslider$) return false; exportLink = timeslider$('#exportplaina').attr('href'); - if(!exportLink) - return false; - return exportLink.substr(exportLink.length - 12) == "0/export/txt"; - }, 6000).always(function(){ - expect( exportLink.substr(exportLink.length - 12) ).to.eql( "0/export/txt" ); + if (!exportLink) return false; + return exportLink.substr(exportLink.length - 12) == '0/export/txt'; + }, 6000).always(() => { + expect(exportLink.substr(exportLink.length - 12)).to.eql('0/export/txt'); done(); }); }, 2500); diff --git a/tests/frontend/specs/undo.js b/tests/frontend/specs/undo.js index 172a2b81e7b..0c94f22306e 100644 --- a/tests/frontend/specs/undo.js +++ b/tests/frontend/specs/undo.js @@ -1,61 +1,54 @@ -describe("undo button", function(){ - beforeEach(function(cb){ +describe('undo button', function () { + beforeEach(function (cb) { helper.newPad(cb); // creates a new pad this.timeout(60000); }); - it("undo some typing by clicking undo button", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('undo some typing by clicking undo button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value + const $firstTextElement = inner$('div span').first(); + const originalValue = $firstTextElement.text(); // get the original value - $firstTextElement.sendkeys("foo"); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value + $firstTextElement.sendkeys('foo'); // send line 1 to the pad + const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change // get clear authorship button as a variable - var $undoButton = chrome$(".buttonicon-undo"); + const $undoButton = chrome$('.buttonicon-undo'); // click the button $undoButton.click(); - helper.waitFor(function(){ - return inner$("div span").first().text() === originalValue; - }).done(function(){ - var finalValue = inner$("div span").first().text(); + helper.waitFor(() => inner$('div span').first().text() === originalValue).done(() => { + const finalValue = inner$('div span').first().text(); expect(finalValue).to.be(originalValue); // expect the value to change done(); }); }); - it("undo some typing using a keypress", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('undo some typing using a keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; // get the first text element inside the editable space - var $firstTextElement = inner$("div span").first(); - var originalValue = $firstTextElement.text(); // get the original value + const $firstTextElement = inner$('div span').first(); + const originalValue = $firstTextElement.text(); // get the original value - $firstTextElement.sendkeys("foo"); // send line 1 to the pad - var modifiedValue = $firstTextElement.text(); // get the modified value + $firstTextElement.sendkeys('foo'); // send line 1 to the pad + const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - helper.waitFor(function(){ - return inner$("div span").first().text() === originalValue; - }).done(function(){ - var finalValue = inner$("div span").first().text(); + helper.waitFor(() => inner$('div span').first().text() === originalValue).done(() => { + const finalValue = inner$('div span').first().text(); expect(finalValue).to.be(originalValue); // expect the value to change done(); }); }); - - }); - diff --git a/tests/frontend/specs/unordered_list.js b/tests/frontend/specs/unordered_list.js index 2af843d6131..4cbdabfacbc 100644 --- a/tests/frontend/specs/unordered_list.js +++ b/tests/frontend/specs/unordered_list.js @@ -1,177 +1,162 @@ -describe("assign unordered list", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('assign unordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("insert unordered list text then removes by outdent", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var originalText = inner$("div").first().text(); + it('insert unordered list text then removes by outdent', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const originalText = inner$('div').first().text(); - var $insertunorderedlistButton = chrome$(".buttonicon-insertunorderedlist"); + const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); $insertunorderedlistButton.click(); - helper.waitFor(function(){ - var newText = inner$("div").first().text(); - if(newText === originalText){ - return inner$("div").first().find("ul li").length === 1; + helper.waitFor(() => { + const newText = inner$('div').first().text(); + if (newText === originalText) { + return inner$('div').first().find('ul li').length === 1; } - }).done(function(){ - + }).done(() => { // remove indentation by bullet and ensure text string remains the same - chrome$(".buttonicon-outdent").click(); - helper.waitFor(function(){ - var newText = inner$("div").first().text(); + chrome$('.buttonicon-outdent').click(); + helper.waitFor(() => { + const newText = inner$('div').first().text(); return (newText === originalText); - }).done(function(){ + }).done(() => { done(); }); - }); }); - }); -describe("unassign unordered list", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('unassign unordered list', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("insert unordered list text then remove by clicking list again", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - var originalText = inner$("div").first().text(); + it('insert unordered list text then remove by clicking list again', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; + const originalText = inner$('div').first().text(); - var $insertunorderedlistButton = chrome$(".buttonicon-insertunorderedlist"); + const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); $insertunorderedlistButton.click(); - helper.waitFor(function(){ - var newText = inner$("div").first().text(); - if(newText === originalText){ - return inner$("div").first().find("ul li").length === 1; + helper.waitFor(() => { + const newText = inner$('div').first().text(); + if (newText === originalText) { + return inner$('div').first().find('ul li').length === 1; } - }).done(function(){ - + }).done(() => { // remove indentation by bullet and ensure text string remains the same $insertunorderedlistButton.click(); - helper.waitFor(function(){ - var isList = inner$("div").find("ul").length === 1; + helper.waitFor(() => { + const isList = inner$('div').find('ul').length === 1; // sohuldn't be list return (isList === false); - }).done(function(){ + }).done(() => { done(); }); - }); }); }); -describe("keep unordered list on enter key", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('keep unordered list on enter key', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("Keeps the unordered list on enter for the new line", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('Keeps the unordered list on enter for the new line', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - var $insertorderedlistButton = chrome$(".buttonicon-insertunorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); $insertorderedlistButton.click(); - //type a bit, make a line break and type again - var $firstTextElement = inner$("div span").first(); + // type a bit, make a line break and type again + const $firstTextElement = inner$('div span').first(); $firstTextElement.sendkeys('line 1'); $firstTextElement.sendkeys('{enter}'); $firstTextElement.sendkeys('line 2'); $firstTextElement.sendkeys('{enter}'); - helper.waitFor(function(){ - return inner$("div span").first().text().indexOf("line 2") === -1; - }).done(function(){ - var $newSecondLine = inner$("div").first().next(); - var hasULElement = $newSecondLine.find("ul li").length === 1; + helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => { + const $newSecondLine = inner$('div').first().next(); + const hasULElement = $newSecondLine.find('ul li').length === 1; expect(hasULElement).to.be(true); - expect($newSecondLine.text()).to.be("line 2"); + expect($newSecondLine.text()).to.be('line 2'); done(); }); }); - }); -describe("Pressing Tab in an UL increases and decreases indentation", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('Pressing Tab in an UL increases and decreases indentation', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("indent and de-indent list item with keypress", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent and de-indent list item with keypress', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var $insertorderedlistButton = chrome$(".buttonicon-insertunorderedlist"); + const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); $insertorderedlistButton.click(); - var e = inner$.Event(helper.evtType); + const e = inner$.Event(helper.evtType); e.keyCode = 9; // tab - inner$("#innerdocbody").trigger(e); + inner$('#innerdocbody').trigger(e); - expect(inner$("div").first().find(".list-bullet2").length === 1).to.be(true); + expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true); e.shiftKey = true; // shift e.keyCode = 9; // tab - inner$("#innerdocbody").trigger(e); - - helper.waitFor(function(){ - return inner$("div").first().find(".list-bullet1").length === 1; - }).done(done); + inner$('#innerdocbody').trigger(e); + helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); }); - }); -describe("Pressing indent/outdent button in an UL increases and decreases indentation and bullet / ol formatting", function(){ - //create a new pad before each test run - beforeEach(function(cb){ +describe('Pressing indent/outdent button in an UL increases and decreases indentation and bullet / ol formatting', function () { + // create a new pad before each test run + beforeEach(function (cb) { helper.newPad(cb); this.timeout(60000); }); - it("indent and de-indent list item with indent button", function(done){ - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it('indent and de-indent list item with indent button', function (done) { + const inner$ = helper.padInner$; + const chrome$ = helper.padChrome$; - //get the first text element out of the inner iframe - var $firstTextElement = inner$("div").first(); + // get the first text element out of the inner iframe + const $firstTextElement = inner$('div').first(); - //select this text element + // select this text element $firstTextElement.sendkeys('{selectall}'); - var $insertunorderedlistButton = chrome$(".buttonicon-insertunorderedlist"); + const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist'); $insertunorderedlistButton.click(); - var $indentButton = chrome$(".buttonicon-indent"); + const $indentButton = chrome$('.buttonicon-indent'); $indentButton.click(); // make it indented twice - expect(inner$("div").first().find(".list-bullet2").length === 1).to.be(true); - var $outdentButton = chrome$(".buttonicon-outdent"); + expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true); + const $outdentButton = chrome$('.buttonicon-outdent'); $outdentButton.click(); // make it deindented to 1 - helper.waitFor(function(){ - return inner$("div").first().find(".list-bullet1").length === 1; - }).done(done); + helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done); }); }); - diff --git a/tests/frontend/specs/urls_become_clickable.js b/tests/frontend/specs/urls_become_clickable.js index 7f5e306795e..a027de9ff91 100644 --- a/tests/frontend/specs/urls_become_clickable.js +++ b/tests/frontend/specs/urls_become_clickable.js @@ -1,70 +1,76 @@ -describe("urls", function(){ - //create a new pad before each test run - beforeEach(function(cb){ - helper.newPad(cb); - this.timeout(60000); - }); - - it("when you enter an url, it becomes clickable", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; +'use strict'; - //get the first text element out of the inner iframe - var firstTextElement = inner$("div").first(); +describe('urls', function () { + // Returns the first text element. Note that any change to the text element will result in the + // element being replaced with another object. + const txt = () => helper.padInner$('div').first(); - // simulate key presses to delete content - firstTextElement.sendkeys('{selectall}'); // select all - firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys('https://etherpad.org'); // insert a URL - - helper.waitFor(function(){ - return inner$("div").first().find("a").length === 1; - }, 2000).done(done); + before(async function () { + this.timeout(60000); + await new Promise((resolve, reject) => helper.newPad((err) => { + if (err != null) return reject(err); + resolve(); + })); }); - it("when you enter a url containing a !, it becomes clickable and contains the whole URL", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //get the first text element out of the inner iframe - var firstTextElement = inner$("div").first(); - var url = "https://etherpad.org/!foo"; - - // simulate key presses to delete content - firstTextElement.sendkeys('{selectall}'); // select all - firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys(url); // insert a URL - - helper.waitFor(function(){ - if(inner$("div").first().find("a").length === 1){ // if it contains an A link - if(inner$("div").first().find("a")[0].href === url){ - return true; - } - }; - }, 2000).done(done); + beforeEach(async function () { + await helper.clearPad(); }); - it("when you enter a url followed by a ], the ] is not included in the URL", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - //get the first text element out of the inner iframe - var firstTextElement = inner$("div").first(); - var url = "https://etherpad.org/"; + describe('entering a URL makes a link', function () { + for (const url of ['https://etherpad.org', 'www.etherpad.org']) { + it(url, async function () { + const url = 'https://etherpad.org'; + await helper.edit(url); + await helper.waitForPromise(() => txt().find('a').length === 1, 2000); + const link = txt().find('a'); + expect(link.attr('href')).to.be(url); + expect(link.text()).to.be(url); + }); + } + }); - // simulate key presses to delete content - firstTextElement.sendkeys('{selectall}'); // select all - firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys(url); // insert a URL - firstTextElement.sendkeys(']'); // put a ] after it + describe('special characters inside URL', function () { + for (const char of '-:@_.,~%+/?=&#!;()$\'*') { + const url = `https://etherpad.org/${char}foo`; + it(url, async function () { + await helper.edit(url); + await helper.waitForPromise(() => txt().find('a').length === 1); + const link = txt().find('a'); + expect(link.attr('href')).to.be(url); + expect(link.text()).to.be(url); + }); + } + }); - helper.waitFor(function(){ - if(inner$("div").first().find("a").length === 1){ // if it contains an A link - if(inner$("div").first().find("a")[0].href === url){ - return true; - } - }; - }, 2000).done(done); + describe('punctuation after URL is ignored', function () { + for (const char of ':.,;?!)\'*]') { + const want = 'https://etherpad.org'; + const input = want + char; + it(input, async function () { + await helper.edit(input); + await helper.waitForPromise(() => txt().find('a').length === 1); + const link = txt().find('a'); + expect(link.attr('href')).to.be(want); + expect(link.text()).to.be(want); + }); + } }); + // Square brackets are in the RFC3986 reserved set so they can legally appear in URIs, but they + // are explicitly excluded from linkification because including them is usually not desired (e.g., + // it can interfere with wiki/markdown link syntax). + describe('square brackets are excluded from linkified URLs', function () { + for (const char of '[]') { + const want = 'https://etherpad.org/'; + const input = `${want}${char}foo`; + it(input, async function () { + await helper.edit(input); + await helper.waitForPromise(() => txt().find('a').length === 1); + const link = txt().find('a'); + expect(link.attr('href')).to.be(want); + expect(link.text()).to.be(want); + }); + } + }); }); diff --git a/tests/frontend/specs/xxauto_reconnect.js b/tests/frontend/specs/xxauto_reconnect.js index e2d2df36a4d..574616ce5bd 100644 --- a/tests/frontend/specs/xxauto_reconnect.js +++ b/tests/frontend/specs/xxauto_reconnect.js @@ -1,48 +1,46 @@ -describe('Automatic pad reload on Force Reconnect message', function() { - var padId, $originalPadFrame; +describe('Automatic pad reload on Force Reconnect message', function () { + let padId, $originalPadFrame; - beforeEach(function(done) { - padId = helper.newPad(function() { + beforeEach(function (done) { + padId = helper.newPad(() => { // enable userdup error to have timer to force reconnect - var $errorMessageModal = helper.padChrome$('#connectivity .userdup'); + const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); $errorMessageModal.addClass('with_reconnect_timer'); // make sure there's a timeout set, otherwise automatic reconnect won't be enabled helper.padChrome$.window.clientVars.automaticReconnectionTimeout = 2; // open same pad on another iframe, to force userdup error - var $otherIframeWithSamePad = $(''); + const $otherIframeWithSamePad = $(``); $originalPadFrame = $('#iframe-container iframe'); $otherIframeWithSamePad.insertAfter($originalPadFrame); // wait for modal to be displayed - helper.waitFor(function() { - return $errorMessageModal.is(':visible'); - }, 50000).done(done); + helper.waitFor(() => $errorMessageModal.is(':visible'), 50000).done(done); }); this.timeout(60000); }); - it('displays a count down timer to automatically reconnect', function(done) { - var $errorMessageModal = helper.padChrome$('#connectivity .userdup'); - var $countDownTimer = $errorMessageModal.find('.reconnecttimer'); + it('displays a count down timer to automatically reconnect', function (done) { + const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); + const $countDownTimer = $errorMessageModal.find('.reconnecttimer'); expect($countDownTimer.is(':visible')).to.be(true); done(); }); - context('and user clicks on Cancel', function() { - beforeEach(function() { - var $errorMessageModal = helper.padChrome$('#connectivity .userdup'); + context('and user clicks on Cancel', function () { + beforeEach(function () { + const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); $errorMessageModal.find('#cancelreconnect').click(); }); - it('does not show Cancel button nor timer anymore', function(done) { - var $errorMessageModal = helper.padChrome$('#connectivity .userdup'); - var $countDownTimer = $errorMessageModal.find('.reconnecttimer'); - var $cancelButton = $errorMessageModal.find('#cancelreconnect'); + it('does not show Cancel button nor timer anymore', function (done) { + const $errorMessageModal = helper.padChrome$('#connectivity .userdup'); + const $countDownTimer = $errorMessageModal.find('.reconnecttimer'); + const $cancelButton = $errorMessageModal.find('#cancelreconnect'); expect($countDownTimer.is(':visible')).to.be(false); expect($cancelButton.is(':visible')).to.be(false); @@ -51,19 +49,17 @@ describe('Automatic pad reload on Force Reconnect message', function() { }); }); - context('and user does not click on Cancel until timer expires', function() { - var padWasReloaded = false; + context('and user does not click on Cancel until timer expires', function () { + let padWasReloaded = false; - beforeEach(function() { - $originalPadFrame.one('load', function() { + beforeEach(function () { + $originalPadFrame.one('load', () => { padWasReloaded = true; }); }); - it('reloads the pad', function(done) { - helper.waitFor(function() { - return padWasReloaded; - }, 5000).done(done); + it('reloads the pad', function (done) { + helper.waitFor(() => padWasReloaded, 5000).done(done); this.timeout(5000); }); diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index 0779a43c4b3..70c850ca8cc 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -1,118 +1,143 @@ -var srcFolder = "../../../src/node_modules/"; -var wd = require(srcFolder + "wd"); -var async = require(srcFolder + "async"); +var srcFolder = '../../../src/node_modules/'; +var wd = require(`${srcFolder}wd`); +var async = require(`${srcFolder}async`); var config = { - host: "ondemand.saucelabs.com" - , port: 80 - , username: process.env.SAUCE_USER - , accessKey: process.env.SAUCE_ACCESS_KEY -} + host: 'ondemand.saucelabs.com', + port: 80, + username: process.env.SAUCE_USER, + accessKey: process.env.SAUCE_ACCESS_KEY, +}; var allTestsPassed = true; +// overwrite the default exit code +// in case not all worker can be run (due to saucelabs limits), `queue.drain` below will not be called +// and the script would silently exit with error code 0 +process.exitCode = 2; +process.on('exit', (code) => { + if (code === 2) { + console.log('\x1B[31mFAILED\x1B[39m Not all saucelabs runner have been started.'); + } +}); -var sauceTestWorker = async.queue(function (testSettings, callback) { - var browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); - var name = process.env.GIT_HASH + " - " + testSettings.browserName + " " + testSettings.version + ", " + testSettings.platform; +var sauceTestWorker = async.queue((testSettings, callback) => { + const browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); + const name = `${process.env.GIT_HASH} - ${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; testSettings.name = name; - testSettings["public"] = true; - testSettings["build"] = process.env.GIT_HASH; - testSettings["extendedDebugging"] = true; // console.json can be downloaded via saucelabs, don't know how to print them into output of the tests - testSettings["tunnelIdentifier"] = process.env.TRAVIS_JOB_NUMBER; - - browser.init(testSettings).get("http://localhost:9001/tests/frontend/", function(){ - var url = "https://saucelabs.com/jobs/" + browser.sessionID; - console.log("Remote sauce test '" + name + "' started! " + url); - - //tear down the test excecution - var stopSauce = function(success,timesup){ - clearInterval(getStatusInterval); - clearTimeout(timeout); - - browser.quit(function(){ - if(!success){ - allTestsPassed = false; - } - - // if stopSauce is called via timeout (in contrast to via getStatusInterval) than the log of up to the last - // five seconds may not be available here. It's an error anyway, so don't care about it. - var testResult = knownConsoleText.replace(/\[red\]/g,'\x1B[31m').replace(/\[yellow\]/g,'\x1B[33m') - .replace(/\[green\]/g,'\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); - testResult = testResult.split("\\n").map(function(line){ - return "[" + testSettings.browserName + " " + testSettings.platform + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line; - }).join("\n"); - - console.log(testResult); - if (timesup) { - console.log("[" + testSettings.browserName + " " + testSettings.platform + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] \x1B[31mFAILED\x1B[39m allowed test duration exceeded"); - } - console.log("Remote sauce test '" + name + "' finished! " + url); - - callback(); - }); - } - - /** - * timeout if a test hangs or the job exceeds 9.5 minutes + testSettings.public = true; + testSettings.build = process.env.GIT_HASH; + testSettings.extendedDebugging = true; // console.json can be downloaded via saucelabs, don't know how to print them into output of the tests + testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { + const url = `https://saucelabs.com/jobs/${browser.sessionID}`; + console.log(`Remote sauce test '${name}' started! ${url}`); + + // tear down the test excecution + const stopSauce = function (success, timesup) { + clearInterval(getStatusInterval); + clearTimeout(timeout); + + browser.quit(() => { + if (!success) { + allTestsPassed = false; + } + + // if stopSauce is called via timeout (in contrast to via getStatusInterval) than the log of up to the last + // five seconds may not be available here. It's an error anyway, so don't care about it. + printLog(logIndex); + + if (timesup) { + console.log(`[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] \x1B[31mFAILED\x1B[39m allowed test duration exceeded`); + } + console.log(`Remote sauce test '${name}' finished! ${url}`); + + callback(); + }); + }; + + /** + * timeout if a test hangs or the job exceeds 14.5 minutes * It's necessary because if travis kills the saucelabs session due to inactivity, we don't get any output * @todo this should be configured in testSettings, see https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts */ - var timeout = setTimeout(function(){ - stopSauce(false,true); - }, 570000); // travis timeout is 10 minutes, set this to a slightly lower value - - var knownConsoleText = ""; - var getStatusInterval = setInterval(function(){ - browser.eval("$('#console').text()", function(err, consoleText){ - if(!consoleText || err){ - return; - } - knownConsoleText = consoleText; - - if(knownConsoleText.indexOf("FINISHED") > 0){ - let match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); - // finished without failures - if (match[2] && match[2] == '0'){ - stopSauce(true); + var timeout = setTimeout(() => { + stopSauce(false, true); + }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value + + let knownConsoleText = ''; + // how many characters of the log have been sent to travis + let logIndex = 0; + var getStatusInterval = setInterval(() => { + browser.eval("$('#console').text()", (err, consoleText) => { + if (!consoleText || err) { + return; + } + knownConsoleText = consoleText; + + if (knownConsoleText.indexOf('FINISHED') > 0) { + const match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); + // finished without failures + if (match[2] && match[2] == '0') { + stopSauce(true); // finished but some tests did not return or some tests failed - } else { - stopSauce(false); - } + } else { + stopSauce(false); } - }); - }, 5000); - }); + } else { + // not finished yet + printLog(logIndex); + logIndex = knownConsoleText.length; + } + }); + }, 5000); + + /** + * Replaces color codes in the test runners log, appends + * browser name, platform etc. to every line and prints them. + * + * @param {number} index offset from where to start + */ + function printLog(index) { + let testResult = knownConsoleText.substring(index).replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') + .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); + testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] ${line}`).join('\n'); -}, 6); //run 6 tests in parrallel + console.log(testResult); + } + }); +}, 6); // run 6 tests in parrallel // 1) Firefox on Linux sauceTestWorker.push({ - 'platform' : 'Windows 7' - , 'browserName' : 'firefox' - , 'version' : '52.0' + platform: 'Windows 7', + browserName: 'firefox', + version: '52.0', }); // 2) Chrome on Linux sauceTestWorker.push({ - 'platform' : 'Windows 7' - , 'browserName' : 'chrome' - , 'version' : '55.0' - , 'args' : ['--use-fake-device-for-media-stream'] + platform: 'Windows 7', + browserName: 'chrome', + version: '55.0', + args: ['--use-fake-device-for-media-stream'], }); +/* // 3) Safari on OSX 10.15 sauceTestWorker.push({ 'platform' : 'OS X 10.15' , 'browserName' : 'safari' , 'version' : '13.1' }); +*/ // 4) Safari on OSX 10.14 sauceTestWorker.push({ - 'platform' : 'OS X 10.14' - , 'browserName' : 'safari' - , 'version' : '12.0' + platform: 'OS X 10.15', + browserName: 'safari', + version: '13.1', }); // IE 10 doesn't appear to be working anyway /* @@ -125,17 +150,17 @@ sauceTestWorker.push({ */ // 5) Edge on Win 10 sauceTestWorker.push({ - 'platform' : 'Windows 10' - , 'browserName' : 'microsoftedge' - , 'version' : '83.0' + platform: 'Windows 10', + browserName: 'microsoftedge', + version: '83.0', }); // 6) Firefox on Win 7 sauceTestWorker.push({ - 'platform' : 'Windows 7' - , 'browserName' : 'firefox' - , 'version' : '78.0' + platform: 'Windows 7', + browserName: 'firefox', + version: '78.0', }); -sauceTestWorker.drain(function() { +sauceTestWorker.drain(() => { process.exit(allTestsPassed ? 0 : 1); }); diff --git a/tests/frontend/travis/runner.sh b/tests/frontend/travis/runner.sh index ffc6bbd5b12..da28ec1efd3 100755 --- a/tests/frontend/travis/runner.sh +++ b/tests/frontend/travis/runner.sh @@ -1,48 +1,45 @@ -#!/bin/bash -if [ -z "${SAUCE_USERNAME}" ]; then echo "SAUCE_USERNAME is unset - exiting"; exit 1; fi -if [ -z "${SAUCE_ACCESS_KEY}" ]; then echo "SAUCE_ACCESS_KEY is unset - exiting"; exit 1; fi +#!/bin/sh -# do not continue if there is an error -set -eu +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } -# source: https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 -MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +[ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting" +[ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting" + +MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1 # reliably move to the etherpad base folder before running it -cd "${MY_DIR}/../../../" - -# start Etherpad, assuming all dependencies are already installed. -# -# This is possible because the "install" section of .travis.yml already contains -# a call to bin/installDeps.sh -echo "Running Etherpad directly, assuming bin/installDeps.sh has already been run" -node node_modules/ep_etherpad-lite/node/server.js "${@}" & - -echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9001" - -# wait for at most 15 seconds until Etherpad starts accepting connections -# -# modified from: -# https://unix.stackexchange.com/questions/5277/how-do-i-tell-a-script-to-wait-for-a-process-to-start-accepting-requests-on-a-po#349138 -# -(timeout 15 bash -c 'until echo > /dev/tcp/localhost/9001; do sleep 0.5; done') || \ - (echo "Could not connect to Etherpad on http://localhost:9001" ; exit 1) - -echo "Successfully connected to Etherpad on http://localhost:9001" - -# On the Travis VM, remote_runner.js is found at -# /home/travis/build/ether/[secure]/tests/frontend/travis/remote_runner.js -# which is the same directory that contains this script. -# Let's move back there. -# -# Probably remote_runner.js is injected by Saucelabs. -cd "${MY_DIR}" +try cd "${MY_DIR}/../../../" + +log "Assuming bin/installDeps.sh has already been run" +node node_modules/ep_etherpad-lite/node/server.js --experimental-worker "${@}" & +ep_pid=$! + +log "Waiting for Etherpad to accept connections (http://localhost:9001)..." +connected=false +can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true +} +now() { date +%s; } +start=$(now) +while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 +done +[ "$connected" = true ] \ + || fatal "Timed out waiting for Etherpad to accept connections" +log "Successfully connected to Etherpad on http://localhost:9001" # start the remote runner -echo "Now starting the remote runner" +try cd "${MY_DIR}" +log "Starting the remote runner..." node remote_runner.js exit_code=$? -kill $(cat /tmp/sauce.pid) - -exit $exit_code +kill "$(cat /tmp/sauce.pid)" +kill "$ep_pid" && wait "$ep_pid" +log "Done." +exit "$exit_code" diff --git a/tests/frontend/travis/runnerBackend.sh b/tests/frontend/travis/runnerBackend.sh index c595dce0220..f829d03870a 100755 --- a/tests/frontend/travis/runnerBackend.sh +++ b/tests/frontend/travis/runnerBackend.sh @@ -1,50 +1,49 @@ -#!/bin/bash +#!/bin/sh -# do not continue if there is an error -set -u +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } -# source: https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 -MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +MY_DIR=$(try cd "${0%/*}" && try pwd) || fatal "failed to find script directory" # reliably move to the etherpad base folder before running it -cd "${MY_DIR}/../../../" - -# Set soffice to /usr/bin/soffice -sed 's#\"soffice\": null,#\"soffice\":\"/usr/bin/soffice\",#g' settings.json.template > settings.json.soffice - -# Set allowAnyoneToImport to true -sed 's/\"allowAnyoneToImport\": false,/\"allowAnyoneToImport\": true,/g' settings.json.soffice > settings.json.allowImport - -# Set "max": 10 to 100 to not agressively rate limit -sed 's/\"max\": 10/\"max\": 100/g' settings.json.allowImport > settings.json.rateLimit - -# Set "points": 10 to 1000 to not agressively rate limit commits -sed 's/\"points\": 10/\"points\": 1000/g' settings.json.rateLimit > settings.json - -# start Etherpad, assuming all dependencies are already installed. -# -# This is possible because the "install" section of .travis.yml already contains -# a call to bin/installDeps.sh -echo "Running Etherpad directly, assuming bin/installDeps.sh has already been run" -node node_modules/ep_etherpad-lite/node/server.js "${@}" > /dev/null & - -echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9001" - -# wait for at most 15 seconds until Etherpad starts accepting connections -# -# modified from: -# https://unix.stackexchange.com/questions/5277/how-do-i-tell-a-script-to-wait-for-a-process-to-start-accepting-requests-on-a-po#349138 -# -(timeout 15 bash -c 'until echo > /dev/tcp/localhost/9001; do sleep 0.5; done') || \ - (echo "Could not connect to Etherpad on http://localhost:9001" ; exit 1) - -echo "Successfully connected to Etherpad on http://localhost:9001" - -# run the backend tests -echo "Now run the backend tests" -cd src - -failed=0 -npm run test || failed=1 - -exit $failed +try cd "${MY_DIR}/../../../" + +try sed -e ' +s!"soffice":[^,]*!"soffice": "/usr/bin/soffice"! +# Reduce rate limit aggressiveness +s!"max":[^,]*!"max": 100! +s!"points":[^,]*!"points": 1000! +# GitHub does not like our output +s!"loglevel":[^,]*!"loglevel": "WARN"! +' settings.json.template >settings.json + +log "Assuming bin/installDeps.sh has already been run" +node node_modules/ep_etherpad-lite/node/server.js "${@}" & +ep_pid=$! + +log "Waiting for Etherpad to accept connections (http://localhost:9001)..." +connected=false +can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true +} +now() { date +%s; } +start=$(now) +while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 +done +[ "$connected" = true ] \ + || fatal "Timed out waiting for Etherpad to accept connections" +log "Successfully connected to Etherpad on http://localhost:9001" + +log "Running the backend tests..." +try cd src +npm test +exit_code=$? + +kill "$ep_pid" && wait "$ep_pid" +log "Done." +exit "$exit_code" diff --git a/tests/frontend/travis/runnerLoadTest.sh b/tests/frontend/travis/runnerLoadTest.sh index 5ac44775843..50ffcbb499e 100755 --- a/tests/frontend/travis/runnerLoadTest.sh +++ b/tests/frontend/travis/runnerLoadTest.sh @@ -1,51 +1,51 @@ -#!/bin/bash +#!/bin/sh -# do not continue if there is an error -set -eu +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } -# source: https://stackoverflow.com/questions/59895/get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 -MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1 # reliably move to the etherpad base folder before running it -cd "${MY_DIR}/../../../" - -# Set "points": 10 to 1000 to not agressively rate limit commits -sed 's/\"points\": 10/\"points\": 1000/g' settings.json.template > settings.json.points -# And enable loadTest -sed 's/\"loadTest\": false,/\"loadTest\": true,/g' settings.json.points > settings.json - -# start Etherpad, assuming all dependencies are already installed. -# -# This is possible because the "install" section of .travis.yml already contains -# a call to bin/installDeps.sh -echo "Running Etherpad directly, assuming bin/installDeps.sh has already been run" - -node node_modules/ep_etherpad-lite/node/server.js "${@}" > /dev/null & - -echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9001" - -# wait for at most 15 seconds until Etherpad starts accepting connections -# -# modified from: -# https://unix.stackexchange.com/questions/5277/how-do-i-tell-a-script-to-wait-for-a-process-to-start-accepting-requests-on-a-po#349138 -# -(timeout 15 bash -c 'until echo > /dev/tcp/localhost/9001; do sleep 0.5; done') || \ - (echo "Could not connect to Etherpad on http://localhost:9001" ; exit 1) - -echo "Successfully connected to Etherpad on http://localhost:9001" - -# Build the minified files? -curl http://localhost:9001/p/minifyme -f -s > /dev/null +try cd "${MY_DIR}/../../../" + +try sed -e ' +s!"loadTest":[^,]*!"loadTest": true! +# Reduce rate limit aggressiveness +s!"points":[^,]*!"points": 1000! +' settings.json.template >settings.json + +log "Assuming bin/installDeps.sh has already been run" +node node_modules/ep_etherpad-lite/node/server.js "${@}" >/dev/null & +ep_pid=$! + +log "Waiting for Etherpad to accept connections (http://localhost:9001)..." +connected=false +can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true +} +now() { date +%s; } +start=$(now) +while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 +done +[ "$connected" = true ] \ + || fatal "Timed out waiting for Etherpad to accept connections" +log "Successfully connected to Etherpad on http://localhost:9001" + +# Build the minified files +try curl http://localhost:9001/p/minifyme -f -s >/dev/null # just in case, let's wait for another 10 seconds before going on sleep 10 -# run the backend tests -echo "Now run the load tests for 30 seconds and if it stalls before 100 then error" -etherpad-loadtest -d 30 +log "Running the load tests..." +etherpad-loadtest -d 25 exit_code=$? -kill $! -sleep 5 - -exit $exit_code +kill "$ep_pid" && wait "$ep_pid" +log "Done." +exit "$exit_code" diff --git a/tests/frontend/travis/sauce_tunnel.sh b/tests/frontend/travis/sauce_tunnel.sh index 4ab6e81a4fd..45827d31e92 100755 --- a/tests/frontend/travis/sauce_tunnel.sh +++ b/tests/frontend/travis/sauce_tunnel.sh @@ -1,23 +1,37 @@ -#!/bin/bash +#!/bin/sh + +pecho() { printf %s\\n "$*"; } +log() { pecho "$@"; } +error() { log "ERROR: $@" >&2; } +fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } + +[ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting" +[ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting" + # download and unzip the sauce connector # # ACHTUNG: as of 2019-12-21, downloading sc-latest-linux.tar.gz does not work. -# It is necessary to explicitly download a specific version, for -# example https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz -# Supported versions are currently listed at: -# https://wiki.saucelabs.com/display/DOCS/Downloading+Sauce+Connect+Proxy -if [ -z "${SAUCE_USERNAME}" ]; then echo "SAUCE_USERNAME is unset - exiting"; exit 1; fi -if [ -z "${SAUCE_ACCESS_KEY}" ]; then echo "SAUCE_ACCESS_KEY is unset - exiting"; exit 1; fi - -curl https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz > /tmp/sauce.tar.gz -tar zxf /tmp/sauce.tar.gz --directory /tmp -mv /tmp/sc-*-linux /tmp/sauce_connect +# It is necessary to explicitly download a specific version, for example +# https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz Supported versions are +# currently listed at: +# https://wiki.saucelabs.com/display/DOCS/Downloading+Sauce+Connect+Proxy +try curl -o /tmp/sauce.tar.gz \ + https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz +try tar zxf /tmp/sauce.tar.gz --directory /tmp +try mv /tmp/sc-*-linux /tmp/sauce_connect -# start the sauce connector in background and make sure it doesn't output the secret key -(/tmp/sauce_connect/bin/sc --user "${SAUCE_USERNAME}" --key "${SAUCE_ACCESS_KEY}" -i "${TRAVIS_JOB_NUMBER}" --pidfile /tmp/sauce.pid --readyfile /tmp/tunnel > /dev/null )& +# start the sauce connector in background and make sure it doesn't output the +# secret key +try rm -f /tmp/tunnel +/tmp/sauce_connect/bin/sc \ + --user "${SAUCE_USERNAME}" \ + --key "${SAUCE_ACCESS_KEY}" \ + -i "${TRAVIS_JOB_NUMBER}" \ + --pidfile /tmp/sauce.pid \ + --readyfile /tmp/tunnel >/dev/null & # wait for the tunnel to build up -while [ ! -e "/tmp/tunnel" ] - do - sleep 1 +while ! [ -e "/tmp/tunnel" ]; do + sleep 1 done diff --git a/tests/ratelimit/Dockerfile.anotherip b/tests/ratelimit/Dockerfile.anotherip new file mode 100644 index 00000000000..5b9d1d21a8c --- /dev/null +++ b/tests/ratelimit/Dockerfile.anotherip @@ -0,0 +1,4 @@ +FROM node:alpine3.12 +WORKDIR /tmp +RUN npm i etherpad-cli-client +COPY ./tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/tests/ratelimit/Dockerfile.nginx b/tests/ratelimit/Dockerfile.nginx new file mode 100644 index 00000000000..ba8dd358f05 --- /dev/null +++ b/tests/ratelimit/Dockerfile.nginx @@ -0,0 +1,2 @@ +FROM nginx +COPY ./tests/ratelimit/nginx.conf /etc/nginx/nginx.conf diff --git a/tests/ratelimit/nginx.conf b/tests/ratelimit/nginx.conf new file mode 100644 index 00000000000..97f0a9e00ea --- /dev/null +++ b/tests/ratelimit/nginx.conf @@ -0,0 +1,26 @@ +events {} +http { + server { + access_log /dev/fd/1; + error_log /dev/fd/2; + location / { + proxy_pass http://172.23.42.2:9001/; + proxy_set_header Host $host; + proxy_pass_header Server; + # be careful, this line doesn't override any proxy_buffering on set in a conf.d/file.conf + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; # http://wiki.nginx.org/HttpProxyModule + proxy_set_header X-Forwarded-For $remote_addr; # EP logs to show the actual remote IP + proxy_set_header X-Forwarded-Proto $scheme; # for EP to set secure cookie flag when https is used + proxy_set_header Host $host; # pass the host header + proxy_http_version 1.1; # recommended with keepalive connections + # WebSocket proxying - from http://nginx.org/en/docs/http/websocket.html + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } +} diff --git a/tests/ratelimit/send_changesets.js b/tests/ratelimit/send_changesets.js new file mode 100644 index 00000000000..b0d994c8c65 --- /dev/null +++ b/tests/ratelimit/send_changesets.js @@ -0,0 +1,24 @@ +try { + var etherpad = require('../../src/node_modules/etherpad-cli-client'); + // ugly +} catch { + var etherpad = require('etherpad-cli-client'); +} +const pad = etherpad.connect(process.argv[2]); +pad.on('connected', () => { + setTimeout(() => { + setInterval(() => { + pad.append('1'); + }, process.argv[3]); + }, 500); // wait because CLIENT_READY message is included in ratelimit + + setTimeout(() => { + process.exit(0); + }, 11000); +}); +// in case of disconnect exit code 1 +pad.on('message', (message) => { + if (message.disconnect == 'rateLimited') { + process.exit(1); + } +}); diff --git a/tests/ratelimit/testlimits.sh b/tests/ratelimit/testlimits.sh new file mode 100755 index 00000000000..778348dcc5c --- /dev/null +++ b/tests/ratelimit/testlimits.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +#sending changesets every 101ms should not trigger ratelimit +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_101ms 101 +if [[ $? -ne 0 ]];then + echo "FAILED: ratelimit was triggered when sending every 101 ms" + exit 1 +fi + +#sending changesets every 99ms should trigger ratelimit +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_99ms 99 +if [[ $? -ne 1 ]];then + echo "FAILED: ratelimit was not triggered when sending every 99 ms" + exit 1 +fi + +#sending changesets every 101ms via proxy +node send_changesets.js http://127.0.0.1:8081/p/BACKEND_TEST_ratelimit_101ms 101 & +pid1=$! + +#sending changesets every 101ms via second IP and proxy +docker exec anotherip node /tmp/send_changesets.js http://172.23.42.1:80/p/BACKEND_TEST_ratelimit_101ms_via_second_ip 101 & +pid2=$! + +wait $pid1 +exit1=$? +wait $pid2 +exit2=$? + +echo "101ms with proxy returned with ${exit1}" +echo "101ms via another ip returned with ${exit2}" + +if [[ $exit1 -eq 1 || $exit2 -eq 1 ]];then + echo "FAILED: ratelimit was triggered during proxy and requests via second ip" + exit 1 +fi