diff --git a/.dockerignore b/.dockerignore index 28c6753f5..d8d3a3ebe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,6 +19,7 @@ Dockerfile .git/ORIG_HEAD .git/packed-refs .git/refs/remotes/ +.git/rr-cache/ .gitignore settings.json diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4adcdb102..041f7c1a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,3 @@ -IMPORTANT: Please disable plugins prior to posting a bug report. If you have a problem with a plugin please post on the plugin repository. Thanks! - --- name: Bug report about: Create a report to help us improve @@ -9,6 +7,8 @@ assignees: '' --- + + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..58200fd3c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/src" + schedule: + interval: "daily" + versioning-strategy: "increase" + - package-ecosystem: "npm" + directory: "/src/bin/doc" + schedule: + interval: "daily" + versioning-strategy: "increase" diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index ec8b0859e..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - Bug - - Serious Bug - - Minor bug - - Black hole bug - - Special case Bug - - Upstream bug - - Feature Request -# Label to use when marking an issue as stale -staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 5f2dedc88..c16b79a5e 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -3,6 +3,9 @@ name: "Backend tests" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: withoutpluginsLinux: # run on pushes to any branch @@ -15,11 +18,11 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [14, 16, 18] steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -52,11 +55,11 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [14, 16, 18] steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -117,11 +120,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json @@ -150,11 +153,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4fa9785f0..ceb8d29b5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,14 +9,21 @@ on: schedule: - cron: '0 13 * * 1' +permissions: + contents: read + jobs: analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report name: Analyze runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -28,10 +35,10 @@ jobs: if: ${{ github.event_name == 'pull_request' }} - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..0e72a00ef --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ef728a6e2..479194fb2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,19 +8,26 @@ on: - 'v?[0-9]+.[0-9]+.[0-9]+' env: TEST_TAG: etherpad/etherpad:test +permissions: + contents: read + jobs: docker: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - + name: Set up QEMU + if: github.event_name == 'push' + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Build and export to Docker - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: . load: true @@ -39,16 +46,25 @@ jobs: - name: Test run: | - docker run --rm -d -p 9001:9001 ${{ env.TEST_TAG }} + docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }} + docker logs -f test & ./src/bin/installDeps.sh - sleep 3 + while true; do + echo "Waiting for Docker container to start..." + status=$(docker container inspect -f '{{.State.Health.Status}}' test) || exit 1 + case ${status} in + healthy) break;; + starting) sleep 2;; + *) printf %s\\n "unexpected status: ${status}" >&2; exit 1;; + esac + done (cd src && npm run test-container) git clean -dxf . - name: Docker meta if: github.event_name == 'push' id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v4 with: images: etherpad/etherpad tags: | @@ -59,16 +75,17 @@ jobs: - name: Log in to Docker Hub if: github.event_name == 'push' - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push if: github.event_name == 'push' - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 54537e84a..3be9e34cc 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -3,6 +3,9 @@ name: "Frontend admin tests powered by Sauce Labs" on: [push] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: withplugins: name: with plugins @@ -11,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [14, 16, 18] steps: - @@ -33,7 +36,7 @@ jobs: printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}' - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index c05248b9f..e0d235d2d 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -3,6 +3,9 @@ name: "Frontend tests powered by Sauce Labs" on: [push] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: withoutplugins: name: without plugins @@ -27,11 +30,11 @@ jobs: printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json @@ -91,11 +94,11 @@ jobs: printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json diff --git a/.github/workflows/lint-package-lock.yml b/.github/workflows/lint-package-lock.yml index cb34c84d1..bc05a1a52 100644 --- a/.github/workflows/lint-package-lock.yml +++ b/.github/workflows/lint-package-lock.yml @@ -3,6 +3,9 @@ name: "Lint" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: lint-package-lock: # run on pushes to any branch @@ -15,11 +18,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 3941dcc57..9d8c4fd9b 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -3,6 +3,9 @@ name: "Loadtest" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: withoutplugins: # run on pushes to any branch @@ -15,11 +18,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json @@ -45,11 +48,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json @@ -102,11 +105,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 3208cdb23..7df7aa4ce 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -3,6 +3,9 @@ name: "rate limit" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: ratelimit: # run on pushes to any branch @@ -15,11 +18,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..4b5f6c404 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,18 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 6 * * *' +permissions: + issues: write + pull-requests: write +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v7 + with: + close-issue-label: wontfix + close-pr-label: wontfix + days-before-close: -1 + exempt-issue-labels: 'pinned,security,Bug,Serious Bug,Minor bug,Black hole bug,Special case Bug,Upstream bug,Feature Request' + exempt-pr-labels: 'pinned,security,Bug,Serious Bug,Minor bug,Black hole bug,Special case Bug,Upstream bug,Feature Request' diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 54e3527ea..a00e9540d 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -3,6 +3,9 @@ name: "Upgrade from latest release" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: withpluginsLinux: # run on pushes to any branch @@ -15,11 +18,11 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [14, 16, 18] steps: - name: Check out latest release - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: master - @@ -64,7 +67,7 @@ jobs: - name: Run the backend tests run: cd src && npm test - # Because actions/checkout@v2 is called with "ref: master" and without + # Because actions/checkout@v3 is called with "ref: master" and without # "fetch-depth: 0", the local clone does not have the ${GITHUB_SHA} # commit. Fetch ${GITHUB_REF} to get the ${GITHUB_SHA} commit. Note that a # plain "git fetch" only fetches "normal" references (refs/heads/* and @@ -87,11 +90,10 @@ jobs: run: cd src && npm test - name: Install Cypress - run: npm install cypress -g + run: cd src && npm install cypress - name: Run Etherpad & Test Frontend run: | node src/node/server.js & curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test - cd src/tests/frontend - cypress run --spec cypress/integration/test.js --config-file cypress/cypress.json + ./src/node_modules/cypress/bin/cypress run --config-file src/tests/frontend/cypress/cypress.config.js diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml deleted file mode 100644 index 7bf22a144..000000000 --- a/.github/workflows/windows-installer.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: "Windows Installer" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - build: - # 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 Zip & Exe - runs-on: windows-latest - steps: - - - uses: msys2/setup-msys2@v2 - with: - path-type: inherit - install: >- - zip - - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: actions/setup-node@v3 - with: - node-version: 12 - cache: 'npm' - cache-dependency-path: | - src/package-lock.json - src/bin/doc/package-lock.json - - - name: Install all dependencies and symlink for ep_etherpad-lite - shell: msys2 {0} - run: src/bin/installDeps.sh - - - name: Run the backend tests - shell: msys2 {0} - run: cd src && npm test - - - name: Build the .zip - shell: msys2 {0} - run: src/bin/buildForWindows.sh - - - name: Extract the .zip into folder - run: 7z x etherpad-lite-win.zip -oetherpad-lite-new - - - name: Grab nsis config - run: git clone https://github.com/ether/etherpad_nsis.git - - - name: Create installer - uses: joncloud/makensis-action@v3.6 - with: - script-file: 'etherpad_nsis/etherpad.nsi' - - - name: Check something.. - run: ls etherpad_nsis - - - name: Archive production artifacts - uses: actions/upload-artifact@v2 - with: - name: etherpad-server-windows.exe - path: etherpad_nsis/etherpad-server-windows.exe diff --git a/.github/workflows/windows-zip.yml b/.github/workflows/windows.yml similarity index 56% rename from .github/workflows/windows-zip.yml rename to .github/workflows/windows.yml index 4cc93ca02..3622fedfe 100644 --- a/.github/workflows/windows-zip.yml +++ b/.github/workflows/windows.yml @@ -1,16 +1,19 @@ -name: "Windows Zip" +name: "Windows Build" # any branch is useful for testing before a PR is submitted on: [push, pull_request] +permissions: + contents: read + jobs: - build: + build-zip: # 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 + name: Build .zip runs-on: windows-latest steps: - @@ -21,11 +24,11 @@ jobs: zip - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | src/package-lock.json @@ -44,45 +47,79 @@ jobs: run: src/bin/buildForWindows.sh - name: Archive production artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: - name: etherpad-lite-win.zip - path: etherpad-lite-win.zip + name: etherpad-win.zip + path: etherpad-win.zip - deploy: + build-exe: + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + name: Build .exe + needs: build-zip + runs-on: windows-latest + steps: + - + name: Checkout repository + uses: actions/checkout@v3 + - + name: Download .zip + uses: actions/download-artifact@v3 + with: + name: etherpad-win.zip + path: .. + - + name: Extract .zip + working-directory: .. + run: 7z x etherpad-win.zip -oetherpad-zip + - + name: Create installer + uses: joncloud/makensis-action@v3.6 + with: + script-file: 'src/bin/nsis/etherpad.nsi' + - + name: Archive production artifacts + uses: actions/upload-artifact@v3 + with: + name: etherpad-win.exe + path: etherpad-win.exe + + deploy-zip: # run on pushes to any branch # run on PRs from external forks + permissions: + contents: none if: | (github.event_name != 'pull_request') || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) name: Deploy - needs: build + needs: build-zip runs-on: windows-latest steps: - name: Download zip - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: - name: etherpad-lite-win.zip + name: etherpad-win.zip - name: Extract Etherpad - run: 7z x etherpad-lite-win.zip -oetherpad + run: 7z x etherpad-win.zip -oetherpad - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 14 cache: 'npm' cache-dependency-path: | etherpad/src/package-lock.json etherpad/src/bin/doc/package-lock.json - name: Install Cypress - run: npm install cypress -g + run: cd src && npm install cypress - name: Run Etherpad run: | cd etherpad node node_modules\ep_etherpad-lite\node\server.js & curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test - cd src\tests\frontend - cypress run --spec cypress\integration\test.js --config-file cypress\cypress.json + src\node_modules\cypress\bin\cypress run --config-file src\tests\frontendcypress\cypress.config.js diff --git a/.gitignore b/.gitignore index 60638c50a..e2f0383a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ +/etherpad-win.exe +/etherpad-win.zip node_modules /settings.json !settings.json.template APIKEY.txt SESSIONKEY.txt -etherpad-lite-win.zip var/dirty.db *~ *.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c5d41fd7..524899dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,123 @@ +# 1.9.0 + +### Notable enhancements and fixes + +* Windows build: + * The bundled `node.exe` was upgraded from v12 to v16. + * The bundled `node.exe` is now a 64-bit executable. If you need the 32-bit + version you must download and install Node.js yourself. +* Improvements to login session management: + * `express_sid` cookies and `sessionstorage:*` database records are no longer + created unless `requireAuthentication` is `true` (or a plugin causes them to + be created). + * Login sessions now have a finite lifetime by default (10 days after + leaving). + * `sessionstorage:*` database records are automatically deleted when the login + session expires (with some exceptions that will be fixed in the future). + * Requests for static content (e.g., `/robots.txt`) and special pages (e.g., + the HTTP API, `/stats`) no longer create login session state. +* The following settings from `settings.json` are now applied as expected (they + were unintentionally ignored before): + * `padOptions.lang` + * `padOptions.showChat` + * `padOptions.userColor` + * `padOptions.userName` +* HTTP API: + * Fixed the return value of `getText` when called with a specific revision. + * Fixed a potential attribute pool corruption bug with + `copyPadWithoutHistory`. + * Mappings created by `createGroupIfNotExistsFor` are now removed from the + database when the group is deleted. + * Fixed race conditions in the `setText`, `appendText`, and `restoreRevision` + functions. + * Added an optional `authorId` parameter to `appendText`, + `copyPadWithoutHistory`, `createGroupPad`, `createPad`, `restoreRevision`, + `setHTML`, and `setText`, and bumped the latest API version to 1.3.0. +* Fixed a crash if the database is busy enough to cause a query timeout. +* New `/health` endpoint for getting information about Etherpad's health (see + [draft-inadarei-api-health-check-06](https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html)). +* Docker now uses the new `/health` endpoint for health checks, which avoids + issues when authentication is enabled. It also avoids the unnecessary creation + of database records for managing browser sessions. +* When copying a pad, the pad's records are copied in batches to avoid database + timeouts with large pads. +* Exporting a large pad to `.etherpad` format should be faster thanks to bulk + database record fetches. +* When importing an `.etherpad` file, records are now saved to the database in + batches to avoid database timeouts with large pads. + +#### For plugin authors + +* New `expressPreSession` server-side hook. +* Pad server-side hook changes: + * `padCheck`: New hook. + * `padCopy`: New `srcPad` and `dstPad` context properties. + * `padDefaultContent`: New hook. + * `padRemove`: New `pad` context property. +* The `db` property on Pad objects is now public. +* New `getAuthorId` server-side hook. +* New APIs for processing attributes: `ep_etherpad-lite/static/js/attributes` + (low-level API) and `ep_etherpad-lite/static/js/AttributeMap` (high-level + API). +* The `import` server-side hook has a new `ImportError` context property. +* New `exportEtherpad` and `importEtherpad` server-side hooks. +* The `handleMessageSecurity` and `handleMessage` server-side hooks have a new + `sessionInfo` context property that includes the user's author ID, the pad ID, + and whether the user only has read-only access. +* The `handleMessageSecurity` server-side hook can now be used to grant write + access for the current message only. +* The `init_` server-side hooks have a new `logger` context + property that plugins can use to log messages. +* Prevent infinite loop when exiting the server +* Bump dependencies + + +### Compatibility changes + +* Node.js v14.15.0 or later is now required. +* The default login session expiration (applicable if `requireAuthentication` is + `true`) changed from never to 10 days after the user leaves. + +#### For plugin authors + +* The `client` context property for the `handleMessageSecurity` and + `handleMessage` server-side hooks is deprecated; use the `socket` context + property instead. +* Pad server-side hook changes: + * `padCopy`: + * The `originalPad` context property is deprecated; use `srcPad` instead. + * The `destinationID` context property is deprecated; use `dstPad.id` + instead. + * `padCreate`: The `author` context property is deprecated; use the new + `authorId` context property instead. Also, the hook now runs asynchronously. + * `padLoad`: Now runs when a temporary Pad object is created during import. + Also, it now runs asynchronously. + * `padRemove`: The `padID` context property is deprecated; use `pad.id` + instead. + * `padUpdate`: The `author` context property is deprecated; use the new + `authorId` context property instead. Also, the hook now runs asynchronously. +* Returning `true` from a `handleMessageSecurity` hook function is deprecated; + return `'permitOnce'` instead. +* Changes to the `src/static/js/Changeset.js` library: + * The following attribute processing functions are deprecated (use the new + attribute APIs instead): + * `attribsAttributeValue()` + * `eachAttribNumber()` + * `makeAttribsString()` + * `opAttributeValue()` + * `opIterator()`: Deprecated in favor of the new `deserializeOps()` generator + function. + * `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()` + generator function. + * `newOp()`: Deprecated in favor of the new `Op` class. +* The `AuthorManager.getAuthor4Token()` function is deprecated; use the new + `AuthorManager.getAuthorId()` function instead. +* The exported database records covered by the `exportEtherpadAdditionalContent` + server-side hook now include keys like `${customPrefix}:${padId}:*`, not just + `${customPrefix}:${padId}`. +* Plugin locales should overwrite core's locales Stale +* Plugin locales overwrite core locales + # 1.8.18 Released: 2022-05-05 diff --git a/Dockerfile b/Dockerfile index c6339ae72..bec6af428 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,16 @@ # # Author: muxator -FROM node:14-buster-slim +FROM node:lts-slim LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" +ARG TIMEZONE= +RUN \ + [ -z "${TIMEZONE}" ] || { \ + ln -sf /usr/share/zoneinfo/"${TIMEZONE#/usr/share/zoneinfo/}" /etc/localtime; \ + dpkg-reconfigure -f noninteractive tzdata; \ + } + # plugins to install while building the container. By default no plugins are # installed. # If given a value, it has to be a space-separated, quoted list of plugin names. @@ -60,12 +67,12 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}" RUN export DEBIAN_FRONTEND=noninteractive; \ mkdir -p /usr/share/man/man1 && \ apt-get -qq update && \ + apt-get -qq dist-upgrade && \ apt-get -qq --no-install-recommends install \ ca-certificates \ git \ - curl \ ${INSTALL_ABIWORD:+abiword} \ - ${INSTALL_SOFFICE:+libreoffice} \ + ${INSTALL_SOFFICE:+libreoffice default-jre libreoffice-java-common} \ && \ apt-get -qq clean && \ rm -rf /var/lib/apt/lists/* @@ -85,7 +92,7 @@ COPY --chown=etherpad:etherpad ./ ./ # seems to confuse tools such as `npm outdated`, `npm update`, and some ESLint # rules. RUN { [ -z "${ETHERPAD_PLUGINS}" ] || \ - npm install --no-save ${ETHERPAD_PLUGINS}; } && \ + npm install --no-save --legacy-peer-deps ${ETHERPAD_PLUGINS}; } && \ src/bin/installDeps.sh && \ rm -rf ~/.npm @@ -95,7 +102,11 @@ COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json # Fix group permissions RUN chmod -R g=u . -HEALTHCHECK --interval=20s --timeout=3s CMD curl -f http://localhost:9001 || exit 1 +USER root +RUN cd src && npm link +USER etherpad + +HEALTHCHECK --interval=20s --timeout=3s CMD ["etherpad-healthcheck"] EXPOSE 9001 -CMD ["node", "src/node/server.js"] +CMD ["etherpad"] diff --git a/README.md b/README.md index e0c110b3f..b18bcdff5 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,78 @@ -# A real-time collaborative editor for the web +# Etherpad: A real-time collaborative editor for the web ![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](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. +## About -# Try it out -Etherpad is extremely flexible providing you the means to modify it to solve whatever problem your community has. We provide some demo instances for you try different experiences available within Etherpad. Pad content is automatically removed after 24 hours. +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. -* [Rich Editing](https://rich.etherpad.com) - A full rich text WYSIWYG editor. -* [Minimalist editor](https://minimalist.etherpad.com) - A minimalist editor that can be embedded within your tool. -* [Dark Mode](https://dark.etherpad.com) - Theme settings to have Etherpad start in dark mode, ideal for using Etherpad at night or for long durations. -* [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad. -* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad. -* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads. -* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation. -* [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad. +## Try it out -# Project Status +Etherpad is extremely flexible providing you the means to modify it to solve +whatever problem your community has. We provide some demo instances for you try +different experiences available within Etherpad. Pad content is automatically +removed after 24 hours. + + * [Rich Editing](https://rich.etherpad.com) - A full rich text WYSIWYG editor. + * [Minimalist editor](https://minimalist.etherpad.com) - A minimalist editor + that can be embedded within your tool. + * [Dark Mode](https://dark.etherpad.com) - Theme settings to have Etherpad + start in dark mode, ideal for using Etherpad at night or for long durations. + * [Images](https://image.etherpad.com) - Plugins to improve provide Image + support within a pad. + * [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio + chat in a pad. + * [Collaboration++](https://collab.etherpad.com) - Plugins to improve the + really-real time collaboration experience, suitable for busy pads. + * [Document Analysis](https://analysis.etherpad.com) - Plugins to improve + author and document analysis during and post creation. + * [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad + sharding which allows Etherpad to scale to ∞ number of Active Pads with up + to ~20,000 edits per second, per pad. + +## Project Status + +We're looking for maintainers and have some funding available. Please contact John McLear if you can help. ### Code Quality -[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/context:javascript) [![package.lock](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml) + +[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) +[![package.lock](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml) ### Testing -[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [![Windows Zip](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml) -[![Frontend admin tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [![Frontend tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [![Sauce Test Status](https://saucelabs.com/buildstatus/etherpad.svg)](https://saucelabs.com/u/etherpad) [![Windows Installer](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml) + +[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) +[![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) +[![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) +[![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml) +[![Frontend admin tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) +[![Frontend tests powered by Sauce Labs](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) +[![Sauce Test Status](https://saucelabs.com/buildstatus/etherpad.svg)](https://saucelabs.com/u/etherpad) +[![Windows Build](https://github.com/ether/etherpad-lite/actions/workflows/windows.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows.yml) ### Engagement -Docker Pulls [![Discord](https://img.shields.io/discord/741309013593030667?color=%2344b492)](https://discord.com/invite/daEjfhw) [![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins")](https://static.etherpad.org/index.html) ![Languages](https://img.shields.io/static/v1?label=Languages&message=105&color=%2344b492) ![Translation Coverage](https://img.shields.io/static/v1?label=Languages&message=98%&color=%2344b492) -# Installation +[![Docker Pulls](https://img.shields.io/docker/pulls/etherpad/etherpad?color=%2344b492)](https://hub.docker.com/r/etherpad/etherpad) +[![Discord](https://img.shields.io/discord/741309013593030667?color=%2344b492)](https://discord.com/invite/daEjfhw) +[![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins")](https://static.etherpad.org/index.html) +![Languages](https://img.shields.io/static/v1?label=Languages&message=105&color=%2344b492) +![Translation Coverage](https://img.shields.io/static/v1?label=Languages&message=98%&color=%2344b492) -## Requirements -- [Node.js](https://nodejs.org/) >= **12.13.0**. +## Installation -## GNU/Linux and other UNIX-like systems +### Requirements -### Quick install on Debian/Ubuntu -``` +[Node.js](https://nodejs.org/) >= **14.0.0**. + +### GNU/Linux and other UNIX-like systems + +#### Quick install on Debian/Ubuntu + +```sh 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 && @@ -45,103 +80,172 @@ cd etherpad-lite && src/bin/run.sh ``` -### Manual install +#### Manual install You'll need Git and [Node.js](https://nodejs.org/) installed. **As any user (we recommend creating a separate user called etherpad):** -1. Move to a folder where you want to install Etherpad. Clone the git repository: `git clone --branch master git://github.com/ether/etherpad-lite.git` -2. Change into the new directory containing the cloned source code: `cd etherpad-lite` -3. run `src/bin/run.sh` and open in your browser. + 1. Move to a folder where you want to install Etherpad. + 2. Clone the Git repository: `git clone --branch master + https://github.com/ether/etherpad-lite.git` + 3. Change into the new directory containing the cloned source code: `cd + etherpad-lite` + 4. Run `src/bin/run.sh` and open http://127.0.0.1:9001 in your browser. To update to the latest released version, execute `git pull origin`. The next start with `src/bin/run.sh` will update the dependencies. -[Next steps](#next-steps). +### Windows -## Windows +#### Prebuilt Windows package -### Prebuilt Windows package -This package runs on any Windows machine. You can perform a manual installation via git for development purposes, but as this uses symlinks which performs unreliably on Windows, please stick to the prebuilt package if possible. +This package runs on any Windows machine. You can perform a manual installation +via git for development purposes, but as this uses symlinks which performs +unreliably on Windows, please stick to the prebuilt package if possible. -1. [Download the latest Windows package](https://etherpad.org/#download) -2. Extract the folder + 1. [Download the latest Windows package](https://etherpad.org/#download) + 2. Extract the folder -Run `start.bat` and open in your browser. You like it? [Next steps](#next-steps). +Run `start.bat` and open in your browser. -### Manually install on Windows -You'll need [node.js](https://nodejs.org) and (optionally, though recommended) git. +#### Manually install on Windows -1. Grab the source, either - - download - - or `git clone --branch master https://github.com/ether/etherpad-lite.git` -2. With a "Run as administrator" command prompt execute - `src\bin\installOnWindows.bat` +You'll need [Node.js](https://nodejs.org) and (optionally, though recommended) +git. -Now, run `start.bat` and open in your browser. + 1. Grab the source, either: + * download + * or `git clone --branch master + https://github.com/ether/etherpad-lite.git` + 2. With a "Run as administrator" command prompt execute + `src\bin\installOnWindows.bat` + +Now, run `start.bat` and open http://localhost:9001 in your browser. Update to the latest version with `git pull origin`, then run `src\bin\installOnWindows.bat`, again. -If cloning to a subdirectory within another project, you may need to do the following: +If cloning to a subdirectory within another project, you may need to do the +following: -1. Start the server manually (e.g. `node src/node/server.js`) -2. Edit the db `filename` in `settings.json` to the relative directory with the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) -3. Add auto-generated files to the main project `.gitignore` + 1. Start the server manually (e.g. `node src/node/server.js`) + 2. Edit the db `filename` in `settings.json` to the relative directory with + the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) + 3. Add auto-generated files to the main project `.gitignore` -## Docker container +### Docker container Find [here](doc/docker.md) information on running Etherpad in a container. -# Next Steps +## Plugins -## Tweak the settings -You can modify the settings in `settings.json`. -If you need to handle multiple settings files, you can pass the path to a -settings file to `src/bin/run.sh` using the `-s|--settings` option: this allows -you to run multiple Etherpad instances from the same installation. -Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt. -**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. -Once you have access to your `/admin` section settings can be modified through the web browser. - -If you are planning to use Etherpad in a production environment, you should use a dedicated database such as `mysql`, since the `dirtyDB` database driver is only for testing and/or development purposes. - -## Secure your installation -If you have enabled authentication in `users` section in `settings.json`, it is a good security practice to **store hashes instead of plain text passwords** in that file. This is _especially_ advised if you are running a production installation. - -Please install [ep_hash_auth plugin](https://www.npmjs.com/package/ep_hash_auth) and configure it. -If you prefer, `ep_hash_auth` also gives you the option of storing the users in a custom directory in the file system, without having to edit `settings.json` and restart Etherpad each time. - -## Customize functionalities with plugins +Etherpad is very customizable through plugins. ![Basic install](doc/images/etherpad_basic.png "Basic Installation") ![Full Features](doc/images/etherpad_full_features.png "You can add a lot of plugins !") -Etherpad is very customizable through plugins. Instructions for installing themes and plugins can be found in [the plugin wiki article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins). +### Available Plugins -## Getting the full features -Run the following command in your Etherpad folder to get all of the features visible in the demo gif: +For a list of available plugins, see the [plugins +site](https://static.etherpad.org). -``` -npm install --no-save --legacy-peer-deps ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2 +### Plugin Installation + +You can install plugins from the admin web interface (e.g., +http://127.0.0.1:9001/admin/plugins). + +Alternatively, you can install plugins from the command line: + +```sh +cd /path/to/etherpad-lite +# The `--no-save` and `--legacy-peer-deps` arguments are necessary to work +# around npm quirks. +npm install --no-save --legacy-peer-deps ep_${plugin_name} ``` -## Customize the style with skin variants +Also see [the plugin wiki +article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins). -Open in your browser and start playing ! +### Suggested Plugins + +Run the following command in your Etherpad folder to get all of the features +visible in the above demo gif: + +```sh +npm install --no-save --legacy-peer-deps \ + ep_align \ + ep_comments_page \ + ep_embedded_hyperlinks2 \ + ep_font_color \ + ep_headings2 \ + ep_markdown \ + ep_webrtc +``` + +For user authentication, you are encouraged to run an [OpenID +Connect](https://openid.net/connect/) identity provider (OP) and install the +following plugins: + + * [ep_openid_connect](https://github.com/ether/ep_openid_connect#readme) to + authenticate against your OP. + * [ep_guest](https://github.com/ether/ep_guest#readme) to create a + "guest" account that has limited access (e.g., read-only access). + * [ep_user_displayname](https://github.com/ether/ep_user_displayname#readme) + to automatically populate each user's displayed name from your OP. + * [ep_stable_authorid](https://github.com/ether/ep_stable_authorid#readme) so + that each user's chosen color, display name, comment ownership, etc. is + strongly linked to their account. + +## Next Steps + +### Tweak the settings + +You can modify the settings in `settings.json`. If you need to handle multiple +settings files, you can pass the path to a settings file to `src/bin/run.sh` +using the `-s|--settings` option: this allows you to run multiple Etherpad +instances from the same installation. Similarly, `--credentials` can be used to +give a settings override file, `--apikey` to give a different APIKEY.txt file +and `--sessionkey` to give a non-default `SESSIONKEY.txt`. **Each configuration +parameter can also be set via an environment variable**, using the syntax +`"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to +`settings.json.template`. Once you have access to your `/admin` section, +settings can be modified through the web browser. + +If you are planning to use Etherpad in a production environment, you should use +a dedicated database such as `mysql`, since the `dirtyDB` database driver is +only for testing and/or development purposes. + +### Secure your installation + +If you have enabled authentication in `users` section in `settings.json`, it is +a good security practice to **store hashes instead of plain text passwords** in +that file. This is _especially_ advised if you are running a production +installation. + +Please install [ep_hash_auth plugin](https://www.npmjs.com/package/ep_hash_auth) +and configure it. If you prefer, `ep_hash_auth` also gives you the option of +storing the users in a custom directory in the file system, without having to +edit `settings.json` and restart Etherpad each time. + +### Customize the style with skin variants + +Open http://127.0.0.1:9001/p/test#skinvariantsbuilder in your browser and start +playing! ![Skin Variant](doc/images/etherpad_skin_variants.gif "Skin variants") ## Helpful resources -The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop resource for Tutorials and How-to's. + +The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop +resource for Tutorials and How-to's. Documentation can be found in `doc/`. -# Development +## Development -## Things you should know +### Things you should know You can debug Etherpad using `src/bin/debugRun.sh`. @@ -150,36 +254,63 @@ developers and advanced users. Be aware that it will skip the dependencies update, so remember to run `src/bin/installDeps.sh` after installing a new dependency or upgrading version. -If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading). +If you want to find out how Etherpad's `Easysync` works (the library that makes +it really realtime), start with this +[PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) +(complex, but worth reading). -## Contributing -Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md) +### Contributing -# Get in touch -The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues). +Read our [**Developer +Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md) -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 -# HTTP API -Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) -that allows your web application to manage pads, users and groups. It is recommended to use the [available client implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries) in order to interact with this API. +Etherpad is designed to be easily embeddable and provides a [HTTP +API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) that allows your web +application to manage pads, users and groups. It is recommended to use the +[available client +implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries) +in order to interact with this API. -OpenAPI (previously swagger) definitions for the API are exposed under `/api/openapi.json`. +OpenAPI (previously swagger) definitions for the API are exposed under +`/api/openapi.json`. -# jQuery plugin -There is a [jQuery plugin](https://github.com/ether/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website. +### jQuery plugin -# Plugin Framework -Etherpad offers a plugin framework, allowing you to easily add your own features. By default your Etherpad is extremely light-weight and it's up to you to customize your experience. Once you have Etherpad installed you should [visit the plugin page](https://static.etherpad.org/) and take control. +There is a [jQuery plugin](https://github.com/ether/etherpad-lite-jquery-plugin) +that helps you to embed Pads into your website. -# Translations / Localizations (i18n / l10n) -Etherpad comes with translations into all languages thanks to the team at [TranslateWiki](https://translatewiki.net/). +### Plugin Framework -If you require translations in [plugins](https://static.etherpad.org/) please send pull request to each plugin individually. +Etherpad offers a plugin framework, allowing you to easily add your own +features. By default your Etherpad is extremely light-weight and it's up to you +to customize your experience. Once you have Etherpad installed you should [visit +the plugin page](https://static.etherpad.org/) and take control. + +### Translations / Localizations (i18n / l10n) + +Etherpad comes with translations into all languages thanks to the team at +[TranslateWiki](https://translatewiki.net/). + +If you require translations in [plugins](https://static.etherpad.org/) please +send pull request to each plugin individually. + +## FAQ -# FAQ Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**. -# License +## 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 +maintainers (a.mux@inwind.it and contact@etherpad.org). + +Join the official [Etherpad Discord +Channel](https://discord.com/invite/daEjfhw). + +## License + [Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 50dfff0db..5998c247e 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -56,26 +56,68 @@ Called from: `src/static/js/pluginfw/plugins.js` Run during startup after the named plugin is initialized. -Context properties: None +Context properties: -## expressConfigure -Called from: src/node/hooks/express.js + * `logger`: An object with the following `console`-like methods: `debug`, + `info`, `log`, `warn`, `error`. -Things in context: +## `expressPreSession` -1. app - the main application object +Called from: `src/node/hooks/express.js` -This is a helpful hook for changing the behavior and configuration of the application. It's called right after the application gets configured. +Called during server startup just before the +[`express-session`](https://www.npmjs.com/package/express-session) middleware is +added to the Express Application object. Use this hook to add route handlers or +middleware that executes before `express-session` state is created and +authentication is performed. This is useful for creating public endpoints that +don't spam the database with new `express-session` records or trigger +authentication. -## expressCreateServer -Called from: src/node/hooks/express.js +**WARNING:** All handlers registered during this hook run before the built-in +authentication checks, so any handled endpoints will be public unless the +handler itself authenticates the user. -Things in context: +Context properties: -1. app - the main express application object (helpful for adding new paths and such) -2. server - the http server object +* `app`: The Express [Application](https://expressjs.com/en/4x/api.html#app) + object. -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. +Example: + +```javascript +exports.expressPreSession = async (hookName, {app}) => { + app.get('/hello-world', (req, res) => res.send('hello world')); +}; +``` + +## `expressConfigure` + +Called from: `src/node/hooks/express.js` + +Called during server startup just after the +[`express-session`](https://www.npmjs.com/package/express-session) middleware is +added to the Express Application object. Use this hook to add route handlers or +middleware that executes after `express-session` state is created and +authentication is performed. + +Context properties: + +* `app`: The Express [Application](https://expressjs.com/en/4x/api.html#app) + object. + +## `expressCreateServer` + +Called from: `src/node/hooks/express.js` + +Identical to the `expressConfigure` hook (the two run in parallel with each +other) except this hook's context includes the HTTP Server object. + +Context properties: + +* `app`: The Express [Application](https://expressjs.com/en/4x/api.html#app) + object. +* `server`: The [http.Server](https://nodejs.org/api/http.html#class-httpserver) + or [https.Server](https://nodejs.org/api/https.html#class-httpsserver) object. ## expressCloseServer @@ -165,15 +207,98 @@ Things in context: This hook gets called when the access to the concrete pad is being checked. Return `false` to deny access. -## padCreate -Called from: src/node/db/Pad.js +## `getAuthorId` -Things in context: +Called from `src/node/db/AuthorManager.js` -1. pad - the pad instance -2. author - the id of the author who created the pad +Called when looking up (or creating) the author ID for a user, except for author +IDs obtained via the HTTP API. Registered hook functions are called until one +returns a non-`undefined` value. If a truthy value is returned by a hook +function, it is used as the user's author ID. Otherwise, the value of the +`dbKey` context property is used to look up the author ID. If there is no such +author ID at that key, a new author ID is generated and associated with that +key. -This hook gets called when a new pad was created. +Context properties: + +* `dbKey`: Database key to use when looking up the user's author ID if no hook + function returns an author ID. This is initialized to the user-supplied token + value (see the `token` context property), but hook functions can modify this + to control how author IDs are allocated to users. If no author ID is + associated with this database key, a new author ID will be randomly generated + and associated with the key. For security reasons, if this is modified it + should be modified to not look like a valid token (see the `token` context + property) unless the plugin intentionally wants the user to be able to + impersonate another user. +* `token`: The user-supplied token, or nullish for an anonymous user. Tokens are + secret values that must not be disclosed to others. If non-null, the token is + guaranteed to be a string with the form `t.` where `` is + any valid non-empty base64url string (RFC 4648 section 5 with padding). + Example: `t.twim3X2_KGiRj8cJ-3602g==`. +* `user`: If the user has authenticated, this is an object from `settings.users` + (or similar from an authentication plugin). Etherpad core and all good + authentication plugins set the `username` property of this object to a string + that uniquely identifies the authenticated user. This object is nullish if the + user has not authenticated. + +Example: + +```javascript +exports.getAuthorId = async (hookName, context) => { + const {username} = context.user || {}; + // If the user has not authenticated, or has "authenticated" as the guest + // user, do the default behavior (try another plugin if any, falling through + // to using the token as the database key). + if (!username || username === 'guest') return; + // The user is authenticated and has a username. Give the user a stable author + // ID so that they appear to be the same author even after clearing cookies or + // accessing the pad from another device. Note that this string is guaranteed + // to never have the form of a valid token; without that guarantee an + // unauthenticated user might be able to impersonate an authenticated user. + context.dbKey = `username=${username}`; + // Return a falsy but non-undefined value to stop Etherpad from calling any + // more getAuthorId hook functions and look up the author ID using the + // username-derived database key. + return ''; +}; +``` + +## `padCreate` + +Called from: `src/node/db/Pad.js` + +Called when a new pad is created. + +Context properties: + +* `pad`: The Pad object. +* `authorId`: The ID of the author who created the pad. +* `author` (**deprecated**): Synonym of `authorId`. + +## `padDefaultContent` + +Called from `src/node/db/Pad.js` + +Called to obtain a pad's initial content, unless the pad is being created with +specific content. The return value is ignored; to change the content, modify the +`content` context property. + +This hook is run asynchronously. All registered hook functions are run +concurrently (via `Promise.all()`), so be careful to avoid race conditions when +reading and modifying the context properties. + +Context properties: + +* `pad`: The newly created Pad object. +* `authorId`: The author ID of the user that is creating the pad. +* `type`: String identifying the content type. Currently this is `'text'` and + must not be changed. Future versions of Etherpad may add support for HTML, + jsdom objects, or other formats, so plugins must assert that this matches a + supported content type before reading `content`. +* `content`: The pad's initial content. Change this property to change the pad's + initial content. If the content type is changed, the `type` property must also + be updated to match. Plugins must check the value of the `type` property + before reading this value. ## `padLoad` @@ -185,44 +310,73 @@ Context properties: * `pad`: The Pad object. -## padUpdate -Called from: src/node/db/Pad.js +## `padUpdate` -Things in context: +Called from: `src/node/db/Pad.js` -1. pad - the pad instance -2. author - the id of the author who updated the pad -3. revs - the index of the new revision -4. changeset - the changeset of this revision (see [Changeset Library](#index_changeset_library)) +Called when an existing pad is updated. -This hook gets called when an existing pad was updated. +Context properties: -## padCopy -Called from: src/node/db/Pad.js +* `pad`: The Pad object. +* `authorId`: The ID of the author who updated the pad. +* `author` (**deprecated**): Synonym of `authorId`. +* `revs`: The index of the new revision. +* `changeset`: The changeset of this revision (see [Changeset + Library](#index_changeset_library)). -Things in context: +## `padCopy` -1. originalPad - the source pad instance -2. destinationID - the id of the pad copied from originalPad +Called from: `src/node/db/Pad.js` -This hook gets called when an existing pad was copied. +Called when a pad is copied so that plugins can copy plugin-specific database +records or perform some other plugin-specific initialization. + +Order of events when a pad is copied: + + 1. Destination pad is deleted if it exists and overwrite is permitted. This + causes the `padRemove` hook to run. + 2. Pad-specific database records are copied in the database, except for + records with plugin-specific database keys. + 3. A new Pad object is created for the destination pad. This causes the + `padLoad` hook to run. + 4. This hook runs. + +Context properties: + + * `srcPad`: The source Pad object. + * `dstPad`: The destination Pad object. Usage examples: -* https://github.com/ether/ep_comments + * https://github.com/ether/ep_comments_page -## padRemove -Called from: src/node/db/Pad.js +## `padRemove` -Things in context: +Called from: `src/node/db/Pad.js` -1. padID +Called when an existing pad is removed/deleted. Plugins should use this to clean +up any plugin-specific pad records from the database. -This hook gets called when an existing pad was removed/deleted. +Context properties: + + * `pad`: Pad object for the pad that is being deleted. Usage examples: -* https://github.com/ether/ep_comments + * https://github.com/ether/ep_comments_page + +## `padCheck` + +Called from: `src/node/db/Pad.js` + +Called when a consistency check is run on a pad, after the core checks have +completed successfully. An exception should be thrown if the pad is faulty in +some way. + +Context properties: + + * `pad`: The Pad object that is being checked. ## socketio Called from: src/node/hooks/express/socketio.js @@ -235,47 +389,52 @@ Things in context: I have no idea what this is useful for, someone else will have to add this description. -## preAuthorize -Called from: src/node/hooks/express/webaccess.js +## `preAuthorize` -Things in context: +Called from: `src/node/hooks/express/webaccess.js` -1. req - the request object -2. res - the response object -3. next - bypass callback. If this is called instead of the normal callback then - all remaining access checks are skipped. +Called for each HTTP request before any authentication checks are performed. The +registered `preAuthorize` hook functions are called one at a time until one +explicitly grants or denies the request by returning `true` or `false`, +respectively. If none of the hook functions return anything, the access decision +is deferred to the normal authentication and authorization checks. -This hook is called for each HTTP request before any authentication checks are -performed. Example uses: +Example uses: * Always grant access to static content. * Process an OAuth callback. * Drop requests from IP addresses that have failed N authentication checks within the past X minutes. -A preAuthorize function is always called for each request unless a preAuthorize -function from another plugin (if any) has already explicitly granted or denied -the request. +Return values: -You can pass the following values to the provided callback: +* `undefined` (or `[]`) defers the access decision to the next registered + `preAuthorize` hook function, or to the normal authentication and + authorization checks if no more `preAuthorize` hook functions remain. +* `true` (or `[true]`) immediately grants access to the requested resource, + unless the request is for an `/admin` page in which case it is treated the + same as returning `undefined`. (This prevents buggy plugins from accidentally + granting admin access to the general public.) +* `false` (or `[false]`) immediately denies the request. The `preAuthnFailure` + hook will be called to handle the failure. -* `[]` defers the access decision to the normal authentication and authorization - checks (or to a preAuthorize function from another plugin, if one exists). -* `[true]` immediately grants access to the requested resource, unless the - request is for an `/admin` page in which case it is treated the same as `[]`. - (This prevents buggy plugins from accidentally granting admin access to the - general public.) -* `[false]` immediately denies the request. The preAuthnFailure hook will be - called to handle the failure. +Context properties: + +* `req`: The Express [Request](https://expressjs.com/en/4x/api.html#req) object. +* `res`: The Express [Response](https://expressjs.com/en/4x/api.html#res) + object. +* `next`: Callback to immediately hand off handling to the next Express + middleware/handler, or to the next matching route if `'route'` is passed as + the first argument. Do not call this unless you understand the consequences. Example: -``` -exports.preAuthorize = (hookName, context, cb) => { - if (ipAddressIsFirewalled(context.req)) return cb([false]); - if (requestIsForStaticContent(context.req)) return cb([true]); - if (requestIsForOAuthCallback(context.req)) return cb([true]); - return cb([]); +```javascript +exports.preAuthorize = async (hookName, {req}) => { + if (await ipAddressIsFirewalled(req)) return false; + if (requestIsForStaticContent(req)) return true; + if (requestIsForOAuthCallback(req)) return true; + // Defer the decision to the next step by returning undefined. }; ``` @@ -530,26 +689,29 @@ exports.authzFailure = (hookName, context, cb) => { }; ``` -## handleMessage -Called from: src/node/handler/PadMessageHandler.js +## `handleMessage` -Things in context: - -1. message - the message being handled -2. socket - the socket.io Socket object -3. client - **deprecated** synonym of socket +Called from: `src/node/handler/PadMessageHandler.js` This hook allows plugins to drop or modify incoming socket.io messages from -clients, before Etherpad processes them. +clients, before Etherpad processes them. If any hook function returns `null` +then the message will not be subject to further processing. -The handleMessage function must return a Promise. If the Promise resolves to -`null`, the message is dropped. Returning `callback(value)` will return a -Promise that is resolved to `value`. +Context properties: -Examples: +* `message`: The message being handled. +* `sessionInfo`: Object describing the socket.io session with the following + properties: + * `authorId`: The user's author ID. + * `padId`: The real (not read-only) ID of the pad. + * `readOnly`: Whether the client has read-only access (true) or read/write + access (false). +* `socket`: The socket.io Socket object. +* `client`: (**Deprecated**; use `socket` instead.) Synonym of `socket`. -``` -// Using an async function: +Example: + +```javascript exports.handleMessage = async (hookName, {message, socket}) => { if (message.type === 'USERINFO_UPDATE') { // Force the display name to the name associated with the account. @@ -557,51 +719,47 @@ exports.handleMessage = async (hookName, {message, socket}) => { if (user.name) message.data.userInfo.name = user.name; } }; - -// Using a regular function: -exports.handleMessage = (hookName, {message, socket}, callback) => { - if (message.type === 'USERINFO_UPDATE') { - // Force the display name to the name associated with the account. - const user = socket.client.request.session.user || {}; - if (user.name) message.data.userInfo.name = user.name; - } - return callback(); -}; ``` -## handleMessageSecurity -Called from: src/node/handler/PadMessageHandler.js +## `handleMessageSecurity` -Things in context: +Called from: `src/node/handler/PadMessageHandler.js` -1. message - the message being handled -2. socket - the socket.io Socket object -3. client - **deprecated** synonym of socket +Called for each incoming message from a client. Allows plugins to grant +temporary write access to a pad. -This hook allows plugins to grant temporary write access to a pad. It is called -for each incoming message from a client. If write access is granted, it applies -to the current message and all future messages from the same socket.io -connection until the next `CLIENT_READY` message. Read-only access is reset -**after** each `CLIENT_READY` message, so granting write access has no effect -for those message types. +Supported return values: -The handleMessageSecurity function must return a Promise. If the Promise -resolves to `true`, write access is granted as described above. Returning -`callback(value)` will return a Promise that is resolved to `value`. +* `undefined`: No change in access status. +* `'permitOnce'`: Override the user's read-only access for the current + `COLLABROOM` message only. Has no effect if the current message is not a + `COLLABROOM` message, or if the user already has write access to the pad. +* `true`: (**Deprecated**; return `'permitOnce'` instead.) Override the user's + read-only access for all `COLLABROOM` messages from the same socket.io + connection (including the current message, if applicable) until the client's + next `CLIENT_READY` message. Has no effect if the user already has write + access to the pad. Read-only access is reset **after** each `CLIENT_READY` + message, so returning `true` has no effect for `CLIENT_READY` messages. -Examples: +Context properties: -``` -// Using an async function: -exports.handleMessageSecurity = async (hookName, {message, socket}) => { - if (shouldGrantWriteAccess(message, socket)) return true; - return; -}; +* `message`: The message being handled. +* `sessionInfo`: Object describing the socket.io connection with the following + properties: + * `authorId`: The user's author ID. + * `padId`: The real (not read-only) ID of the pad. + * `readOnly`: Whether the client has read-only access (true) or read/write + access (false). +* `socket`: The socket.io Socket object. +* `client`: (**Deprecated**; use `socket` instead.) Synonym of `socket`. -// Using a regular function: -exports.handleMessageSecurity = (hookName, {message, socket}, callback) => { - if (shouldGrantWriteAccess(message, socket)) return callback(true); - return callback(); +Example: + +```javascript +exports.handleMessageSecurity = async (hookName, context) => { + const {message, sessionInfo: {readOnly}} = context; + if (!readOnly || message.type !== 'COLLABROOM') return; + if (await messageIsBenign(message)) return 'permitOnce'; }; ``` @@ -647,39 +805,36 @@ exports.clientVars = (hookName, context, callback) => { }; ``` -## getLineHTMLForExport -Called from: src/node/utils/ExportHtml.js +## `getLineHTMLForExport` -Things in context: +Called from: `src/node/utils/ExportHtml.js` -1. apool - pool object -2. attribLine - line attributes -3. text - line text +This hook will allow a plug-in developer to re-write each line when exporting to +HTML. -This hook will allow a plug-in developer to re-write each line when exporting to HTML. +Context properties: + +* `apool`: Pool object. +* `attribLine`: Line attributes. +* `line`: +* `lineContent`: +* `text`: Line text. +* `padId`: Writable (not read-only) pad identifier. Example: -``` -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -exports.getLineHTMLForExport = function (hook, context) { - var header = _analyzeLine(context.attribLine, context.apool); - if (header) { - return "<" + header + ">" + context.lineContent + ""; - } -} +```javascript +const AttributeMap = require('ep_etherpad-lite/static/js/AttributeMap'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -function _analyzeLine(alineAttrs, apool) { - var header = null; - if (alineAttrs) { - var opIter = Changeset.opIterator(alineAttrs); - if (opIter.hasNext()) { - var op = opIter.next(); - header = Changeset.opAttributeValue(op, 'heading', apool); - } - } - return header; -} +exports.getLineHTMLForExport = async (hookName, context) => { + if (!context.attribLine) return; + const [op] = Changeset.deserializeOps(context.attribLine); + if (op == null) return; + const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading'); + if (!heading) return; + context.lineContent = `<${heading}>${context.lineContent}`; +}; ``` ## exportHTMLAdditionalContent @@ -790,17 +945,19 @@ exports.exportHtmlAdditionalTagsWithData = function(hook, pad, cb){ }; ``` -## exportEtherpadAdditionalContent -Called from src/node/utils/ExportEtherpad.js and -src/node/utils/ImportEtherpad.js +## `exportEtherpadAdditionalContent` -Things in context: Nothing +Called from `src/node/utils/ExportEtherpad.js` and +`src/node/utils/ImportEtherpad.js`. -Useful for exporting and importing pad metadata that is stored in the database -but not in the pad's content or attributes. For example, in ep_comments_page the -comments are stored as `comments:padId:uniqueIdOfComment` so a complete export -of all pad data to an `.etherpad` file must include the `comments:padId:*` -records. +Called when exporting to an `.etherpad` file or when importing from an +`.etherpad` file. The hook function should return prefixes for pad-specific +records that should be included in the export/import. On export, all +`${prefix}:${padId}` and `${prefix}:${padId}:*` records are included in the +generated `.etherpad` file. On import, all `${prefix}:${padId}` and +`${prefix}:${padId}:*` records are loaded into the database. + +Context properties: None. Example: @@ -809,6 +966,48 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` +## `exportEtherpad` + +Called from `src/node/utils/ExportEtherpad.js`. + +Called when exporting to an `.etherpad` file. + +Context properties: + + * `pad`: The exported pad's Pad object. + * `data`: JSONable output object. This is pre-populated with records from core + Etherpad as well as pad-specific records with prefixes from the + `exportEtherpadAdditionalContent` hook. Registered hook functions can modify + this object (but not replace the object) to perform any desired + transformations to the exported data (such as the inclusion of + plugin-specific records). All registered hook functions are executed + concurrently, so care should be taken to avoid race conditions with other + plugins. + * `dstPadId`: The pad ID that should be used when writing pad-specific records + to `data` (instead of `pad.id`). This avoids leaking the writable pad ID + when a user exports a read-only pad. This might be a dummy value; plugins + should not assume that it is either the pad's real writable ID or its + read-only ID. + +## `importEtherpad` + +Called from `src/node/utils/ImportEtherpad.js`. + +Called when importing from an `.etherpad` file. + +Context properties: + + * `pad`: Temporary Pad object containing the pad's data read from the imported + `.etherpad` file. The `pad.db` object is a temporary in-memory database + whose records will be copied to the real database after they are validated + (see the `padCheck` hook). Registered hook functions MUST NOT use the real + database to access (read or write) pad-specific records; they MUST instead + use `pad.db`. All registered hook functions are executed concurrently, so + care should be taken to avoid race conditions with other plugins. + * `data`: Raw JSONable object from the `.etherpad` file. This data must not be + modified. + * `srcPadId`: The pad ID used for the pad-specific information in `data`. + ## `import` Called from: `src/node/handler/ImportHandler.js` @@ -824,6 +1023,19 @@ Context properties: period** (examples: `'.docx'`, `'.html'`, `'.etherpad'`). * `padId`: The identifier of the destination pad. * `srcFile`: The document to convert. +* `ImportError`: Subclass of Error that can be thrown to provide a specific + error message to the user. The constructor's first argument must be a string + matching one of the [known error + identifiers](https://github.com/ether/etherpad-lite/blob/1.8.16/src/static/js/pad_impexp.js#L80-L86). + +Example: + +```javascript +exports.import = async (hookName, {fileEnding, ImportError}) => { + // Reject all *.etherpad imports with a permission denied message. + if (fileEnding === '.etherpad') throw new ImportError('permission'); +}; +``` ## `userJoin` diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 9cab7f56b..63bd072b9 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -65,7 +65,7 @@ Portal submits content into new blog post ## Usage ### API version -The latest version is `1.2.14` +The latest version is `1.2.15` The current version can be queried via /api. @@ -173,14 +173,15 @@ returns all pads of this group * `{code: 0, message:"ok", data: {padIDs : ["g.s8oes9dhwrvt0zif$test", "g.s8oes9dhwrvt0zif$test2"]}` * `{code: 1, message:"groupID does not exist", data: null}` -#### createGroupPad(groupID, padName [, text]) +#### createGroupPad(groupID, padName, [text], [authorId]) * API >= 1 + * `authorId` in API >= 1.3.0 creates a new pad in this group *Example returns:* - * `{code: 0, message:"ok", data: null}` - * `{code: 1, message:"pad does already exist", data: null}` + * `{code: 0, message:"ok", data: {padID: "g.s8oes9dhwrvt0zif$test"}` + * `{code: 1, message:"padName does already exist", data: null}` * `{code: 1, message:"groupID does not exist", data: null}` #### listAllGroups() @@ -293,8 +294,9 @@ returns the text of a pad * `{code: 0, message:"ok", data: {text:"Welcome Text"}}` * `{code: 1, message:"padID does not exist", data: null}` -#### setText(padID, text) +#### setText(padID, text, [authorId]) * API >= 1 + * `authorId` in API >= 1.3.0 Sets the text of a pad. @@ -305,8 +307,9 @@ If your text is long (>8 KB), please invoke via POST and include `text` paramete * `{code: 1, message:"padID does not exist", data: null}` * `{code: 1, message:"text too long", data: null}` -#### appendText(padID, text) +#### appendText(padID, text, [authorId]) * API >= 1.2.13 + * `authorId` in API >= 1.3.0 Appends text to a pad. @@ -326,8 +329,9 @@ returns the text of a pad formatted as HTML * `{code: 0, message:"ok", data: {html:"Welcome Text
More Text"}}` * `{code: 1, message:"padID does not exist", data: null}` -#### setHTML(padID, html) +#### setHTML(padID, html, [authorId]) * API >= 1 + * `authorId` in API >= 1.3.0 sets the text of a pad based on HTML, HTML must be well-formed. Malformed HTML will send a warning to the API log. @@ -387,8 +391,9 @@ returns an object of diffs from 2 points in a pad * `{"code":0,"message":"ok","data":{"html":"Welcome to Etherpad!

This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!

Get involved with Etherpad at http://etherpad.org
aw

","authors":["a.HKIv23mEbachFYfH",""]}}` * `{"code":4,"message":"no or wrong API Key","data":null}` -#### restoreRevision(padId, rev) +#### restoreRevision(padId, rev, [authorId]) * API >= 1.2.11 + * `authorId` in API >= 1.3.0 Restores revision from past as new changeset @@ -437,8 +442,9 @@ creates a chat message, saves it to the database and sends it to all connected c ### Pad Group pads are normal pads, but with the name schema GROUPID$PADNAME. A security manager controls access of them and it's forbidden for normal pads to include a $ in the name. -#### createPad(padID [, text]) +#### createPad(padID, [text], [authorId]) * API >= 1 + * `authorId` in API >= 1.3.0 creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**. You get an error message if you use one of the following characters in the padID: "/", "?", "&" or "#". @@ -519,8 +525,9 @@ copies a pad with full history and chat. If force is true and the destination pa * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"padID does not exist", data: null}` -#### copyPadWithoutHistory(sourceID, destinationID[, force=false]) +#### copyPadWithoutHistory(sourceID, destinationID, [force=false], [authorId]) * API >= 1.2.15 + * `authorId` in API >= 1.3.0 copies a pad without copying the history and chat. If force is true and the destination pad exists, it will be overwritten. Note that all the revisions will be lost! In most of the cases one should use `copyPad` API instead. @@ -559,11 +566,12 @@ returns the id of a pad which is assigned to the readOnlyID #### setPublicStatus(padID, publicStatus) * API >= 1 -sets a boolean for the public status of a pad +sets a boolean for the public status of a group pad *Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"padID does not exist", data: null}` + * `{code: 1, message:"You can only get/set the publicStatus of pads that belong to a group", data: null}` #### getPublicStatus(padID) * API >= 1 @@ -573,6 +581,7 @@ return true of false *Example returns:* * `{code: 0, message:"ok", data: {publicStatus: true}}` * `{code: 1, message:"padID does not exist", data: null}` + * `{code: 1, message:"You can only get/set the publicStatus of pads that belong to a group", data: null}` #### listAuthorsOfPad(padID) * API >= 1 diff --git a/doc/docker.md b/doc/docker.md index 91caab97e..f72c4dd66 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -116,12 +116,12 @@ If your database needs additional settings, you will have to use a personalized | `PAD_OPTIONS_SHOW_CHAT` | | `true` | | `PAD_OPTIONS_SHOW_LINE_NUMBERS` | | `true` | | `PAD_OPTIONS_USE_MONOSPACE_FONT` | | `false` | -| `PAD_OPTIONS_USER_NAME` | | `false` | -| `PAD_OPTIONS_USER_COLOR` | | `false` | +| `PAD_OPTIONS_USER_NAME` | | `null` | +| `PAD_OPTIONS_USER_COLOR` | | `null` | | `PAD_OPTIONS_RTL` | | `false` | | `PAD_OPTIONS_ALWAYS_SHOW_CHAT` | | `false` | | `PAD_OPTIONS_CHAT_AND_USERS` | | `false` | -| `PAD_OPTIONS_LANG` | | `en-gb` | +| `PAD_OPTIONS_LANG` | | `null` | ### Shortcuts @@ -185,36 +185,39 @@ For the editor container, you can also make it full width by adding `full-width- ### Advanced -| Variable | Description | Default | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | -| `SHOW_SETTINGS_IN_ADMIN_PAGE` | hide/show the settings.json in admin page | `true` | -| `TRUST_PROXY` | set to `true` if you are using a reverse proxy in front of Etherpad (for example: Traefik for SSL termination via Let's Encrypt). This will affect security and correctness of the logs if not done | `false` | -| `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) | -| `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` | -| `IMPORT_EXPORT_RATE_LIMIT_WINDOW` | the call rate for import/export requests will be estimated in this time window (in milliseconds) | `90000` | -| `COMMIT_RATE_LIMIT_DURATION` | duration of the rate limit window for commits by individual users/IPs (in seconds) | `1` | -| `COMMIT_RATE_LIMIT_POINTS` | maximum number of changes per IP to allow during the rate limit window | `10` | -| `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` | -| `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` | -| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` | -| `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` | -| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) | -| `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` | -| `SOFFICE` | This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting. | `null` | -| `TIDY_HTML` | Path to the Tidy executable. Tidy is used to improve the quality of exported pads. Setting it to null disables Tidy. | `null` | -| `ALLOW_UNKNOWN_FILE_ENDS` | Allow import of file types other than the supported ones: txt, doc, docx, rtf, odt, html & htm | `true` | -| `REQUIRE_AUTHENTICATION` | This setting is used if you require authentication of all users. Note: "/admin" always requires authentication. | `false` | -| `REQUIRE_AUTHORIZATION` | Require authorization by a module, or a user with is_admin set, see below. | `false` | -| `AUTOMATIC_RECONNECTION_TIMEOUT` | Time (in seconds) to automatically reconnect pad when a "Force reconnect" message is shown to user. Set to 0 to disable automatic reconnection. | `0` | -| `FOCUS_LINE_PERCENTAGE_ABOVE` | Percentage of viewport height to be additionally scrolled. e.g. 0.5, to place caret line in the middle of viewport, when user edits a line above of the viewport. Set to 0 to disable extra scrolling | `0` | -| `FOCUS_LINE_PERCENTAGE_BELOW` | Percentage of viewport height to be additionally scrolled. e.g. 0.5, to place caret line in the middle of viewport, when user edits a line below of the viewport. Set to 0 to disable extra scrolling | `0` | -| `FOCUS_LINE_PERCENTAGE_ARROW_UP` | Percentage of viewport height to be additionally scrolled when user presses arrow up in the line of the top of the viewport. Set to 0 to let the scroll to be handled as default by Etherpad | `0` | -| `FOCUS_LINE_DURATION` | Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation | `0` | -| `FOCUS_LINE_CARET_SCROLL` | Flag to control if it should scroll when user places the caret in the last line of the viewport | `false` | -| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. | `10000` | -| `LOAD_TEST` | Allow Load Testing tools to hit the Etherpad Instance. WARNING: this will disable security on the instance. | `false` | -| `DUMP_ON_UNCLEAN_EXIT` | Enable dumping objects preventing a clean exit of Node.js. WARNING: this has a significant performance impact. | `false` | -| `EXPOSE_VERSION` | Expose Etherpad version in the web interface and in the Server http header. Do not enable on production machines. | `false` | +| Variable | Description | Default | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | +| `COOKIE_SAME_SITE` | Value of the SameSite cookie property. | `"Lax"` | +| `COOKIE_SESSION_LIFETIME` | How long (ms) a user can be away before they must log in again. | `864000000` (10 days) | +| `COOKIE_SESSION_REFRESH_INTERVAL` | How often (ms) to write the latest cookie expiration time. | `86400000` (1 day) | +| `SHOW_SETTINGS_IN_ADMIN_PAGE` | hide/show the settings.json in admin page | `true` | +| `TRUST_PROXY` | set to `true` if you are using a reverse proxy in front of Etherpad (for example: Traefik for SSL termination via Let's Encrypt). This will affect security and correctness of the logs if not done | `false` | +| `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) | +| `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` | +| `IMPORT_EXPORT_RATE_LIMIT_WINDOW` | the call rate for import/export requests will be estimated in this time window (in milliseconds) | `90000` | +| `COMMIT_RATE_LIMIT_DURATION` | duration of the rate limit window for commits by individual users/IPs (in seconds) | `1` | +| `COMMIT_RATE_LIMIT_POINTS` | maximum number of changes per IP to allow during the rate limit window | `10` | +| `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` | +| `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` | +| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` | +| `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` | +| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) | +| `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` | +| `SOFFICE` | This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting. | `null` | +| `TIDY_HTML` | Path to the Tidy executable. Tidy is used to improve the quality of exported pads. Setting it to null disables Tidy. | `null` | +| `ALLOW_UNKNOWN_FILE_ENDS` | Allow import of file types other than the supported ones: txt, doc, docx, rtf, odt, html & htm | `true` | +| `REQUIRE_AUTHENTICATION` | This setting is used if you require authentication of all users. Note: "/admin" always requires authentication. | `false` | +| `REQUIRE_AUTHORIZATION` | Require authorization by a module, or a user with is_admin set, see below. | `false` | +| `AUTOMATIC_RECONNECTION_TIMEOUT` | Time (in seconds) to automatically reconnect pad when a "Force reconnect" message is shown to user. Set to 0 to disable automatic reconnection. | `0` | +| `FOCUS_LINE_PERCENTAGE_ABOVE` | Percentage of viewport height to be additionally scrolled. e.g. 0.5, to place caret line in the middle of viewport, when user edits a line above of the viewport. Set to 0 to disable extra scrolling | `0` | +| `FOCUS_LINE_PERCENTAGE_BELOW` | Percentage of viewport height to be additionally scrolled. e.g. 0.5, to place caret line in the middle of viewport, when user edits a line below of the viewport. Set to 0 to disable extra scrolling | `0` | +| `FOCUS_LINE_PERCENTAGE_ARROW_UP` | Percentage of viewport height to be additionally scrolled when user presses arrow up in the line of the top of the viewport. Set to 0 to let the scroll to be handled as default by Etherpad | `0` | +| `FOCUS_LINE_DURATION` | Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation | `0` | +| `FOCUS_LINE_CARET_SCROLL` | Flag to control if it should scroll when user places the caret in the last line of the viewport | `false` | +| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. | `10000` | +| `LOAD_TEST` | Allow Load Testing tools to hit the Etherpad Instance. WARNING: this will disable security on the instance. | `false` | +| `DUMP_ON_UNCLEAN_EXIT` | Enable dumping objects preventing a clean exit of Node.js. WARNING: this has a significant performance impact. | `false` | +| `EXPOSE_VERSION` | Expose Etherpad version in the web interface and in the Server http header. Do not enable on production machines. | `false` | ### Examples diff --git a/doc/plugins.md b/doc/plugins.md index cc7c867d5..acfa069b8 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -225,7 +225,7 @@ publish your plugin. "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": {"node": ">=12.13.0"} + "engines": {"node": ">=12.17.0"} } ``` diff --git a/settings.json.docker b/settings.json.docker index ed1be901d..725af9f31 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -234,12 +234,12 @@ "showChat": "${PAD_OPTIONS_SHOW_CHAT:true}", "showLineNumbers": "${PAD_OPTIONS_SHOW_LINE_NUMBERS:true}", "useMonospaceFont": "${PAD_OPTIONS_USE_MONOSPACE_FONT:false}", - "userName": "${PAD_OPTIONS_USER_NAME:false}", - "userColor": "${PAD_OPTIONS_USER_COLOR:false}", + "userName": "${PAD_OPTIONS_USER_NAME:null}", + "userColor": "${PAD_OPTIONS_USER_COLOR:null}", "rtl": "${PAD_OPTIONS_RTL:false}", "alwaysShowChat": "${PAD_OPTIONS_ALWAYS_SHOW_CHAT:false}", "chatAndUsers": "${PAD_OPTIONS_CHAT_AND_USERS:false}", - "lang": "${PAD_OPTIONS_LANG:en-gb}" + "lang": "${PAD_OPTIONS_LANG:null}" }, /* @@ -374,7 +374,51 @@ * significant usability drawbacks vs. "Lax". See * https://stackoverflow.com/q/41841880 for discussion. */ - "sameSite": "${COOKIE_SAME_SITE:Lax}" + "sameSite": "${COOKIE_SAME_SITE:Lax}", + + /* + * How long (in milliseconds) after navigating away from Etherpad before the + * user is required to log in again. (The express_sid cookie is set to + * expire at time now + sessionLifetime when first created, and its + * expiration time is periodically refreshed to a new now + sessionLifetime + * value.) If requireAuthentication is false then this value does not really + * matter. + * + * The "best" value depends on your users' usage patterns and the amount of + * convenience you desire. A long lifetime is more convenient (users won't + * have to log back in as often) but has some drawbacks: + * - It increases the amount of state kept in the database. + * - It might weaken security somewhat: The cookie expiration is refreshed + * indefinitely without consulting authentication or authorization + * hooks, so once a user has accessed a pad, the user can continue to + * use the pad until the user leaves for longer than sessionLifetime. + * + * Session lifetime can be set to infinity (not recommended) by setting this + * to null or 0. Note that if the session does not expire, most browsers + * will delete the cookie when the browser exits, but a session record is + * kept in the database forever. + */ + // 864000000 = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s + "sessionLifetime": "${COOKIE_SESSION_LIFETIME:864000000}", + + /* + * How long (in milliseconds) before the expiration time of an active user's + * session is refreshed (to now + sessionLifetime). This setting affects the + * following: + * - How often a new session expiration time will be written to the + * database. + * - How often each user's browser will ping the Etherpad server to + * refresh the expiration time of the session cookie. + * + * High values reduce the load on the database and the load from browsers, + * but can shorten the effective session lifetime if Etherpad is restarted + * or the user navigates away. + * + * Automatic session refreshes can be disabled (not recommended) by setting + * this to null. + */ + // 86400000 = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s + "sessionRefreshInterval": "${COOKIE_SESSION_REFRESH_INTERVAL:86400000}" }, /* @@ -587,5 +631,8 @@ "loglevel": "${LOGLEVEL:INFO}", /* Override any strings found in locale directories */ - "customLocaleStrings": {} + "customLocaleStrings": {}, + + /* Disable Admin UI tests */ + "enableAdminUITests": false } diff --git a/settings.json.template b/settings.json.template index 8b8766be8..b2cb9555a 100644 --- a/settings.json.template +++ b/settings.json.template @@ -235,12 +235,12 @@ "showChat": true, "showLineNumbers": true, "useMonospaceFont": false, - "userName": false, - "userColor": false, + "userName": null, + "userColor": null, "rtl": false, "alwaysShowChat": false, "chatAndUsers": false, - "lang": "en-gb" + "lang": null }, /* @@ -375,7 +375,49 @@ * significant usability drawbacks vs. "Lax". See * https://stackoverflow.com/q/41841880 for discussion. */ - "sameSite": "Lax" + "sameSite": "Lax", + + /* + * How long (in milliseconds) after navigating away from Etherpad before the + * user is required to log in again. (The express_sid cookie is set to + * expire at time now + sessionLifetime when first created, and its + * expiration time is periodically refreshed to a new now + sessionLifetime + * value.) If requireAuthentication is false then this value does not really + * matter. + * + * The "best" value depends on your users' usage patterns and the amount of + * convenience you desire. A long lifetime is more convenient (users won't + * have to log back in as often) but has some drawbacks: + * - It increases the amount of state kept in the database. + * - It might weaken security somewhat: The cookie expiration is refreshed + * indefinitely without consulting authentication or authorization + * hooks, so once a user has accessed a pad, the user can continue to + * use the pad until the user leaves for longer than sessionLifetime. + * + * Session lifetime can be set to infinity (not recommended) by setting this + * to null or 0. Note that if the session does not expire, most browsers + * will delete the cookie when the browser exits, but a session record is + * kept in the database forever. + */ + "sessionLifetime": 864000000, // = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s + + /* + * How long (in milliseconds) before the expiration time of an active user's + * session is refreshed (to now + sessionLifetime). This setting affects the + * following: + * - How often a new session expiration time will be written to the + * database. + * - How often each user's browser will ping the Etherpad server to + * refresh the expiration time of the session cookie. + * + * High values reduce the load on the database and the load from browsers, + * but can shorten the effective session lifetime if Etherpad is restarted + * or the user navigates away. + * + * Automatic session refreshes can be disabled (not recommended) by setting + * this to null. + */ + "sessionRefreshInterval": 86400000 // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s }, /* @@ -550,7 +592,6 @@ "points": 10 }, - /* * Toolbar buttons configuration. * diff --git a/src/.eslintrc.cjs b/src/.eslintrc.cjs new file mode 100644 index 000000000..95c9efa07 --- /dev/null +++ b/src/.eslintrc.cjs @@ -0,0 +1,141 @@ +'use strict'; + +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('eslint-config-etherpad/patch/modern-module-resolution'); + +module.exports = { + ignorePatterns: [ + '/static/js/admin/jquery.autosize.js', + '/static/js/admin/minify.json.js', + '/static/js/vendors/browser.js', + '/static/js/vendors/farbtastic.js', + '/static/js/vendors/gritter.js', + '/static/js/vendors/html10n.js', + '/static/js/vendors/jquery.js', + '/static/js/vendors/nice-select.js', + '/tests/frontend/lib/', + ], + overrides: [ + { + files: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/node', + }, + { + files: [ + '**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/**/*', + ], + extends: 'etherpad/node', + }, + { + files: [ + 'static/**/*', + 'tests/frontend/helper.js', + 'tests/frontend/helper/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/browser', + env: { + 'shared-node-browser': true, + }, + overrides: [ + { + files: [ + 'tests/frontend/helper/**/*', + ], + globals: { + helper: 'readonly', + }, + }, + ], + }, + { + files: [ + 'tests/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/cypress/**/*', + 'tests/frontend/helper.js', + 'tests/frontend/helper/**/*', + 'tests/frontend/travis/**/*', + 'tests/ratelimit/**/*', + ], + extends: 'etherpad/tests', + rules: { + 'mocha/no-exports': 'off', + 'mocha/no-top-level-hooks': 'off', + }, + }, + { + files: [ + 'tests/backend/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/tests/backend', + overrides: [ + { + files: [ + 'tests/backend/**/*', + ], + excludedFiles: [ + 'tests/backend/specs/**/*', + ], + rules: { + 'mocha/no-exports': 'off', + 'mocha/no-top-level-hooks': 'off', + }, + }, + ], + }, + { + files: [ + 'tests/frontend/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/cypress/**/*', + 'tests/frontend/helper.js', + 'tests/frontend/helper/**/*', + 'tests/frontend/travis/**/*', + ], + extends: 'etherpad/tests/frontend', + overrides: [ + { + files: [ + 'tests/frontend/**/*', + ], + excludedFiles: [ + 'tests/frontend/specs/**/*', + ], + rules: { + 'mocha/no-exports': 'off', + 'mocha/no-top-level-hooks': 'off', + }, + }, + ], + }, + { + files: [ + 'tests/frontend/cypress/**/*', + ], + extends: 'etherpad/tests/cypress', + }, + { + files: [ + 'tests/frontend/travis/**/*', + ], + extends: 'etherpad/node', + }, + ], + root: true, +}; diff --git a/src/bin/buildForWindows.sh b/src/bin/buildForWindows.sh index 5fd2a4bde..3d04fff72 100755 --- a/src/bin/buildForWindows.sh +++ b/src/bin/buildForWindows.sh @@ -1,62 +1,57 @@ #!/bin/sh +set -e + pecho() { printf %s\\n "$*"; } log() { pecho "$@"; } error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } +try() { "$@" || fatal "'$@' failed"; } is_cmd() { command -v "$@" >/dev/null 2>&1; } -# Move to the folder where ep-lite is installed -cd "$(cd "${0%/*}" && pwd -P)/../.." +for x in git unzip wget zip; do + is_cmd "${x}" || fatal "Please install ${x}" +done -# Is wget installed? -is_cmd wget || fatal "Please install wget" +# Move to the folder where Etherpad is checked out +try cd "${0%/*}" +workdir=$(try git rev-parse --show-toplevel) || exit 1 +try cd "${workdir}" +[ -f src/package.json ] || fatal "failed to cd to etherpad root directory" -# Is zip installed? -is_cmd zip || fatal "Please install zip" +OUTPUT=${workdir}/etherpad-win.zip -# Is zip installed? -is_cmd unzip || fatal "Please install unzip" - -START_FOLDER=$(pwd); -TMP_FOLDER=$(mktemp -d) +TMP_FOLDER=$(try mktemp -d) || exit 1 +trap 'exit 1' HUP INT TERM +trap 'log "cleaning up..."; try cd / && try rm -rf "${TMP_FOLDER}"' EXIT log "create a clean environment in $TMP_FOLDER..." -cp -ar . "$TMP_FOLDER" -cd "$TMP_FOLDER" -rm -rf node_modules -rm -f etherpad-lite-win.zip +try git archive --format=tar HEAD | (try cd "${TMP_FOLDER}" && try tar xf -) \ + || fatal "failed to copy etherpad to temporary folder" +try mkdir "${TMP_FOLDER}"/.git +try git rev-parse HEAD >${TMP_FOLDER}/.git/HEAD +try cd "${TMP_FOLDER}" +[ -f src/package.json ] || fatal "failed to copy etherpad to temporary folder" # setting NODE_ENV=production ensures that dev dependencies are not installed, # making the windows package smaller export NODE_ENV=production log "do a normal unix install first..." -src/bin/installDeps.sh || exit 1 +try ./src/bin/installDeps.sh log "copy the windows settings template..." -cp settings.json.template settings.json +try cp settings.json.template settings.json log "resolve symbolic links..." -cp -rL node_modules node_modules_resolved -rm -rf node_modules -mv node_modules_resolved node_modules +try cp -rL node_modules node_modules_resolved +try rm -rf node_modules +try mv node_modules_resolved node_modules log "download windows node..." -wget "https://nodejs.org/dist/latest-erbium/win-x86/node.exe" -O node.exe - -log "remove git history to reduce folder size" -rm -rf .git/objects - -log "remove windows jsdom-nocontextify/test folder" -rm -rf "$TMP_FOLDER"/src/node_modules/wd/node_modules/request/node_modules/form-data/node_modules/combined-stream/test -rm -rf "$TMP_FOLDER"/src/node_modules/nodemailer/node_modules/mailcomposer/node_modules/mimelib/node_modules/encoding/node_modules/iconv-lite/encodings/tables +try wget "https://nodejs.org/dist/latest-v16.x/win-x64/node.exe" -O node.exe log "create the zip..." -cd "$TMP_FOLDER" -zip -9 -r "$START_FOLDER"/etherpad-lite-win.zip ./* -x var +try zip -9 -r "${OUTPUT}" ./* -log "clean up..." -rm -rf "$TMP_FOLDER" - -log "Finished. You can find the zip in the Etherpad root folder, it's called etherpad-lite-win.zip" +log "Finished. You can find the zip at ${OUTPUT}" diff --git a/src/bin/createRelease.sh b/src/bin/createRelease.sh index 531a21502..14e3cd337 100755 --- a/src/bin/createRelease.sh +++ b/src/bin/createRelease.sh @@ -145,12 +145,12 @@ function push_builds { cd $TMP_DIR/etherpad-lite/ echo "Copying windows build and docs to website repo..." GIT_SHA=$(git rev-parse HEAD | cut -c1-10) - mv etherpad-lite-win.zip $TMP_DIR/ether.github.com/downloads/etherpad-lite-win-$VERSION-$GIT_SHA.zip + mv etherpad-win.zip $TMP_DIR/ether.github.com/downloads/etherpad-win-$VERSION-$GIT_SHA.zip mv out/doc $TMP_DIR/ether.github.com/doc/v$VERSION cd $TMP_DIR/ether.github.com/ - sed -i "s/etherpad-lite-win.*\.zip/etherpad-lite-win-$VERSION-$GIT_SHA.zip/" index.html + sed -i "s/etherpad-win.*\.zip/etherpad-win-$VERSION-$GIT_SHA.zip/" index.html sed -i "s/$LATEST_GIT_TAG/$VERSION/g" index.html git checkout -b release_$VERSION [[ $? != 0 ]] && echo "Aborting: Error creating new release branch" && exit 1 diff --git a/src/bin/doc/package-lock.json b/src/bin/doc/package-lock.json index 40fe13e45..2ee477966 100644 --- a/src/bin/doc/package-lock.json +++ b/src/bin/doc/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==" + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==" } } } diff --git a/src/bin/doc/package.json b/src/bin/doc/package.json index c17020ea6..a674e5d96 100644 --- a/src/bin/doc/package.json +++ b/src/bin/doc/package.json @@ -4,10 +4,10 @@ "description": "Internal tool for generating Node.js API docs", "version": "0.0.0", "engines": { - "node": ">=12.13.0" + "node": ">=12.17.0" }, "dependencies": { - "marked": "^2.0.0" + "marked": "^4.2.12" }, "devDependencies": {}, "optionalDependencies": {}, diff --git a/src/bin/etherpad-healthcheck b/src/bin/etherpad-healthcheck new file mode 100755 index 000000000..59105d38a --- /dev/null +++ b/src/bin/etherpad-healthcheck @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +// Checks the health of Etherpad by visiting http://localhost:9001/health. Returns 0 on success, 1 +// on error as required by the Dockerfile HEALTHCHECK instruction. + +'use strict'; + +// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an +// unhandled rejection into an uncaught exception, which does cause Node.js to exit. +process.on('unhandledRejection', (err) => { throw err; }); + +const assert = require('assert').strict; +const superagent = require('superagent'); + +(async () => { + const res = await superagent.get('http://localhost:9001/health') + .accept('application/health+json') + .buffer(true) + .parse(superagent.parse['application/json']); + assert(res.ok, `Unexpected HTTP status: ${res.status}`); + assert.equal(res.type, 'application/health+json'); + const {body: {status} = {}} = res; + assert(status != null); + assert.equal(typeof status, 'string'); + assert(['pass', 'ok', 'up'].includes(status.toLowerCase()), `Unexpected status: ${status}`); +})(); diff --git a/src/bin/extractPadData.js b/src/bin/extractPadData.js index 353c5d21e..b0d0212d7 100644 --- a/src/bin/extractPadData.js +++ b/src/bin/extractPadData.js @@ -30,9 +30,7 @@ const padId = process.argv[2]; // initialize output database const dirty = dirtyDB(`${padId}.db`); - // Promise wrapped get and set function - const wrapped = db.db.db.wrappedDB; - const get = util.promisify(wrapped.get.bind(wrapped)); + // Promise set function const set = util.promisify(dirty.set.bind(dirty)); // array in which required key values will be accumulated @@ -55,7 +53,7 @@ const padId = process.argv[2]; } for (const dbkey of neededDBValues) { - let dbvalue = await get(dbkey); + let dbvalue = await db.get(dbkey); if (dbvalue && typeof dbvalue !== 'object') { dbvalue = JSON.parse(dbvalue); } diff --git a/src/bin/nsis/README.md b/src/bin/nsis/README.md new file mode 100644 index 000000000..21e2def95 --- /dev/null +++ b/src/bin/nsis/README.md @@ -0,0 +1,9 @@ +A simple NSIS script to Install Etherpad (Server) on Windows and start it. + +# TODO +1. i18n +1. Run as Service +1. Display messages during install + +# License +Apache 2 diff --git a/src/bin/nsis/brand.ico b/src/bin/nsis/brand.ico new file mode 100644 index 000000000..cb7d6a019 Binary files /dev/null and b/src/bin/nsis/brand.ico differ diff --git a/src/bin/nsis/etherpad.nsi b/src/bin/nsis/etherpad.nsi new file mode 100644 index 000000000..3140e6bb9 --- /dev/null +++ b/src/bin/nsis/etherpad.nsi @@ -0,0 +1,55 @@ +;Include Modern UI +!include "MUI2.nsh" +!include x64.nsh + +;-------------------------------- +;Styling +!define MUI_ICON "brand.ico" +Icon "brand.ico" +BrandingText "Etherpad Foundation" +Name "Etherpad Server" +OutFile "..\..\..\etherpad-win.exe" + +!insertmacro MUI_LANGUAGE "English" + +Page directory +Page instfiles + +; The default installation directory +InstallDir "$PROGRAMFILES64\Etherpad Foundation\Etherpad Server" + +Section + SectionIn RO + + ${If} ${RunningX64} + DetailPrint "Installer running on x64 host" + ${Else} + Abort "Unsupported CPU architecture (only x64 is supported)" + ${Endif} + + ; Set output path to the installation directory. + SetOutPath $INSTDIR + + ; Put files there + File /r "..\..\..\..\etherpad-zip\*" + +SectionEnd + +Section + CreateDirectory "$SMPROGRAMS\Etherpad Foundation" + CreateShortCut "$SMPROGRAMS\Etherpad Foundation\Etherpad Server.lnk" "$INSTDIR\start.bat" "brand.ico" "Etherpad Server" + CreateShortCut "$SMPROGRAMS\Etherpad Foundation\Etherpad.lnk" "http://127.0.0.1:9001" "brand.ico" "Etherpad" + CreateShortCut "$SMPROGRAMS\Etherpad Foundation\Etherpad Admin.lnk" "http://127.0.0.1:9001/admin" "brand.ico" "Etherpad Admin" + CreateShortCut "$SMPROGRAMS\Etherpad Foundation\Uninstall Etherpad Server.lnk" "$INSTDIR\uninstall.exe" + WriteUninstaller "$INSTDIR\uninstall.exe" + Exec '$INSTDIR\start.bat' +SectionEnd + +UninstPage instfiles + +Section Uninstall + Delete "$INSTDIR\*" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + SetAutoClose false +SectionEnd diff --git a/src/bin/plugins/README.md b/src/bin/plugins/README.md index b14065821..10602f987 100755 --- a/src/bin/plugins/README.md +++ b/src/bin/plugins/README.md @@ -21,12 +21,18 @@ node src/bin/plugins/checkPlugin.js ep_webrtc node src/bin/plugins/checkPlugin.js ep_whatever autofix ``` -## Autocommitting, push, npm minor patch and npm publish (highly dangerous) +## Autocommitting - fix issues and commit ``` node src/bin/plugins/checkPlugin.js ep_whatever autocommit ``` +## Autopush - fix issues, commit, push, and publish (highly dangerous) + +``` +node src/bin/plugins/checkPlugin.js ep_whatever autopush +``` + # All the plugins Replace johnmclear with your github username diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js index 4876c1362..8f557c3c5 100755 --- a/src/bin/plugins/checkPlugin.js +++ b/src/bin/plugins/checkPlugin.js @@ -5,423 +5,377 @@ * * Normal usage: node src/bin/plugins/checkPlugin.js ep_whatever * Auto fix the things it can: node src/bin/plugins/checkPlugin.js ep_whatever autofix - * Auto commit, push and publish to npm (highly dangerous): - * node src/bin/plugins/checkPlugin.js ep_whatever autocommit + * Auto fix and commit: node src/bin/plugins/checkPlugin.js ep_whatever autocommit + * Auto fix, commit, push and publish to npm (highly dangerous): + * node src/bin/plugins/checkPlugin.js ep_whatever autopush */ +const process = require('process'); + // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an // unhandled rejection into an uncaught exception, which does cause Node.js to exit. process.on('unhandledRejection', (err) => { throw err; }); +const assert = require('assert').strict; const fs = require('fs'); +const fsp = fs.promises; const childProcess = require('child_process'); +const log4js = require('log4js'); +const path = require('path'); -// get plugin name & path from user input -const pluginName = process.argv[2]; +const logger = log4js.getLogger('checkPlugin'); -if (!pluginName) throw new Error('no plugin name specified'); +(async () => { + // get plugin name & path from user input + const pluginName = process.argv[2]; -const pluginPath = `node_modules/${pluginName}`; + if (!pluginName) throw new Error('no plugin name specified'); + logger.info(`Checking the plugin: ${pluginName}`); -console.log(`Checking the plugin: ${pluginName}`); + const epRootDir = await fsp.realpath(path.join(await fsp.realpath(__dirname), '../../..')); + logger.info(`Etherpad root directory: ${epRootDir}`); + process.chdir(epRootDir); + const pluginPath = await fsp.realpath(`node_modules/${pluginName}`); + logger.info(`Plugin directory: ${pluginPath}`); + const epSrcDir = await fsp.realpath(path.join(epRootDir, 'src')); -const optArgs = process.argv.slice(3); -const autoCommit = optArgs.indexOf('autocommit') !== -1; -const autoFix = autoCommit || optArgs.indexOf('autofix') !== -1; + const optArgs = process.argv.slice(3); + const autoPush = optArgs.includes('autopush'); + const autoCommit = autoPush || optArgs.includes('autocommit'); + const autoFix = autoCommit || optArgs.includes('autofix'); -const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, { - cwd: `${pluginPath}/`, - ...opts, -}) || '').toString().replace(/\n+$/, ''); + const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, { + cwd: `${pluginPath}/`, + ...opts, + }) || '').toString().replace(/\n+$/, ''); -const writePackageJson = (obj) => { - let s = JSON.stringify(obj, null, 2); - if (s.length && s.slice(s.length - 1) !== '\n') s += '\n'; - return fs.writeFileSync(`${pluginPath}/package.json`, s); -}; + const writePackageJson = async (obj) => { + let s = JSON.stringify(obj, null, 2); + if (s.length && s.slice(s.length - 1) !== '\n') s += '\n'; + return await fsp.writeFile(`${pluginPath}/package.json`, s); + }; -const updateDeps = (parsedPackageJson, key, wantDeps) => { - const {[key]: deps = {}} = parsedPackageJson; - let changed = false; - for (const [pkg, verInfo] of Object.entries(wantDeps)) { - const {ver, overwrite = true} = typeof verInfo === 'string' ? {ver: verInfo} : verInfo; - if (deps[pkg] === ver) continue; - if (deps[pkg] == null) { - console.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`); - } else { - if (!overwrite) continue; - console.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`); + const checkEntries = (got, want) => { + let changed = false; + for (const [key, val] of Object.entries(want)) { + try { + assert.deepEqual(got[key], val); + } catch (err) { + logger.warn(`${key} possibly outdated.`); + logger.warn(err.message); + if (autoFix) { + got[key] = val; + changed = true; + } + } } - if (autoFix) { - deps[pkg] = ver; - changed = true; + return changed; + }; + + const updateDeps = async (parsedPackageJson, key, wantDeps) => { + const {[key]: deps = {}} = parsedPackageJson; + let changed = false; + for (const [pkg, verInfo] of Object.entries(wantDeps)) { + const {ver, overwrite = true} = + typeof verInfo === 'string' || verInfo == null ? {ver: verInfo} : verInfo; + if (deps[pkg] === ver || (deps[pkg] == null && ver == null)) continue; + if (deps[pkg] == null) { + logger.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`); + } else { + if (!overwrite) continue; + logger.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`); + } + if (autoFix) { + if (ver == null) delete deps[pkg]; + else deps[pkg] = ver; + changed = true; + } } - } - if (changed) { - parsedPackageJson[key] = deps; - writePackageJson(parsedPackageJson); - } -}; + if (changed) { + parsedPackageJson[key] = deps; + await writePackageJson(parsedPackageJson); + } + }; -const prepareRepo = () => { - let branch = execSync('git symbolic-ref HEAD'); - if (branch !== 'refs/heads/master' && branch !== 'refs/heads/main') { - throw new Error('master/main must be checked out'); - } - branch = branch.replace(/^refs\/heads\//, ''); - execSync('git rev-parse --verify -q HEAD^0 || ' + - `{ echo "Error: no commits on ${branch}" >&2; exit 1; }`); - execSync('git rev-parse --verify @{u}'); // Make sure there's a remote tracking branch. - const modified = execSync('git diff-files --name-status'); - if (modified !== '') throw new Error(`working directory has modifications:\n${modified}`); - const untracked = execSync('git ls-files -o --exclude-standard'); - if (untracked !== '') throw new Error(`working directory has untracked files:\n${untracked}`); - const indexStatus = execSync('git diff-index --cached --name-status HEAD'); - if (indexStatus !== '') throw new Error(`uncommitted staged changes to files:\n${indexStatus}`); - execSync('git pull --ff-only', {stdio: 'inherit'}); - if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits'); - if (autoCommit) { - execSync('git config --get user.name'); - execSync('git config --get user.email'); - } -}; + const prepareRepo = () => { + const modified = execSync('git diff-files --name-status'); + if (modified !== '') throw new Error(`working directory has modifications:\n${modified}`); + const untracked = execSync('git ls-files -o --exclude-standard'); + if (untracked !== '') throw new Error(`working directory has untracked files:\n${untracked}`); + const indexStatus = execSync('git diff-index --cached --name-status HEAD'); + if (indexStatus !== '') throw new Error(`uncommitted staged changes to files:\n${indexStatus}`); + let br; + if (autoCommit) { + br = execSync('git symbolic-ref HEAD'); + if (!br.startsWith('refs/heads/')) throw new Error('detached HEAD'); + br = br.replace(/^refs\/heads\//, ''); + execSync('git rev-parse --verify -q HEAD^0 || ' + + `{ echo "Error: no commits on ${br}" >&2; exit 1; }`); + execSync('git config --get user.name'); + execSync('git config --get user.email'); + } + if (autoPush) { + if (!['master', 'main'].includes(br)) throw new Error('master/main not checked out'); + execSync('git rev-parse --verify @{u}'); + execSync('git pull --ff-only', {stdio: 'inherit'}); + if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits'); + } + }; -if (autoCommit) { - console.warn('Auto commit is enabled, I hope you know what you are doing...'); -} + const checkFile = async (srcFn, dstFn, overwrite = true) => { + const outFn = path.join(pluginPath, dstFn); + const wantContents = await fsp.readFile(srcFn, {encoding: 'utf8'}); + let gotContents = null; + try { + gotContents = await fsp.readFile(outFn, {encoding: 'utf8'}); + } catch (err) { /* treat as if the file doesn't exist */ } + try { + assert.equal(gotContents, wantContents); + } catch (err) { + logger.warn(`File ${dstFn} does not match the default`); + logger.warn(err.message); + if (!overwrite && gotContents != null) { + logger.warn('Leaving existing contents alone.'); + return; + } + if (autoFix) { + await fsp.mkdir(path.dirname(outFn), {recursive: true}); + await fsp.writeFile(outFn, wantContents); + } + } + }; -fs.readdir(pluginPath, (err, rootFiles) => { - // handling error - if (err) { - return console.log(`Unable to scan directory: ${err}`); + if (autoPush) { + logger.warn('Auto push is enabled, I hope you know what you are doing...'); } - // rewriting files to lower case - const files = []; + const files = await fsp.readdir(pluginPath); // some files we need to know the actual file name. Not compulsory but might help in the future. - let readMeFileName; - let repository; + const readMeFileName = files.filter((f) => f === 'README' || f === 'README.md')[0]; - 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) throw new Error('No .git folder, aborting'); + if (!files.includes('.git')) throw new Error('No .git folder, aborting'); prepareRepo(); - try { - const path = `${pluginPath}/.github/workflows/npmpublish.yml`; - if (!fs.existsSync(path)) { - console.log('no .github/workflows/npmpublish.yml'); - console.log('create one and set npm secret to auto publish to npm on commit'); - if (autoFix) { - const npmpublish = - fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, npmpublish); - 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 workflows = ['backend-tests.yml', 'frontend-tests.yml', 'npmpublish.yml']; + await Promise.all(workflows.map(async (fn) => { + await checkFile(`src/bin/plugins/lib/${fn}`, `.github/workflows/${fn}`); + })); + await checkFile('src/bin/plugins/lib/dependabot.yml', '.github/dependabot.yml'); - const reqVersionFile = - fs.readFileSync('src/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('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, npmpublish); - } - } - } 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'); - console.log('create one and set npm secret to auto publish to npm on commit'); - if (autoFix) { - const backendTests = - fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, backendTests); - } - } 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('src/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('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'}); - fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true}); - fs.writeFileSync(path, backendTests); - } - } - } catch (err) { - console.error(err); - } - - if (files.indexOf('package.json') === -1) { - console.warn('no package.json, please create'); - } - - if (files.indexOf('package.json') !== -1) { + if (!files.includes('package.json')) { + logger.warn('no package.json, please create'); + } else { const packageJSON = - fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'}); + await fsp.readFile(`${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) { - writePackageJson(parsedPackageJSON); - } - } - if (packageJSON.toLowerCase().indexOf('repository') === -1) { - console.warn('No repository in package.json'); - if (autoFix) { - console.warn('Repository not detected in package.json. Add repository section.'); - } - } else { - // useful for creating README later. - repository = parsedPackageJSON.repository.url; - } - - updateDeps(parsedPackageJSON, 'devDependencies', { - 'eslint': '^7.28.0', - 'eslint-config-etherpad': '^2.0.0', - 'eslint-plugin-cypress': '^2.11.3', - 'eslint-plugin-eslint-comments': '^3.2.0', - 'eslint-plugin-mocha': '^9.0.0', - 'eslint-plugin-node': '^11.1.0', - 'eslint-plugin-prefer-arrow': '^1.2.3', - 'eslint-plugin-promise': '^5.1.0', - 'eslint-plugin-you-dont-need-lodash-underscore': '^6.12.0', + await updateDeps(parsedPackageJSON, 'devDependencies', { + 'eslint': '^8.14.0', + 'eslint-config-etherpad': '^3.0.13', + // Changing the TypeScript version can break plugin code, so leave it alone if present. + 'typescript': {ver: '^4.6.4', overwrite: false}, + // These were moved to eslint-config-etherpad's dependencies so they can be removed: + '@typescript-eslint/eslint-plugin': null, + '@typescript-eslint/parser': null, + 'eslint-import-resolver-typescript': null, + 'eslint-plugin-cypress': null, + 'eslint-plugin-eslint-comments': null, + 'eslint-plugin-import': null, + 'eslint-plugin-mocha': null, + 'eslint-plugin-node': null, + 'eslint-plugin-prefer-arrow': null, + 'eslint-plugin-promise': null, + 'eslint-plugin-you-dont-need-lodash-underscore': null, }); - updateDeps(parsedPackageJSON, 'peerDependencies', { + await updateDeps(parsedPackageJSON, 'peerDependencies', { // Some plugins require a newer version of Etherpad so don't overwrite if already set. 'ep_etherpad-lite': {ver: '>=1.8.6', overwrite: false}, }); - if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) { - console.warn('No esLintConfig in package.json'); - if (autoFix) { - const eslintConfig = { - root: true, - extends: 'etherpad/plugin', - }; - parsedPackageJSON.eslintConfig = eslintConfig; - writePackageJson(parsedPackageJSON); - } - } + await updateDeps(parsedPackageJSON, 'engines', { + node: '>=12.17.0', + }); - if (packageJSON.toLowerCase().indexOf('scripts') === -1) { - console.warn('No scripts in package.json'); - if (autoFix) { - const scripts = { - 'lint': 'eslint .', - 'lint:fix': 'eslint --fix .', - }; - parsedPackageJSON.scripts = scripts; - writePackageJson(parsedPackageJSON); - } + if (parsedPackageJSON.eslintConfig != null && autoFix) { + delete parsedPackageJSON.eslintConfig; + await writePackageJson(parsedPackageJSON); } - - if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) { - console.warn('No engines or node engine in package.json'); - if (autoFix) { - const engines = { - node: '>=12.13.0', - }; - parsedPackageJSON.engines = engines; - writePackageJson(parsedPackageJSON); - } - } - } - - if (files.indexOf('package-lock.json') === -1) { - console.warn('package-lock.json not found'); - if (!autoFix) { - console.warn('Run npm install in the plugin folder and commit the package-lock.json file.'); - } - } - 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'); - console.log('please edit the README.md file further to include plugin specific details.'); - let readme = fs.readFileSync('src/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); + if (files.includes('.eslintrc.js')) { + const [from, to] = [`${pluginPath}/.eslintrc.js`, `${pluginPath}/.eslintrc.cjs`]; + if (!files.includes('.eslintrc.cjs')) { + if (autoFix) { + await fsp.rename(from, to); + } else { + logger.warn(`please rename ${from} to ${to}`); + } } else { - console.warn('Unable to find repository in package.json, aborting.'); + logger.error(`both ${from} and ${to} exist; delete ${from}`); } + } else { + checkFile('src/bin/plugins/lib/eslintrc.cjs', '.eslintrc.cjs', false); + } + + if (checkEntries(parsedPackageJSON, { + funding: { + type: 'individual', + url: 'https://etherpad.org/', + }, + })) await writePackageJson(parsedPackageJSON); + + if (parsedPackageJSON.scripts == null) parsedPackageJSON.scripts = {}; + if (checkEntries(parsedPackageJSON.scripts, { + 'lint': 'eslint .', + 'lint:fix': 'eslint --fix .', + })) await writePackageJson(parsedPackageJSON); + } + + if (!files.includes('package-lock.json')) { + logger.warn('package-lock.json not found'); + if (!autoFix) { + logger.warn('Run npm install in the plugin folder and commit the package-lock.json file.'); } } - if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) { - console.warn('CONTRIBUTING.md file not found, please create'); + const fillTemplate = async (templateFilename, outputFilename) => { + const contents = (await fsp.readFile(templateFilename, 'utf8')) + .replace(/\[name of copyright owner\]/g, execSync('git config user.name')) + .replace(/\[plugin_name\]/g, pluginName) + .replace(/\[yyyy\]/g, new Date().getFullYear()); + await fsp.writeFile(outputFilename, contents); + }; + + if (!readMeFileName) { + logger.warn('README.md file not found, please create'); if (autoFix) { - console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' + + logger.info('Autofixing missing README.md file'); + logger.info('please edit the README.md file further to include plugin specific details.'); + await fillTemplate('src/bin/plugins/lib/README.md', `${pluginPath}/README.md`); + } + } + + if (!files.includes('CONTRIBUTING') && !files.includes('CONTRIBUTING.md')) { + logger.warn('CONTRIBUTING.md file not found, please create'); + if (autoFix) { + logger.info('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' + 'file further to include plugin specific details.'); - let contributing = - fs.readFileSync('src/bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'}); - contributing = contributing.replace(/\[plugin_name\]/g, pluginName); - fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing); + await fillTemplate('src/bin/plugins/lib/CONTRIBUTING.md', `${pluginPath}/CONTRIBUTING.md`); } } if (readMeFileName) { let readme = - fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'}); - if (readme.toLowerCase().indexOf('license') === -1) { - console.warn('No license section in README'); + await fsp.readFile(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'}); + if (!readme.toLowerCase().includes('license')) { + logger.warn('No license section in README'); if (autoFix) { - console.warn('Please add License section to README manually.'); + logger.warn('Please add License section to README manually.'); } } // eslint-disable-next-line max-len const publishBadge = `![Publish Status](https://github.com/ether/${pluginName}/workflows/Node.js%20Package/badge.svg)`; // eslint-disable-next-line max-len const testBadge = `![Backend Tests Status](https://github.com/ether/${pluginName}/workflows/Backend%20tests/badge.svg)`; - if (readme.toLowerCase().indexOf('travis') !== -1) { - console.warn('Remove Travis badges'); + if (readme.toLowerCase().includes('travis')) { + logger.warn('Remove Travis badges'); } - if (readme.indexOf('workflows/Node.js%20Package/badge.svg') === -1) { - console.warn('No Github workflow badge detected'); + if (!readme.includes('workflows/Node.js%20Package/badge.svg')) { + logger.warn('No Github workflow badge detected'); if (autoFix) { readme = `${publishBadge} ${testBadge}\n\n${readme}`; // write readme to file system - fs.writeFileSync(`${pluginPath}/${readMeFileName}`, readme); - console.log('Wrote Github workflow badges to README'); + await fsp.writeFile(`${pluginPath}/${readMeFileName}`, readme); + logger.info('Wrote Github workflow badges to README'); } } } - if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) { - console.warn('LICENSE.md file not found, please create'); + if (!files.includes('LICENSE') && !files.includes('LICENSE.md')) { + logger.warn('LICENSE file not found, please create'); if (autoFix) { - console.log('Autofixing missing LICENSE.md file, including Apache 2 license.'); - let license = - fs.readFileSync('src/bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'}); - license = license.replace('[yyyy]', new Date().getFullYear()); - license = license.replace('[name of copyright owner]', execSync('git config user.name')); - fs.writeFileSync(`${pluginPath}/LICENSE.md`, license); + logger.info('Autofixing missing LICENSE file (Apache 2.0).'); + await fsp.copyFile('src/bin/plugins/lib/LICENSE', `${pluginPath}/LICENSE`); } } - if (files.indexOf('.gitignore') === -1) { - console.warn('.gitignore file not found, please create. .gitignore files are useful to ' + + if (!files.includes('.gitignore')) { + logger.warn('.gitignore file not found, please create. .gitignore files are useful to ' + "ensure files aren't incorrectly commited to a repository."); if (autoFix) { - console.log('Autofixing missing .gitignore file'); + logger.info('Autofixing missing .gitignore file'); const gitignore = - fs.readFileSync('src/bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'}); - fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + await fsp.readFile('src/bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'}); + await fsp.writeFile(`${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'); + await fsp.readFile(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'}); + if (!gitignore.includes('node_modules/')) { + logger.warn('node_modules/ missing from .gitignore'); if (autoFix) { gitignore += 'node_modules/'; - fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore); + await fsp.writeFile(`${pluginPath}/.gitignore`, gitignore); } } } // 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. ' + + if (files.includes('templates') && !files.includes('locales')) { + logger.warn('Translations not found, please create. ' + 'Translation files help with Etherpad accessibility.'); } - if (files.indexOf('.ep_initialized') !== -1) { - console.warn( + if (files.includes('.ep_initialized')) { + logger.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) { - console.log('Autofixing incorrectly existing .ep_initialized file'); - fs.unlinkSync(`${pluginPath}/.ep_initialized`); + logger.info('Autofixing incorrectly existing .ep_initialized file'); + await fsp.unlink(`${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 ' + + if (files.includes('npm-debug.log')) { + logger.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to ' + 'your repository.'); if (autoFix) { - console.log('Autofixing incorrectly existing npm-debug.log file'); - fs.unlinkSync(`${pluginPath}/npm-debug.log`); + logger.info('Autofixing incorrectly existing npm-debug.log file'); + await fsp.unlink(`${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'); - } - }); + if (files.includes('static')) { + const staticFiles = await fsp.readdir(`${pluginPath}/static`); + if (!staticFiles.includes('tests')) { + logger.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'); + logger.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'); } // Install dependencies so we can run ESLint. This should also create or update package-lock.json // if autoFix is enabled. const npmInstall = `npm install${autoFix ? '' : ' --no-package-lock'}`; execSync(npmInstall, {stdio: 'inherit'}); - // The ep_etherpad-lite peer dep must be installed last otherwise `npm install` will nuke it. An - // absolute path to etherpad-lite/src is used here so that pluginPath can be a symlink. - execSync( - `${npmInstall} --no-save ep_etherpad-lite@file:${__dirname}/../../`, {stdio: 'inherit'}); + // Create the ep_etherpad-lite symlink if necessary. This must be done after running `npm install` + // because that command nukes the symlink. + try { + const d = await fsp.realpath(path.join(pluginPath, 'node_modules/ep_etherpad-lite')); + assert.equal(d, epSrcDir); + } catch (err) { + execSync(`${npmInstall} --no-save ep_etherpad-lite@file:${epSrcDir}`, {stdio: 'inherit'}); + } // linting begins try { - console.log('Linting...'); + logger.info('Linting...'); const lintCmd = autoFix ? 'npx eslint --fix .' : 'npx eslint'; execSync(lintCmd, {stdio: 'inherit'}); } catch (e) { // it is gonna throw an error anyway - console.log('Manual linting probably required, check with: npm run lint'); + logger.info('Manual linting probably required, check with: npm run lint'); } // linting ends. @@ -436,24 +390,31 @@ fs.readdir(pluginPath, (err, rootFiles) => { env: {...process.env, GIT_INDEX_FILE: '.git/checkPlugin.index'}, stdio: 'inherit', }); - fs.unlinkSync(`${pluginPath}/.git/checkPlugin.index`); + await fsp.unlink(`${pluginPath}/.git/checkPlugin.index`); - const cmd = [ + const commitCmd = [ 'git add -A', 'git commit -m "autofixes from Etherpad checkPlugin.js"', - 'git push', ].join(' && '); if (autoCommit) { - console.log('Attempting autocommit and auto publish to npm'); - execSync(cmd, {stdio: 'inherit'}); + logger.info('Committing changes...'); + execSync(commitCmd, {stdio: 'inherit'}); } else { - console.log('Fixes applied. Check the above git diff then run the following command:'); - console.log(`(cd node_modules/${pluginName} && ${cmd})`); + logger.info('Fixes applied. Check the above git diff then run the following command:'); + logger.info(`(cd node_modules/${pluginName} && ${commitCmd})`); + } + const pushCmd = 'git push'; + if (autoPush) { + logger.info('Pushing new commit...'); + execSync(pushCmd, {stdio: 'inherit'}); + } else { + logger.info('Changes committed. To push, run the following command:'); + logger.info(`(cd node_modules/${pluginName} && ${pushCmd})`); } } else { - console.log('No changes.'); + logger.info('No changes.'); } } - console.log('Finished'); -}); + logger.info('Finished'); +})(); diff --git a/src/bin/plugins/getCorePlugins.sh b/src/bin/plugins/getCorePlugins.sh index e8ce68b21..85552ab14 100755 --- a/src/bin/plugins/getCorePlugins.sh +++ b/src/bin/plugins/getCorePlugins.sh @@ -1,4 +1,39 @@ -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 +#!/bin/sh + +set -e + +newline=' +' + +pecho () { printf %s\\n "$*"; } +log () { pecho "$@"; } +error () { log "ERROR: $@" >&2; } +fatal () { error "$@"; exit 1; } + +mydir=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${mydir}/../../.." +pdir=$(cd .. && pwd -P) || exit 1 + +plugins=$("${mydir}/listOfficialPlugins") || exit 1 +for d in ${plugins}; do + log "============================================================" + log "${d}" + log "============================================================" + fd=${pdir}/${d} + repo=git@github.com:ether/${plugin}.git + [ -d "${fd}" ] || { + log "Cloning ${repo} to ${fd}..." + (cd "${pdir}" && git clone "${repo}" "${d}") || exit 1 + } || exit 1 + log "Fetching latest commits..." + (cd "${fd}" && git pull --ff-only) || exit 1 + log "Getting plugin name..." + pn=$(cd "${fd}" && npx -c 'printf %s\\n "${npm_package_name}"') || exit 1 + [ -n "${pn}" ] || fatal "Unable to determine plugin name for ${d}" + md=node_modules/${pn} + [ -d "${md}" ] || { + log "Installing plugin to ${md}..." + ln -s ../../"${d}" "${md}" + } || exit 1 + [ "${md}" -ef "${fd}" ] || fatal "${md} is not a symlink to ${fd}" +done diff --git a/src/bin/plugins/lib/LICENSE b/src/bin/plugins/lib/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/src/bin/plugins/lib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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/src/bin/plugins/lib/LICENSE.md b/src/bin/plugins/lib/LICENSE.md deleted file mode 100755 index 004c62e1b..000000000 --- a/src/bin/plugins/lib/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ -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/src/bin/plugins/lib/README.md b/src/bin/plugins/lib/README.md index e17a23ed1..2c50b538e 100755 --- a/src/bin/plugins/lib/README.md +++ b/src/bin/plugins/lib/README.md @@ -1,32 +1,47 @@ -[![Travis (.com)](https://api.travis-ci.com/[org_name]/[repo_url].svg?branch=develop)](https://travis-ci.com/github/[org_name]/[repo_url]) +# [plugin_name] -# My awesome plugin README example -Explain what your plugin does and who it's useful for. +TODO: Describe the plugin. ## Example animated gif of usage if appropriate + ![screenshot](https://user-images.githubusercontent.com/220864/99979953-97841d80-2d9f-11eb-9782-5f65817c58f4.PNG) -## Installing +## Installation -``` +From the Etherpad working directory, run: + +```shell npm install --no-save --legacy-peer-deps [plugin_name] ``` -or Use the Etherpad ``/admin`` interface. +Or, install from Etherpad's `/admin/plugins` page. -## Settings -Document settings if any +## Configuration + +TODO ## Testing -Document how to run backend / frontend tests. -### Frontend +To run the backend tests, run the following from the Etherpad working directory: -Visit http://whatever/tests/frontend/ to run the frontend tests. +```shell +(cd src && npm test) +``` -### backend +To run the frontend tests, visit: http://localhost:9001/tests/frontend/ -Type ``cd src && npm run test`` to run the backend tests. +## Copyright and License -## LICENSE -Apache 2.0 +Copyright © [yyyy] [name of copyright owner] +and the [plugin_name] authors and contributors + +Licensed under the [Apache License, Version 2.0](LICENSE) (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/src/bin/plugins/lib/backend-tests.yml b/src/bin/plugins/lib/backend-tests.yml index 3cb7dad50..b80f57019 100644 --- a/src/bin/plugins/lib/backend-tests.yml +++ b/src/bin/plugins/lib/backend-tests.yml @@ -1,6 +1,3 @@ -# 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 @@ -11,40 +8,68 @@ jobs: # 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) + (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: src/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 - - - name: Run the backend tests - run: cd src && npm test - -##ETHERPAD_NPM_V=2 -## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh + - + 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 etherpad core + uses: actions/checkout@v3 + with: + repository: ether/etherpad-lite + - + name: Checkout plugin repository + uses: actions/checkout@v3 + with: + path: ./node_modules/__tmp + - + name: Determine plugin name + id: plugin_name + run: | + cd ./node_modules/__tmp + npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"' + - + name: Rename plugin directory + run: | + mv ./node_modules/__tmp ./node_modules/"${PLUGIN_NAME}" + env: + PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} + - + uses: actions/setup-node@v3 + with: + node-version: 12 + cache: 'npm' + cache-dependency-path: | + src/package-lock.json + src/bin/doc/package-lock.json + node_modules/${{ steps.plugin_name.outputs.plugin_name }}/package-lock.json + - + name: Install plugin dependencies + run: | + cd ./node_modules/"${PLUGIN_NAME}" + npm ci + env: + PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} + # Etherpad core dependencies must be installed after installing the + # plugin's dependencies, otherwise npm will try to hoist common + # dependencies by removing them from src/node_modules and installing them + # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears + # to be buggy, because it sometimes removes dependencies from + # src/node_modules but fails to add them to the top-level node_modules. + # Even if npm correctly hoists the dependencies, the hoisting seems to + # confuse tools such as `npm outdated`, `npm update`, and some ESLint + # rules. + - + name: Install Etherpad core dependencies + run: src/bin/installDeps.sh + - + name: Run the backend tests + run: cd src && npm test diff --git a/src/bin/plugins/lib/dependabot.yml b/src/bin/plugins/lib/dependabot.yml new file mode 100644 index 000000000..1c4bd803f --- /dev/null +++ b/src/bin/plugins/lib/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: "increase" diff --git a/src/bin/plugins/lib/eslintrc.cjs b/src/bin/plugins/lib/eslintrc.cjs new file mode 100644 index 000000000..6c5a92652 --- /dev/null +++ b/src/bin/plugins/lib/eslintrc.cjs @@ -0,0 +1,9 @@ +'use strict'; + +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('eslint-config-etherpad/patch/modern-module-resolution'); + +module.exports = { + root: true, + extends: 'etherpad/plugin', +}; diff --git a/src/bin/plugins/lib/frontend-tests.yml b/src/bin/plugins/lib/frontend-tests.yml new file mode 100644 index 000000000..8491ec3d1 --- /dev/null +++ b/src/bin/plugins/lib/frontend-tests.yml @@ -0,0 +1,110 @@ +# Publicly credit Sauce Labs because they generously support open source +# projects. +name: "frontend tests powered by Sauce Labs" + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - + name: Fail if Dependabot + if: github.actor == 'dependabot[bot]' + run: | + cat <&2 + Frontend tests skipped because Dependabot can't access secrets. + Manually re-run the jobs to run the frontend tests. + For more information, see: + https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/ + EOF + exit 1 + - + name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{github.event.repository.name}} ${{ github.workflow }} - ${{ github.job }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - + name: Check out Etherpad core + uses: actions/checkout@v3 + with: + repository: ether/etherpad-lite + - + uses: actions/setup-node@v3 + with: + node-version: 12 + cache: 'npm' + cache-dependency-path: | + src/package-lock.json + src/bin/doc/package-lock.json + - + name: Check out the plugin + uses: actions/checkout@v3 + with: + path: ./node_modules/__tmp + - + name: export GIT_HASH to env + id: environment + run: | + cd ./node_modules/__tmp + echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - + name: Determine plugin name + id: plugin_name + run: | + cd ./node_modules/__tmp + npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"' + - + name: Rename plugin directory + env: + PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} + run: | + mv ./node_modules/__tmp ./node_modules/"${PLUGIN_NAME}" + - + name: Install plugin dependencies + env: + PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }} + run: | + cd ./node_modules/"${PLUGIN_NAME}" + npm ci + # Etherpad core dependencies must be installed after installing the + # plugin's dependencies, otherwise npm will try to hoist common + # dependencies by removing them from src/node_modules and installing them + # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears + # to be buggy, because it sometimes removes dependencies from + # src/node_modules but fails to add them to the top-level node_modules. + # Even if npm correctly hoists the dependencies, the hoisting seems to + # confuse tools such as `npm outdated`, `npm update`, and some ESLint + # rules. + - + name: Install Etherpad core dependencies + run: src/bin/installDeps.sh + - + name: Create settings.json + run: cp settings.json.template settings.json + - + name: Disable import/export rate limiting + run: | + sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json + - + name: Remove standard frontend test files + run: rm -rf src/tests/frontend/specs + - + uses: saucelabs/sauce-connect-action@v2.1.1 + with: + username: ${{ secrets.SAUCE_USERNAME }} + accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} + - + name: Run the frontend tests + shell: bash + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} + TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} + GIT_HASH: ${{ steps.environment.outputs.sha_short }} + run: | + src/tests/frontend/travis/runner.sh diff --git a/src/bin/plugins/lib/npmpublish.yml b/src/bin/plugins/lib/npmpublish.yml index 4a930144e..d9dfb3fee 100644 --- a/src/bin/plugins/lib/npmpublish.yml +++ b/src/bin/plugins/lib/npmpublish.yml @@ -21,50 +21,86 @@ jobs: # 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 + - + uses: actions/checkout@v3 with: repository: ether/etherpad-lite path: etherpad-lite - - run: mv 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 + - + uses: actions/checkout@v3 + # This is necessary for actions/setup-node because '..' can't be used in + # cache-dependency-path. + - + name: Create ep_etherpad-lite symlink + run: | + mkdir -p node_modules + ln -s ../../etherpad-lite/src node_modules/ep_etherpad-lite + - + uses: actions/setup-node@v3 with: node-version: 12 + cache: 'npm' + cache-dependency-path: | + node_modules/ep_etherpad-lite/package-lock.json + node_modules/ep_etherpad-lite/bin/doc/package-lock.json + package-lock.json # 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 + - + 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 + - + 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 + - + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - + uses: actions/setup-node@v3 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: git push --follow-tags + cache: 'npm' + - + name: Bump version (patch) + run: | + LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1 + NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1 + [ "${NEW_COMMITS}" -gt 0 ] || exit 0 + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + npm ci + npm version patch + git push --follow-tags + # This is required if the package has a prepare script that uses something + # in dependencies or devDependencies. + - + run: npm ci # `npm publish` must come after `git push` otherwise there is a race # condition: If two PRs are merged back-to-back then master/main will be # updated with the commits from the second PR before the first PR's @@ -75,9 +111,12 @@ jobs: # already-used version number. By running `npm publish` after `git push`, # back-to-back merges will cause the first merge's workflow to fail but # the second's will succeed. - - run: npm publish + - + run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - + name: Add package to etherpad organization + run: npm access grant read-write etherpad:developers env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - -##ETHERPAD_NPM_V=2 -## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh diff --git a/src/bin/plugins/listOfficialPlugins b/src/bin/plugins/listOfficialPlugins new file mode 100755 index 000000000..322ad5d3b --- /dev/null +++ b/src/bin/plugins/listOfficialPlugins @@ -0,0 +1,14 @@ +#!/bin/sh +set -e +newline=' +' +mydir=$(cd "${0%/*}" && pwd -P) || exit 1 +cd "${mydir}/../../.." +pdir=$(cd .. && pwd -P) || exit 1 +plugins= +for p in "" "&page=2" "&page=3"; do + curlOut=$(curl "https://api.github.com/users/ether/repos?per_page=100${p}") || exit 1 + plugins=${plugins}${newline}$(printf %s\\n "${curlOut}" \ + | sed -n -e 's;.*git@github.com:ether/\(ep_[^"]*\)\.git.*;\1;p'); +done +printf %s\\n "${plugins}" | sort -u | grep -v '^[[:space:]]*$' diff --git a/src/bin/plugins/reTestAllPlugins.sh b/src/bin/plugins/reTestAllPlugins.sh index 58628bdb0..abe1bca80 100755 --- a/src/bin/plugins/reTestAllPlugins.sh +++ b/src/bin/plugins/reTestAllPlugins.sh @@ -4,7 +4,7 @@ do echo $dir if [[ $dir == *"ep_"* ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then - # node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate + # node src/bin/plugins/checkPlugin.js $dir autopush cd node_modules/$dir git commit -m "Automatic update: bump update to re-run latest Etherpad tests" --allow-empty git push origin master diff --git a/src/bin/plugins/updateAllPluginsScript.sh b/src/bin/plugins/updateAllPluginsScript.sh index bf5280ee0..79be4bc47 100755 --- a/src/bin/plugins/updateAllPluginsScript.sh +++ b/src/bin/plugins/updateAllPluginsScript.sh @@ -10,7 +10,7 @@ do # echo $0 if [[ $dir == *"ep_"* ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then - node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate + node src/bin/plugins/checkPlugin.js $dir autopush fi fi # echo $dir diff --git a/src/bin/plugins/updateCorePlugins.sh b/src/bin/plugins/updateCorePlugins.sh index 402a080ec..3866b8444 100755 --- a/src/bin/plugins/updateCorePlugins.sh +++ b/src/bin/plugins/updateCorePlugins.sh @@ -5,5 +5,5 @@ set -e for dir in node_modules/ep_*; do dir=${dir#node_modules/} [ "$dir" != ep_etherpad-lite ] || continue - node src/bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate + node src/bin/plugins/checkPlugin.js "$dir" autopush done diff --git a/src/bin/release.js b/src/bin/release.js index b66bce4da..e1dbcae3d 100644 --- a/src/bin/release.js +++ b/src/bin/release.js @@ -132,9 +132,9 @@ try { // Many users will be using the latest LTS version of npm, and the latest LTS version of npm uses // lockfileVersion 1. Enforce v1 so that users don't see a (benign) compatibility warning. - if (readJson('./src/package-lock.json').lockfileVersion !== 1) { - throw new Error('Please regenerate package-lock.json with npm v6.x.'); - } + const pkglock = readJson('./src/package-lock.json'); + pkglock.lockfileVersion = 1; + writeJson('./src/package-lock.json', pkglock); run('git add src/package.json'); run('git add src/package-lock.json'); diff --git a/src/ep.json b/src/ep.json index b917aa1f3..ec09696c5 100644 --- a/src/ep.json +++ b/src/ep.json @@ -23,7 +23,7 @@ { "name": "static", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static" + "expressPreSession": "ep_etherpad-lite/node/hooks/express/static" } }, { @@ -35,13 +35,14 @@ { "name": "i18n", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n" + "expressPreSession": "ep_etherpad-lite/node/hooks/i18n" } }, { "name": "specialpages", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages" + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages", + "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages" } }, { @@ -50,16 +51,10 @@ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize" } }, - { - "name": "webaccess", - "hooks": { - "expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess" - } - }, { "name": "apicalls", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls" + "expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls" } }, { @@ -85,7 +80,7 @@ { "name": "tests", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests" + "expressPreSession": "ep_etherpad-lite/node/hooks/express/tests" } }, { @@ -111,7 +106,7 @@ { "name": "openapi", "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/openapi" + "expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi" } } ] diff --git a/src/locales/ar.json b/src/locales/ar.json index 6bfda0854..7255581ec 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -15,21 +15,29 @@ "محمد أحمد عبد الفتاح" ] }, - "admin_plugins": "مدير المساعد", - "admin_plugins.available": "الإضافات المتوفرة", - "admin_plugins.available_not-found": "لم يتم العثور على مكونات إضافية.", - "admin_plugins.available_fetching": "جارٍ الجلب...", - "admin_plugins.available_install.value": "تنصيب", - "admin_plugins.available_search.placeholder": " تنصيب عن الإضافات لتثبيتها", + "admin.page-title": "لوحة تحكم المسؤول - Etherpad", "admin_plugins.description": "الوصف", - "admin_plugins.installed_uninstall.value": "فك التنصيب", - "admin_plugins.last-update": "آخر تحديث", + "admin_plugins.installed": "الإضافات المثبتة", + "admin_plugins.installed_fetching": "جارٍ إحضار المكونات الإضافية المثبتة ...", + "admin_plugins.installed_nothing": "لم تقم بتثبيت أي مكونات إضافية حتى الآن.", "admin_plugins.name": "الاسم", + "admin_plugins.page-title": "مدير البرنامج المساعد - Etherpad", "admin_plugins.version": "الإصدار", - "admin_plugins_info.version_latest": "أحدث نسخة متاحة", + "admin_plugins_info": "معلومات استكشاف الأخطاء وإصلاحها", + "admin_plugins_info.hooks": "خطافات مثبتة", + "admin_plugins_info.hooks_client": "خطاطيف من جانب العميل", + "admin_plugins_info.hooks_server": "خطاطيف من جانب الخادم", + "admin_plugins_info.parts": "الأجزاء المثبتة", + "admin_plugins_info.plugins": "الإضافات المثبتة", + "admin_plugins_info.page-title": "معلومات البرنامج المساعد - Etherpad", + "admin_plugins_info.version": "إصدار Etherpad", "admin_plugins_info.version_number": "رقم الإصدار", "admin_settings": "إعدادات", - "admin_settings.current": "التكوين الحالي", + "admin_settings.current_example-devel": "مثال على قالب إعدادات التطوير", + "admin_settings.current_example-prod": "مثال على قالب إعدادات الإنتاج", + "admin_settings.current_restart.value": "أعد تشغيل Etherpad", + "admin_settings.current_save.value": "حفظ الإعدادات", + "admin_settings.page-title": "الإعدادات - Etherpad", "index.newPad": "باد جديد", "index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:", "index.openPad": "افتح باد موجودة بالاسم:", @@ -101,8 +109,8 @@ "pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.", "pad.modals.deleted": "محذوف.", "pad.modals.deleted.explanation": "تمت إزالة هذا الباد.", - "pad.modals.rateLimited.explanation": "لقد أرسلت عددًا كبيرًا جدًا من الرسائل إلى هذه اللوحة ، لذا فقد قطع اتصالك.", - "pad.modals.rejected.explanation": "رفض الخادم رسالة أرسلها متصفحك.", + "pad.modals.rateLimited": "معدل محدود.", + "pad.modals.rejected.cause": "ربما تم تحديث الخادم أثناء عرض اللوحة ، أو ربما كان هناك خطأ في Etherpad. حاول إعادة تحميل الصفحة.", "pad.modals.disconnected": "لم تعد متصلا.", "pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم", "pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.", @@ -115,6 +123,7 @@ "pad.chat.loadmessages": "تحميل المزيد من الرسائل", "pad.chat.stick.title": "ألصق الدردشة بالشاشة", "pad.chat.writeMessage.placeholder": "اكتب رسالتك هنا", + "timeslider.followContents": "اتبع تحديثات محتوى الوسادة", "timeslider.pageTitle": "{{appTitle}} متصفح التاريخ", "timeslider.toolbar.returnbutton": "العودة إلى الباد", "timeslider.toolbar.authors": "المؤلفون:", diff --git a/src/locales/awa.json b/src/locales/awa.json index 5457e37d3..947cde330 100644 --- a/src/locales/awa.json +++ b/src/locales/awa.json @@ -1,7 +1,8 @@ { "@metadata": { "authors": [ - "1AnuraagPandey" + "1AnuraagPandey", + "बडा काजी" ] }, "index.newPad": "नयाँ प्याड", @@ -35,7 +36,7 @@ "pad.modals.unauth": "अनाधिकृत", "pad.modals.initsocketfail": "सर्भरमा पहुँच से बहरे है ।", "pad.share.readonly": "पढय वाला खाली", - "pad.share.link": "लिङ्क", + "pad.share.link": "कडी", "pad.share.emebdcode": "URL जोडा जाय", "pad.chat": "बातचीत", "timeslider.pageTitle": "{{appTitle}} समय रेखा", diff --git a/src/locales/be-tarask.json b/src/locales/be-tarask.json index a759eda7e..b785fb9cc 100644 --- a/src/locales/be-tarask.json +++ b/src/locales/be-tarask.json @@ -13,6 +13,28 @@ "admin_plugins.available_not-found": "Плагіны ня знойдзеныя.", "admin_plugins.available_fetching": "Атрымліваем…", "admin_plugins.available_install.value": "Усталяваць", + "admin_plugins.available_search.placeholder": "Шукаць пашырэньні для ўсталяваньня", + "admin_plugins.description": "Апісаньне", + "admin_plugins.installed": "Усталяваныя пашырэньні", + "admin_plugins.installed_fetching": "Атрыманьне ўсталяваных пашырэньняў…", + "admin_plugins.installed_nothing": "Вы пакуль не ўсталявалі ніводнага пашырэньня.", + "admin_plugins.installed_uninstall.value": "Выдаліць", + "admin_plugins.last-update": "Апошняе абнаўленьне", + "admin_plugins.name": "Назва", + "admin_plugins.page-title": "Кіраўнік пашырэньняў — Etherpad", + "admin_plugins.version": "Вэрсія", + "admin_plugins_info": "Інфармацыя пра вырашэньне няспраўнасьцяў", + "admin_plugins_info.hooks": "Усталяваныя кручкі", + "admin_plugins_info.hooks_client": "Кліенцкія кручкі", + "admin_plugins_info.hooks_server": "Сэрвэрныя кручкі", + "admin_plugins_info.parts": "Усталяваныя часткі", + "admin_plugins_info.plugins": "Усталяваныя пашырэньні", + "admin_plugins_info.page-title": "Інфармацыя пра пашырэньне — Etherpad", + "admin_plugins_info.version": "Вэрсія Etherpad", + "admin_plugins_info.version_latest": "Апошняя даступная вэрсія", + "admin_plugins_info.version_number": "Нумар вэрсіі", + "admin_settings": "Налады", + "admin_settings.current": "Цяперашняя канфігурацыя", "admin_settings.page-title": "Налады — Etherpad", "index.newPad": "Стварыць", "index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:", diff --git a/src/locales/bn.json b/src/locales/bn.json index b46ed8bdb..aa551f807 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -9,6 +9,7 @@ "Nasir8891", "Sankarshan", "Sibabrata Banerjee", + "আজিজ", "আফতাবুজ্জামান" ] }, @@ -22,6 +23,7 @@ "admin_plugins.description": "বিবরণ", "admin_plugins.installed": "ইন্সটল হওয়া প্লাগিনসমূহ", "admin_plugins.installed_fetching": "ইন্সটলকৃত প্লাগিন আনা হচ্ছে", + "admin_plugins.installed_nothing": "আপনি এখনও কোনো প্লাগইন ইনস্টল করেননি।", "admin_plugins.installed_uninstall.value": "আনইনস্টল করুন", "admin_plugins.last-update": "সর্বশেষ হালনাগাদ", "admin_plugins.name": "নাম", @@ -39,10 +41,14 @@ "admin_plugins_info.version_number": "সংস্করণ সংখ্যা", "admin_settings": "সেটিংসমূহ", "admin_settings.current": "বর্তমান কনফিগারেশন", + "admin_settings.current_example-devel": "উদাহরণ ডেভেলপমেন্ট সেটিংস টেমপ্লেট", + "admin_settings.current_example-prod": "উদাহরণ উৎপাদন সেটিংস টেমপ্লেট", "admin_settings.current_restart.value": "ইথারপ্যাড পুনরায় চালু করুন", "admin_settings.current_save.value": "সেটিংসমূহ সংরক্ষণ করুন", + "admin_settings.page-title": "সেটিংস - ইথারপ্যাড", "index.newPad": "নতুন প্যাড", "index.createOpenPad": "অথবা নাম লিখে প্যাড খুলুন/তৈরী করুন:", + "index.openPad": "নাম সহ একটি বিদ্যমান প্যাড খুলুন:", "pad.toolbar.bold.title": "গাঢ় (Ctrl-B)", "pad.toolbar.italic.title": "বাঁকা (Ctrl+I)", "pad.toolbar.underline.title": "নিম্নরেখা (Ctrl+U)", @@ -90,12 +96,27 @@ "pad.modals.connected": "সংযোগস্থাপন করা হয়েছে।", "pad.modals.reconnecting": "আপনার প্যাডের সাথে সংযোগস্থাপন করা হচ্ছে…", "pad.modals.forcereconnect": "পুনরায় সংযোগস্থাপনের চেষ্টা", + "pad.modals.reconnecttimer": "পুনঃসংযোগের চেষ্টা করা হচ্ছে", + "pad.modals.cancel": "বাতিল", "pad.modals.userdup": "অন্য উইন্ডো-তে খোলা হয়েছে", + "pad.modals.userdup.explanation": "এই প্যাডটি এই কম্পিউটারে একাধিক ব্রাউজার উইন্ডোতে খোলা হয়েছে বলে মনে হচ্ছে৷", + "pad.modals.userdup.advice": "পরিবর্তে এই উইন্ডোটি ব্যবহার করতে পুনঃসংযোগ করুন৷", "pad.modals.unauth": "আপনার অধিকার নেই", + "pad.modals.unauth.explanation": "এই পৃষ্ঠাটি দেখার সময় আপনার অনুমতি পরিবর্তিত হয়েছে৷ পুনঃসংযোগের চেষ্টা করুন।", + "pad.modals.looping.explanation": "সিঙ্ক্রোনাইজেশন সার্ভারের সাথে যোগাযোগের সমস্যা রয়েছে।", + "pad.modals.looping.cause": "সম্ভবত আপনি একটি বেমানান ফায়ারওয়াল বা প্রক্সির মাধ্যমে সংযুক্ত হয়েছেন৷", "pad.modals.initsocketfail": "সার্ভারে পৌঁছানো যাচ্ছে না।", + "pad.modals.initsocketfail.explanation": "সিঙ্ক্রোনাইজেশন সার্ভারের সাথে সংযোগ করা যায়নি৷", + "pad.modals.initsocketfail.cause": "এটি সম্ভবত আপনার ব্রাউজার বা আপনার ইন্টারনেট সংযোগের কোনও সমস্যার কারণে হয়েছে৷", "pad.modals.slowcommit.explanation": "সার্ভার সাড়া দিচ্ছে না।", + "pad.modals.slowcommit.cause": "এটি নেটওয়ার্ক সংযোগের সমস্যার কারণে হতে পারে।", + "pad.modals.badChangeset.explanation": "আপনার করা একটি সম্পাদনা সিঙ্ক্রোনাইজেশন সার্ভার কর্তৃক বেআইনি হিসেবে শ্রেণীবদ্ধ করা হয়েছে৷", + "pad.modals.corruptPad.explanation": "আপনি যে প্যাডে প্রবেশ করার চেষ্টা করছেন সেটি দূষিত।", + "pad.modals.corruptPad.cause": "এটি একটি ভুল সার্ভার কনফিগারেশন বা অন্য কোনও অপ্রত্যাশিত আচরণের কারণে হতে পারে। পরিষেবা প্রশাসকের সাথে যোগাযোগ করুন।", "pad.modals.deleted": "অপসারিত।", "pad.modals.deleted.explanation": "এই প্যাডটি অপসারণ করা হয়েছে।", + "pad.modals.rateLimited.explanation": "আপনি এই প্যাডে অনেকগুলি বার্তা পাঠিয়েছেন তাই এটি আপনাকে সংযোগ বিচ্ছিন্ন করেছে৷", + "pad.modals.rejected.explanation": "সার্ভার আপনার ব্রাউজারের পাঠানো একটি বার্তা প্রত্যাখ্যান করেছে৷", "pad.modals.disconnected": "আপনি সংযোগ বিচ্ছিন্ন হয়েছে গেছে।", "pad.modals.disconnected.explanation": "সার্ভারের সাথে যোগাযোগ করা যাচ্ছে না", "pad.share": "শেয়ার করুন", @@ -127,6 +148,7 @@ "timeslider.month.november": "নভেম্বর", "timeslider.month.december": "ডিসেম্বর", "timeslider.unnamedauthors": "নামবিহীন {{num}} জন {[plural(num) one: লেখক, other: লেখক ]}", + "pad.savedrevs.marked": "এই সংশোধনটি এখন সংরক্ষিত সংশোধন হিসেবে চিহ্নিত করা হয়েছে", "pad.userlist.entername": "আপনার নাম লিখুন", "pad.userlist.unnamed": "কোন নাম নির্বাচন করা হয়নি", "pad.impexp.importbutton": "এখন আমদানি করুন", @@ -135,5 +157,6 @@ "pad.impexp.uploadFailed": "আপলোড করতে ব্যর্থ, দয়া করে আবার চেষ্টা করুন", "pad.impexp.importfailed": "আমদানি ব্যর্থ", "pad.impexp.copypaste": "দয়া করে অনুলিপি প্রতিলেপন করুন", - "pad.impexp.exportdisabled": "{{type}} হিসেবে রপ্তানি করা নিষ্ক্রিয় আছে। বিস্তারিত জানার জন্য আপনার সিস্টেম প্রশাসকের সাথে যোগাযোগ করুন।" + "pad.impexp.exportdisabled": "{{type}} হিসেবে রপ্তানি করা নিষ্ক্রিয় আছে। বিস্তারিত জানার জন্য আপনার সিস্টেম প্রশাসকের সাথে যোগাযোগ করুন।", + "pad.impexp.maxFileSize": "ফাইল খুব বড়। আমদানির জন্য অনুমোদিত ফাইলের আকার বাড়াতে আপনার সাইট প্রশাসকের সাথে যোগাযোগ করুন" } diff --git a/src/locales/br.json b/src/locales/br.json index aab67517c..d3c33202d 100644 --- a/src/locales/br.json +++ b/src/locales/br.json @@ -4,6 +4,7 @@ "Fohanno", "Fulup", "Gwenn-Ael", + "Huñvreüs", "Y-M D" ] }, @@ -86,7 +87,7 @@ "pad.chat.title": "Digeriñ ar flap kevelet gant ar pad-mañ.", "pad.chat.loadmessages": "Kargañ muioc'h a gemennadennoù", "pad.chat.stick.title": "Gwriziennañ an diviz war ar skramm", - "pad.chat.writeMessage.placeholder": "Skrivañ ho kemenadenn amañ", + "pad.chat.writeMessage.placeholder": "Skrivañ ho kemennadenn amañ", "timeslider.pageTitle": "Istor dinamek eus {{appTitle}}", "timeslider.toolbar.returnbutton": "Distreiñ d'ar pad-mañ.", "timeslider.toolbar.authors": "Aozerien :", diff --git a/src/locales/de.json b/src/locales/de.json index 439e9c9e3..99f6321ac 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -3,6 +3,7 @@ "authors": [ "Bjarncraft", "Dom", + "Justman10000", "Killarnee", "Metalhead64", "Mklehr", @@ -44,6 +45,7 @@ "admin_settings": "Einstellungen", "admin_settings.current": "Derzeitige Konfiguration", "admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates", + "admin_settings.current_example-prod": "Beispiel einer Vorlage für Produktionseinstellungen", "admin_settings.current_restart.value": "Etherpad neustarten", "admin_settings.current_save.value": "Einstellungen speichern", "admin_settings.page-title": "Einstellungen - Etherpad", @@ -121,6 +123,7 @@ "pad.modals.rateLimited": "Begrenzte Rate.", "pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.", "pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.", + "pad.modals.rejected.cause": "Möglicherweise wurde der Server aktualisiert, während du das Pad angesehen hast, oder es existiert ein Fehler in Etherpad. Versuche, die Seite neu zu laden.", "pad.modals.disconnected": "Ihre Verbindung wurde getrennt.", "pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.", "pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.", diff --git a/src/locales/dsb.json b/src/locales/dsb.json index e5fbc8de9..cedac2f88 100644 --- a/src/locales/dsb.json +++ b/src/locales/dsb.json @@ -4,19 +4,53 @@ "Michawiki" ] }, + "admin.page-title": "Administratorowa delka – Etherpad", + "admin_plugins": "Zastojnik tykacow", + "admin_plugins.available": "K dispoziciji stojece tykace", + "admin_plugins.available_not-found": "Žedne tykace namakane.", + "admin_plugins.available_fetching": "Wobstarujo se …", + "admin_plugins.available_install.value": "Instalěrowaś", + "admin_plugins.available_search.placeholder": "Tykace za instalaciju pytaś", + "admin_plugins.description": "Wopisanje", + "admin_plugins.installed": "Zainstalěrowane tykace", + "admin_plugins.installed_fetching": "Zainstalěrowane tykace se wobstaruju …", + "admin_plugins.installed_nothing": "Hyšći njejsćo zainstalěrował tykace.", + "admin_plugins.installed_uninstall.value": "Wótinstalěrowaś", + "admin_plugins.last-update": "Slědna aktualizacija", + "admin_plugins.name": "Mě", + "admin_plugins.page-title": "Zastojnik tykacow – Etherpad", + "admin_plugins.version": "Wersija", + "admin_plugins_info": "Informacije wó rozwězanju problemow", + "admin_plugins_info.hooks": "Zainstalěrowane kokulki", + "admin_plugins_info.hooks_client": "Kokulki z boka klienta", + "admin_plugins_info.hooks_server": "Kokulki z boka serwera", + "admin_plugins_info.parts": "Zainstalěrowane źěle", + "admin_plugins_info.plugins": "Zainstalěrowane tykace", + "admin_plugins_info.page-title": "Tykacowe informacije – Ehterpad", + "admin_plugins_info.version": "Wersija Etherpad", + "admin_plugins_info.version_latest": "Nejnowša wersija", + "admin_plugins_info.version_number": "Wersijowy numer", + "admin_settings": "Nastajenja", + "admin_settings.current": "Aktualna konfiguracija", + "admin_settings.current_example-devel": "Pśikładowa pśedłoga wuwijańskich nastajenjow", + "admin_settings.current_example-prod": "Pśikładowa pśedłoga produkciskich nastajenjow", + "admin_settings.current_restart.value": "Etherpad znowego startowaś", + "admin_settings.current_save.value": "Nastajenja składowaś", + "admin_settings.page-title": "Nastajenja – Etherpad", "index.newPad": "Nowy zapisnik", "index.createOpenPad": "abo napóraj/wócyń zapisnik z mjenim:", + "index.openPad": "wócyńśo eksistěrujucy Pad z mjenim:", "pad.toolbar.bold.title": "Tucny (Strg-B)", "pad.toolbar.italic.title": "Kursiwny (Strg-I)", "pad.toolbar.underline.title": "Pódšmarnuś (Strg-U)", - "pad.toolbar.strikethrough.title": "Pśešmarnuś", - "pad.toolbar.ol.title": "Numerěrowana lisćina", - "pad.toolbar.ul.title": "Nalicenje", + "pad.toolbar.strikethrough.title": "Pśešmarnuś (Strg+5)", + "pad.toolbar.ol.title": "Numerěrowana lisćina (Strg+Umsch+N)", + "pad.toolbar.ul.title": "Nalicenje (Strg+Umsch+L)", "pad.toolbar.indent.title": "Zasunuś (TAB)", "pad.toolbar.unindent.title": "Wusunuś (Umsch+TAB)", "pad.toolbar.undo.title": "Anulěrowaś (Strg-Z)", "pad.toolbar.redo.title": "Wóspjetowaś (Strg-Y)", - "pad.toolbar.clearAuthorship.title": "Awtorowe barwy lašowaś", + "pad.toolbar.clearAuthorship.title": "Awtorowe barwy lašowaś (Strg+Umsch+C)", "pad.toolbar.import_export.title": "Import/Eksport z/do drugich datajowych formatow", "pad.toolbar.timeslider.title": "Wersijowa historija", "pad.toolbar.savedRevision.title": "Wersiju składowaś", @@ -26,29 +60,36 @@ "pad.colorpicker.save": "Składowaś", "pad.colorpicker.cancel": "Pśetergnuś", "pad.loading": "Zacytujo se...", + "pad.noCookie": "Cookie njejo se namakał. Pšosym dowólśo cookieje w swójom wobglědowaku! Wašo pósejźenje a waše nastajenja se mjazy dwěma woglědoma njeskładuju. To móžo se stas, gaž Etherpad jo w někotarych wobglědowakach w iFrame wopśimjony. Pšosym zawěsććo, až Etherpad jo na samskej póddomenje/domenje ako nadrědowany iFrame", "pad.permissionDenied": "Njamaš pśistupne pšawo za toś ten zapisnik.", "pad.settings.padSettings": "Nastajenja zapisnika", "pad.settings.myView": "Mój naglěd", "pad.settings.stickychat": "Chat pśecej na wobrazowce pokazaś", + "pad.settings.chatandusers": "Chat a wužywarje pokazaś", "pad.settings.colorcheck": "Awtorowe barwy", "pad.settings.linenocheck": "Smužkowe numery", "pad.settings.rtlcheck": "Wopśimjeśe wótpšawa nalěwo cytaś?", "pad.settings.fontType": "Pismowa družyna:", "pad.settings.fontType.normal": "Normalny", "pad.settings.language": "Rěc:", + "pad.settings.about": "Wó", + "pad.settings.poweredBy": "Pódpěrany wót", "pad.importExport.import_export": "Import/Eksport", "pad.importExport.import": "Tekstowu dataju abo dokument nagraś", "pad.importExport.importSuccessful": "Wuspěšny!", "pad.importExport.export": "Aktualny zapisnik eksportěrowaś ako:", + "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Lutny tekst", "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Móžoš jano z fprmatow lutnego teksta abo z HTML-formata importěrowaś. Za wěcej rozšyrjone importěrowańske funkcije instalěruj pšosym Abiword.", + "pad.importExport.abiword.innerHTML": "Móžoš jano z fprmatow lutnego teksta abo z HTML-formata importěrowaś. Za wěcej rozšyrjone importěrowańske funkcije instalěruj pšosym Abiword abo LibreOffice.", "pad.modals.connected": "Zwězany.", "pad.modals.reconnecting": "Zwězujo se znowego z twójim zapisnikom...", "pad.modals.forcereconnect": "Znowego zwězaś", + "pad.modals.reconnecttimer": "Wopytaj se znowego zwězaś w", + "pad.modals.cancel": "Pśetergnuś", "pad.modals.userdup": "W drugem woknje wócynjony", "pad.modals.userdup.explanation": "Zda se, až toś ten zapisnik jo se we wěcej ako jadnem woknje wobglědowaka na toś tom licadłu wócynił.", "pad.modals.userdup.advice": "Zwězaj znowego, aby toś to wokno město togo wužywał.", @@ -67,6 +108,10 @@ "pad.modals.corruptPad.cause": "To jo se snaź wopacneje serweroweje konfiguracije dla abo drugego njewócakanego zaźaržanja dla stało. Pšosym staj se ze słužbowym administratorom do zwiska.", "pad.modals.deleted": "Wulašowany.", "pad.modals.deleted.explanation": "Toś ten zapisnik jo se wótpórał.", + "pad.modals.rateLimited": "Wobgranicowana rata.", + "pad.modals.rateLimited.explanation": "Sćo pósłał pśewjele powěsćow na zapisnik, togodla jo se zwisk źělił.", + "pad.modals.rejected.explanation": "Serwer jo wótpokazał powěsć, kótaraž jo se pósłał pśez waš wobglědowak pósłał.", + "pad.modals.rejected.cause": "Serwer jo se snaź zaktualizěrował, mjaztym až sy se woglědał zapisnik, abo dajo snaź zmólku w Etherpad. Wopytaj bok znowego zacytaś.", "pad.modals.disconnected": "Zwisk jo pśetergnjony.", "pad.modals.disconnected.explanation": "Zwisk ze serwerom jo se zgubił", "pad.modals.disconnected.cause": "Serwer njestoj k dispoziciji. Pšosym informěruj słužbowego administratora, jolic to se dalej stawa.", @@ -77,6 +122,9 @@ "pad.chat": "Chat", "pad.chat.title": "Chat za toś ten zapisnik wócyniś", "pad.chat.loadmessages": "Dalšne powěsći zacytaś", + "pad.chat.stick.title": "Chat k wobrazowce pśipěś", + "pad.chat.writeMessage.placeholder": "Piš swóju powěsć how", + "timeslider.followContents": "Aktualizacijam wopśimjeśa zapisnika slědowaś", "timeslider.pageTitle": "{{appTitle}} - wersijowa historija", "timeslider.toolbar.returnbutton": "Slědk k zapisnikoju", "timeslider.toolbar.authors": "Awtory:", @@ -85,6 +133,9 @@ "timeslider.exportCurrent": "Aktualnu wersiju eksportěrowaś ako:", "timeslider.version": "Wersija {{version}}", "timeslider.saved": "Składowany {{day}}. {{month}} {{year}}", + "timeslider.playPause": "Wopśimjeśe zapisnika wótgraś/pawzěrowaś", + "timeslider.backRevision": "Wó jadnu wersiju w toś tom dokumenśe slědk hyś", + "timeslider.forwardRevision": "Wó jadnu wersiju w toś tom dokumenśe doprědka hyś", "timeslider.dateformat": "{{day}}. {{month}} {{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "januara", "timeslider.month.february": "februara", @@ -100,15 +151,18 @@ "timeslider.month.december": "decembra", "timeslider.unnamedauthors": "{{num}} {[plural(num) one: awtor, two: awtora, few: awtory, other: awtorow ]} bźez mjenja", "pad.savedrevs.marked": "Toś ta wersija jo se něnto ako składowana wersija markěrowała", + "pad.savedrevs.timeslider": "Móžoš se skłaźone wersije woglědowaś, gaž se k historiji dokumenta woglědujoś.", "pad.userlist.entername": "Zapódaj swójo mě", "pad.userlist.unnamed": "bźez mjenja", - "pad.editbar.clearcolors": "Awtorowe barwy w cełem dokumenśe lašowaś?", + "pad.editbar.clearcolors": "Awtorowe barwy w cełem dokumenśe lašowaś? To njedajo se anulěrowaś", "pad.impexp.importbutton": "Něnto importěrowaś", "pad.impexp.importing": "Importěrujo se...", "pad.impexp.confirmimport": "Importowanje dataje pśepišo aktualny tekst zapisnika. Coš napšawdu pókšacowaś?", "pad.impexp.convertFailed": "Njejsmy mógli toś tu dataju importěrowaś. Pšosym wužyj drugi dokumentowy format abo kopěruj manuelnje", + "pad.impexp.padHasData": "Njejsmy mógli toś tu dataju importěrowaś, dokulaž toś ten dokument južo změny wopśimujo, pšosym importěruj nowy dokument.", "pad.impexp.uploadFailed": "Nagraśe njejo se raźiło, pšosym wopytaj hyšći raz", "pad.impexp.importfailed": "Import njejo se raził", "pad.impexp.copypaste": "Pšosym kopěrowaś a zasajźiś", - "pad.impexp.exportdisabled": "Eksport ako format {{type}} jo znjemóžnjony. Pšosym staj se ze swójim systemowym administratorom za drobnostki do zwiska." + "pad.impexp.exportdisabled": "Eksport ako format {{type}} jo znjemóžnjony. Pšosym staj se ze swójim systemowym administratorom za drobnostki do zwiska.", + "pad.impexp.maxFileSize": "Dataja jo pśewjelika. Staj se ze swójim sedłowym administratorom do zwiska, aby dowólonu datajowu wjelikosć za import pówušył" } diff --git a/src/locales/dty.json b/src/locales/dty.json index 50e97bac4..b0dd53568 100644 --- a/src/locales/dty.json +++ b/src/locales/dty.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Nirajan pant", + "बडा काजी", "रमेश सिंह बोहरा", "राम प्रसाद जोशी" ] @@ -78,7 +79,7 @@ "pad.modals.disconnected.cause": "सर्भर अनुपलब्ध होइसकन्छ। यदि यो हुनोइ रयाबर कृपया सेवा व्यवस्थापकलाई सूचित अरऽ।", "pad.share": "यस प्याडलाई बाड्न्या", "pad.share.readonly": "पड्‍ड्या मात्तरै", - "pad.share.link": "लिङ्क", + "pad.share.link": "कडी", "pad.share.emebdcode": "URL थप्प्या", "pad.chat": "कुरणिकानी", "pad.chat.title": "येइ प्याड खिलाइ गफ खोलऽ", diff --git a/src/locales/el.json b/src/locales/el.json index 8ca8266d6..502063c11 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -6,35 +6,66 @@ "Glavkos", "Monopatis", "Norhorn", + "Papspyr", "Protnet" ] }, - "index.newPad": "Νέο Pad", - "index.createOpenPad": "ή δημιουργία/άνοιγμα ενός Pad με όνομα:", - "index.openPad": "άνοιγμα υπάρχων Pad με όνομα:", - "pad.toolbar.bold.title": "Έντονη (Ctrl-B)", + "admin.page-title": "Πίνακας ελέγχου διαχειριστή - Etherpad", + "admin_plugins": "Διαχειριστής πρόσθετων", + "admin_plugins.available": "Διαθέσιμα πρόσθετα", + "admin_plugins.available_not-found": "Δεν βρέθηκαν πρόσθετα.", + "admin_plugins.available_install.value": "Εγκατάσταση", + "admin_plugins.available_search.placeholder": "Αναζητήστε πρόσθετα για εγκατάσταση", + "admin_plugins.description": "Περιγραφή", + "admin_plugins.installed": "Εγκατεστημένα πρόσθετα", + "admin_plugins.installed_nothing": "Δεν έχετε εγκαταστήσει πρόσθετα ακόμη.", + "admin_plugins.installed_uninstall.value": "Απεγκατάσταση", + "admin_plugins.last-update": "Τελευταία ενημέρωση", + "admin_plugins.name": "Όνομα", + "admin_plugins.page-title": "Διαχειριστής πρόσθετων - Etherpad", + "admin_plugins.version": "Έκδοση", + "admin_plugins_info": "Πληροφορίες αντιμετώπισης προβλημάτων", + "admin_plugins_info.hooks": "Εγκατεστημένα άγκιστρα", + "admin_plugins_info.hooks_server": "Άγκιστρα από την πλευρά του διακομιστή", + "admin_plugins_info.parts": "Εγκατεστημένα εξαρτήματα", + "admin_plugins_info.plugins": "Εγκατεστημένα πρόσθετα", + "admin_plugins_info.page-title": "Πληροφορίες πρόσθετου - Etherpad", + "admin_plugins_info.version": "Έκδοση Etherpad", + "admin_plugins_info.version_latest": "Τελευταία διαθέσιμη έκδοση", + "admin_plugins_info.version_number": "Αριθμός έκδοσης", + "admin_settings": "Ρυθμίσεις", + "admin_settings.current": "Τρέχουσα διαμόρφωση", + "admin_settings.current_example-devel": "Παράδειγμα προτύπου ρυθμίσεων ανάπτυξης", + "admin_settings.current_example-prod": "Παράδειγμα προτύπου ρυθμίσεων παραγωγής", + "admin_settings.current_restart.value": "Επανεκκινήστε το Etherpad", + "admin_settings.current_save.value": "Αποθήκευση Ρυθμίσεων", + "admin_settings.page-title": "Ρυθμίσεις - Etherpad", + "index.newPad": "Νέος Κοινόχρηστος Πίνακας", + "index.createOpenPad": "ή δημιουργία/άνοιγμα ενός κοινόχρηστου πίνακα με όνομα:", + "index.openPad": "άνοιγμα υπάρχοντος κοινόχρηστού πίνακα με όνομα:", + "pad.toolbar.bold.title": "Έντονα (Ctrl-B)", "pad.toolbar.italic.title": "Πλάγια (Ctrl-I)", - "pad.toolbar.underline.title": "Υπογράμμιση (Ctrl-U)", + "pad.toolbar.underline.title": "Υπογραμμισμένα (Ctrl-U)", "pad.toolbar.strikethrough.title": "Διακριτή διαγραφή (Ctrl+5)", "pad.toolbar.ol.title": "Ταξινομημένη λίστα (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Λίστα χωρίς σειρά (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Αριστερά εσοχή (TAB)", - "pad.toolbar.unindent.title": "Δεξιά εσοχή (Shift+TAB)", + "pad.toolbar.ul.title": "Λίστα χωρίς ταξινόμηση (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Εσοχή (TAB)", + "pad.toolbar.unindent.title": "Εσοχή (Shift+TAB)", "pad.toolbar.undo.title": "Αναίρεση (Ctrl-Z)", "pad.toolbar.redo.title": "Επανάληψη (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Εκκαθάριση των χρωμάτων των συντακτών (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "Εκκαθάριση χρωμάτων σύνταξης κειμένου (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Εισαγωγή/Εξαγωγή από/σε διαφορετικούς τύπους αρχείων", "pad.toolbar.timeslider.title": "Χρονοδιάγραμμα", "pad.toolbar.savedRevision.title": "Αποθήκευση Αναθεώρησης", "pad.toolbar.settings.title": "Ρυθμίσεις", - "pad.toolbar.embed.title": "Διαμοίραση και Ενσωμάτωση αυτού του pad", - "pad.toolbar.showusers.title": "Εμφάνιση των χρηστών αυτού του pad", + "pad.toolbar.embed.title": "Διαμοίραση και Ενσωμάτωση αυτού του κοινόχρηστου πίνακα", + "pad.toolbar.showusers.title": "Εμφάνιση των χρηστών αυτού του κοινόχρηστου πίνακα", "pad.colorpicker.save": "Αποθήκευση", - "pad.colorpicker.cancel": "Άκυρο", + "pad.colorpicker.cancel": "Ακύρωση", "pad.loading": "Φόρτωση...", - "pad.noCookie": "Το cookie δεν βρέθηκε. Παρακαλώ επιτρέψτε τα cookies στον περιηγητή σας! Η περίοδος σύνδεσης και οι ρυθμίσεις σας δεν θα αποθηκευτούν μεταξύ των επισκέψεων. Αυτό μπορεί να οφείλεται στο ότι το Etherpad περιλαμβάνεται σε ένα iFrame σε ορισμένα προγράμματα περιήγησης. Βεβαιωθείτε ότι το Etherpad βρίσκεται στον ίδιο υποτομέα/τομέα με το γονικό iFrame", - "pad.permissionDenied": "Δεν έχετε δικαίωμα πρόσβασης σε αυτό το pad", - "pad.settings.padSettings": "Ρυθμίσεις Pad", + "pad.noCookie": "Το cookie δε βρέθηκε. Παρακαλούμε επιτρέψτε τα cookies στο φυλλομετρητή σας! Η περίοδος σύνδεσης και οι ρυθμίσεις σας δε θα αποθηκευτούν μεταξύ των επισκέψεων. Αυτό μπορεί να οφείλεται επειδή το Etherpad περιλαμβάνεται στο iFrame σε ορισμένα προγράμματα πλοήγησης. Παρακαλούμε βεβαιωθείτε ότι το Etherpad βρίσκεται στον ίδιο υποτομέα/τομέα με το iFrame", + "pad.permissionDenied": "Δεν έχετε δικαίωμα πρόσβασης σε αυτόν τον κοινόχρηστο πίνακα", + "pad.settings.padSettings": "Ρυθμίσεις κοινόχρηστου πίνακα", "pad.settings.myView": "Η προβολή μου", "pad.settings.stickychat": "Να είναι πάντα ορατή η συνομιλία", "pad.settings.chatandusers": "Εμφάνιση Συνομιλίας και Χρηστών", @@ -63,7 +94,7 @@ "pad.modals.reconnecttimer": "Προσπάθεια επανασύνδεσης σε", "pad.modals.cancel": "Ακύρωση", "pad.modals.userdup": "Ανοιγμένο σε άλλο παράθυρο", - "pad.modals.userdup.explanation": "Αυτό το pad φαίνεται να είναι ανοιχτό σε περισσότερα από ένα παράθυρο του προγράμματος περιήγησης σε αυτόν τον υπολογιστή.", + "pad.modals.userdup.explanation": "Αυτός ο κοινόχρηστος πίνακας φαίνεται να είναι ανοιχτός σε περισσότερα από ένα παράθυρο του προγράμματος περιήγησης σε αυτόν τον υπολογιστή.", "pad.modals.userdup.advice": "Επανασυνδεθείτε για να χρησιμοποιήσετε αυτό το παράθυρο.", "pad.modals.unauth": "Δεν επιτρέπεται", "pad.modals.unauth.explanation": "Τα δικαιώματά σας άλλαξαν όσο βλέπατε αυτήν τη σελίδα. Δοκιμάστε να επανασυνδεθείτε.", @@ -80,6 +111,7 @@ "pad.modals.corruptPad.cause": "Αυτό μπορεί να οφείλεται σε ένα λάθος στη ρύθμιση του διακομιστή ή κάποια άλλη απρόβλεπτη συμπεριφορά. Παρακαλώ επικοινωνήστε με τον διαχειριστή της υπηρεσίας.", "pad.modals.deleted": "Διεγράφη.", "pad.modals.deleted.explanation": "Αυτό το pad έχει καταργηθεί.", + "pad.modals.rejected.explanation": "Ο διακομιστής απέρριψε ένα μήνυμα που στάλθηκε από το πρόγραμμα περιήγησής σας.", "pad.modals.disconnected": "Είστε αποσυνδεδεμένοι.", "pad.modals.disconnected.explanation": "Χάθηκε η σύνδεση με τον διακομιστή", "pad.modals.disconnected.cause": "Ο διακομιστής μπορεί να μην είναι διαθέσιμος. Παρακαλούμε ειδοποιήστε τον διαχειριστή της υπηρεσίας εάν εξακολουθεί να συμβαίνει αυτό.", diff --git a/src/locales/es.json b/src/locales/es.json index 33e80b425..b23b90814 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -2,8 +2,10 @@ "@metadata": { "authors": [ "Armando-Martin", + "DDPAT", "Dgstranz", "Fitoschido", + "Ice bulldog", "Jacobo", "Joker", "Larjona", @@ -98,7 +100,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Solo es posible importar texto sin formato o en HTML. Para obtener funciones de importación más avanzadas es necesario instalar AbiWord.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando a tu pad..", + "pad.modals.reconnecting": "Reconectando a tu pad...", "pad.modals.forcereconnect": "Forzar reconexión", "pad.modals.reconnecttimer": "Se intentará reconectar en", "pad.modals.cancel": "Cancelar", diff --git a/src/locales/fa.json b/src/locales/fa.json index 4790770fe..0d3c2011a 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "BMRG14", + "Beginneruser", "Dalba", "Ebraminio", "FarsiNevis", @@ -12,7 +13,7 @@ "الناز" ] }, - "admin.page-title": "داشبورد مدیر - اترپد", + "admin.page-title": "پیش‌خوان مدیر - اترپد", "admin_plugins": "مدیریت افزونه", "admin_plugins.available": "افزونه‌های موجود", "admin_plugins.available_not-found": "هیچ افزونه‌ای یافت نشد.", @@ -59,7 +60,7 @@ "pad.toolbar.undo.title": "باطل‌کردن (Ctrl-Z)", "pad.toolbar.redo.title": "از نو (Ctrl-Y)", "pad.toolbar.clearAuthorship.title": "پاک‌کردن رنگ‌های نویسندگی (Ctrl+Shift+C)", - "pad.toolbar.import_export.title": "درون‌ریزی/برون‌ریزی از/به قالب‌های مختلف", + "pad.toolbar.import_export.title": "درون‌ریزی/برون‌بری از/به قالب‌های مختلف پرونده", "pad.toolbar.timeslider.title": "لغزندهٔ زمان", "pad.toolbar.savedRevision.title": "ذخیره‌سازی نسخه", "pad.toolbar.settings.title": "تنظیمات", @@ -82,10 +83,10 @@ "pad.settings.language": "زبان:", "pad.settings.about": "درباره", "pad.settings.poweredBy": "قدرست‌گرفته از", - "pad.importExport.import_export": "درون‌ریزی/برون‌ریزی", + "pad.importExport.import_export": "درون‌ریزی/برون‌بری", "pad.importExport.import": "بارگذاری پرونده‌ی متنی یا سند", "pad.importExport.importSuccessful": "موفقیت آمیز بود!", - "pad.importExport.export": "برون‌ریزی این دفترچه یادداشت با قالب:", + "pad.importExport.export": "برون‌بری این دفترچه یادداشت با قالب:", "pad.importExport.exportetherpad": "اترپد", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "متن ساده", @@ -133,7 +134,7 @@ "timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت", "timeslider.toolbar.authors": "نویسندگان:", "timeslider.toolbar.authorsList": "بدون نویسنده", - "timeslider.toolbar.exportlink.title": "برون‌ریزی", + "timeslider.toolbar.exportlink.title": "برون‌بری", "timeslider.exportCurrent": "برون‌ریزی نگارش کنونی به عنوان:", "timeslider.version": "نگارش {{version}}", "timeslider.saved": "{{month}} {{day}}، {{year}} ذخیره شد", @@ -163,9 +164,9 @@ "pad.impexp.importing": "در حال درون‌ریزی...", "pad.impexp.confirmimport": "با درون‌ریزی یک پرونده نوشتهٔ کنونی دفترچه پاک می‌شود. آیا می‌خواهید ادامه دهید؟", "pad.impexp.convertFailed": "ما نمی‌توانیم این پرونده را درون‌ریزی کنیم. خواهشمندیم قالب دیگری برای سندتان انتخاب کرده یا بصورت دستی آنرا کپی کنید", - "pad.impexp.padHasData": "امکان درون‌ریز این پرونده نیست زیرا این پد تغییر کرده‌است. لطفاً در پد جدید درون‌ریزی کنید.", + "pad.impexp.padHasData": "امکان درون‌ریزی این پرونده نیست زیرا این پد تغییر کرده‌است. لطفاً در پد جدید درون‌ریزی کنید.", "pad.impexp.uploadFailed": "آپلود انجام نشد، دوباره تلاش کنید", "pad.impexp.importfailed": "درون‌ریزی انجام نشد", "pad.impexp.copypaste": "کپی پیست کنید", - "pad.impexp.exportdisabled": "برون‌ریزی با قالب {{type}} از کار افتاده است. برای جزئیات بیشتر با مدیر سیستمتان تماس بگیرید." + "pad.impexp.exportdisabled": "برون‌ریزی با قالب {{type}} از کار افتاده است. برای جزئیات بیشتر با مدیر سامانه خودتان تماس بگیرید." } diff --git a/src/locales/ff.json b/src/locales/ff.json new file mode 100644 index 000000000..dbb6c3b02 --- /dev/null +++ b/src/locales/ff.json @@ -0,0 +1,167 @@ +{ + "@metadata": { + "authors": [ + "Ibrahima Malal Sarr" + ] + }, + "admin.page-title": "Tiimtorde Jiiloowo - Etherpad", + "admin_plugins": "Toppitorde Ceŋe", + "admin_plugins.available": "Ceŋe goodaaɗe", + "admin_plugins.available_not-found": "Alaa ceŋe njiytaa.", + "admin_plugins.available_fetching": "Nana balloo…", + "admin_plugins.available_install.value": "Aaf", + "admin_plugins.available_search.placeholder": "Yiylo ceŋe aafeteeɗe", + "admin_plugins.description": "Cifol", + "admin_plugins.installed": "Ceŋe aafaaɗe", + "admin_plugins.installed_fetching": "Nana yiyloo ceŋe aafaaɗe…", + "admin_plugins.installed_nothing": "A aafaani ceŋe tawo.", + "admin_plugins.installed_uninstall.value": "Aaftu", + "admin_plugins.last-update": "Kesɗitinal cakkitiingal", + "admin_plugins.name": "Innde", + "admin_plugins.page-title": "Toppitorde ceŋe - Etherpad", + "admin_plugins.version": "Yamre", + "admin_plugins_info": "Humpito njiylaw caɗe", + "admin_plugins_info.hooks": "Logge aafaaɗe", + "admin_plugins_info.hooks_client": "Logge senngo-kuutoro", + "admin_plugins_info.hooks_server": "Logge senngo-sarworde", + "admin_plugins_info.parts": "Terɗe aafaaɗe", + "admin_plugins_info.plugins": "Ceŋe aafaaɗe", + "admin_plugins_info.page-title": "Humpito ceŋe - Etherpad", + "admin_plugins_info.version": "Yamre Etherpad", + "admin_plugins_info.version_latest": "Yamre sakkitiinde woodunde", + "admin_plugins_info.version_number": "Tonngoode yamre", + "admin_settings": "Teelte", + "admin_settings.current": "Teeltannde wonaango", + "admin_settings.current_example-devel": "Yeru tugnorgal teelte topagol", + "admin_settings.current_example-prod": "Yeru tugnorgal teelte baayino", + "admin_settings.current_restart.value": "Hurmitin Etherpad", + "admin_settings.current_save.value": "Danndu Teelte", + "admin_settings.page-title": "Teelte - Etherpad", + "index.newPad": "Alluwal Kesal", + "index.createOpenPad": "walla sos/uddit Alluwal e innde:", + "index.openPad": "Uddit paɗo woodungo e ndee innde:", + "pad.toolbar.bold.title": "Buutol (Ctrl+B)", + "pad.toolbar.italic.title": "Italik (Ctrl+I)", + "pad.toolbar.underline.title": "Diidoles (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Baar (Ctrl+5)", + "pad.toolbar.ol.title": "Doggol leemtangol(Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Doggol ngol lemtaaka", + "pad.toolbar.indent.title": "Ɓeydu taaɓal (TAB)", + "pad.toolbar.unindent.title": "Ruttu taaɓal (Shift+TAB)", + "pad.toolbar.undo.title": "Firtu (Ctrl+Z)", + "pad.toolbar.redo.title": "Waɗtu (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Momtu Noone Wallifɓe (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Jiggo/Jiggito iwde/faade mbayka piille goɗɗe", + "pad.toolbar.timeslider.title": "Daasorde tuma", + "pad.toolbar.savedRevision.title": "Dannde Baylital", + "pad.toolbar.settings.title": "Teelte", + "pad.toolbar.embed.title": "Lollin maa soomtor ngoo faɗo", + "pad.toolbar.showusers.title": "Hollu huutorɓe e ngoo faɗo", + "pad.colorpicker.save": "Danndu", + "pad.colorpicker.cancel": "Haaytin", + "pad.loading": "Nana loowa...", + "pad.noCookie": "Kukii yiytaaka. Tiiɗno yamir kukiije e wanngorde maa!\nNaatal maa e teelte maa danndetaake hakkunde njulluuji. Ɗuum ena waawi tawa Etherpad ena soomaa e nder iFrame e won e banngorɗe. Tiiɗno ƴeewto so Etherpad woni ko e domen/lesdomen iFrame yumma oo.", + "pad.permissionDenied": "A alaa yamiroore naatde e ngoo faɗo", + "pad.settings.padSettings": "Teelte Faɗo", + "pad.settings.myView": "Jiytol am", + "pad.settings.stickychat": "Yeewtere e yaynirde sahaa kala", + "pad.settings.chatandusers": "Hollu Yeewtere e Huutorɓe", + "pad.settings.colorcheck": "Noone Wallifɓe", + "pad.settings.linenocheck": "Tonngooɗe gori", + "pad.settings.rtlcheck": "Tar loowdi iwde ñaamo faya nano?", + "pad.settings.fontType": "Fannu binndi:", + "pad.settings.language": "Ɗemngal:", + "pad.settings.about": "Baɗte", + "pad.settings.poweredBy": "Dognata ko", + "pad.importExport.import_export": "Jiggo/Jiggito", + "pad.importExport.import": "Yollu fiilde binndol maa fiilannde", + "pad.importExport.importSuccessful": "Ɓennii no haaniri!", + "pad.importExport.export": "Jiggito faɗo wonaango e innde:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Binndi ɓolɓolti", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.importExport.abiword.innerHTML": "Mbaaw-ɗaa jiggaade tan ko baykaaji binndi ɓolɓolti maa HTML. Ngam heɓde fannuuji jiggagol ɓurɗi seeɓde, tiiɗno yillo install AbiWord or LibreOffice.", + "pad.modals.connected": "Seŋiima.", + "pad.modals.reconnecting": "Nana seŋoo e faɗo maa…", + "pad.modals.forcereconnect": "Forsu ceŋagol kadi", + "pad.modals.reconnecttimer": "Nana etoo seŋaade kadi ɗoo e", + "pad.modals.cancel": "Haaytin", + "pad.modals.userdup": "Uddit e henorde woɗnde", + "pad.modals.userdup.explanation": "Ngoo faɗo ellee ena udditii e ko ɓuri henorde wanngorde wootere e ndee komputere.", + "pad.modals.userdup.advice": "Seŋo kadi ngam huutoraade ndee henorde kisa.", + "pad.modals.unauth": "Yamiraaka", + "pad.modals.unauth.explanation": "Jamirooje maa mbayliima tuma nde ƴeewataa ngoo hello. Eto seŋaade kadi.", + "pad.modals.looping.explanation": "Caɗeele jokkondiral ena ngoodi faade e sarworde canngoɗinal ndee.", + "pad.modals.looping.cause": "Ma a taw ko a ceŋoriiɗo proxy maa ɓalal-jayngue.", + "pad.modals.initsocketfail": "Sorworde heɓotaako.", + "pad.modals.initsocketfail.explanation": "Horiima seŋaade e sarworde canngoɗinal ndee.", + "pad.modals.initsocketfail.cause": "Ɗuum ena gasa waɗi ɗum ko saɗeede wonnde e wanngorde maa maa ceŋol Enternet maa.", + "pad.modals.slowcommit.explanation": "Sarworde ndee jaabaaki.", + "pad.modals.slowcommit.cause": "Ɗuum ena gasa ko caɗeele ceŋagol laylaytol.", + "pad.modals.badChangeset.explanation": "Taƴtol ngol mbaɗ-ɗaa joopaama rewaani laawol to sarworde canngoɗinal ndee.", + "pad.modals.badChangeset.cause": "Ɗuum ena gasa ko teeltol sarworde ngol moƴƴaani maa geɗel ngel tijjanooka. Tiiɗno jokkondir e jiiloowo sarwiis oo, so aɗa sikki ɗuum ko juumre. Eto seŋaade kadi ngam jokkude taƴtagol maa.", + "pad.modals.corruptPad.explanation": "Faɗo ngo etoto-ɗaa naatde ngoo nattii moƴƴude.", + "pad.modals.corruptPad.cause": "Ɗuum ena gasa addi ɗum ko teeltol sarworde ngol feewaani maa geɗel ngel tijjanooka. Tiiɗno jokkondir e jiiloowo.", + "pad.modals.deleted": "Momtaama.", + "pad.modals.deleted.explanation": "Ngoo faɗo ko momtaango.", + "pad.modals.rateLimited": "Cookol happinaama.", + "pad.modals.rateLimited.explanation": "A neldii ɓatakuuje keewɗe haa ɓurti e ngoo faɗo, wadde e seŋtaama.", + "pad.modals.rejected.explanation": "Sarworde ndee riiwtii ɓatakuru ngu wanngorde maa neldunoo.", + "pad.modals.rejected.cause": "Sarworde ndee ena gasa koko hesɗitinanoo tuma nde ngon-ɗaa e ƴeewde faɗo ngoo, walla ma a taw ena woodi buggere e Etherpad. Eto loowtude hello ngoo.", + "pad.modals.disconnected": "A seŋtaama.", + "pad.modals.disconnected.explanation": "Ceŋagol to sarworde waasaama", + "pad.modals.disconnected.cause": "Sarworde ndee ena gasa heɓotaako. Tiiɗno habru jiiloowo sarwii soo so ɗum nattaani.", + "pad.share": "Lollin ngoo faɗo", + "pad.share.readonly": "Targol tan", + "pad.share.link": "Jokkorde", + "pad.share.emebdcode": "Soomtor URL", + "pad.chat": "Yeewtere", + "pad.chat.title": "Uddit yeewtere ngoo faɗo.", + "pad.chat.loadmessages": "Loow ɓatakuuje goɗɗe", + "pad.chat.stick.title": "Hedde e yaynirde yeewtere", + "pad.chat.writeMessage.placeholder": "Winndu ɗoo ɓatakuru maa", + "timeslider.followContents": "Rewindo kesɗitine loowdi faɗo", + "timeslider.pageTitle": "{{appTitle}} Daasorde tuma", + "timeslider.toolbar.returnbutton": "Rutto to faɗo", + "timeslider.toolbar.authors": "Willifɓe:", + "timeslider.toolbar.authorsList": "Alaa ballifo", + "timeslider.toolbar.exportlink.title": "Jiggito", + "timeslider.exportCurrent": "Jiggito yamre wonaande e innde:", + "timeslider.version": "Yamre {{version}}", + "timeslider.saved": "Danndaama {{month}} {{day}} {{year}}", + "timeslider.playPause": "Tar / Dartin Loowdi Faɗo", + "timeslider.backRevision": "Rutto to baylital e ngoo Faɗo", + "timeslider.forwardRevision": "Yah yeeso to baylital en ngoo Faɗo", + "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "Siilo", + "timeslider.month.february": "Colte", + "timeslider.month.march": "MBooy", + "timeslider.month.april": "Seeɗto", + "timeslider.month.may": "Duujal", + "timeslider.month.june": "Korse", + "timeslider.month.july": "Morso", + "timeslider.month.august": "Juko", + "timeslider.month.september": "Silto", + "timeslider.month.october": "Yarkomaa", + "timeslider.month.november": "Jolal", + "timeslider.month.december": "Bowte", + "timeslider.unnamedauthors": "{{num}} innitaaka {[plural(num) goo: ballifo, goɗɗo: wallifɓe]}", + "pad.savedrevs.marked": "Ndee yamre maantaama jooni ko baylital danndangal", + "pad.savedrevs.timeslider": "Aɗa waawi yiyde baylitte danndaaɗe so yillaade daasorde tuma ndee", + "pad.userlist.entername": " Naatnu innde maa", + "pad.userlist.unnamed": "innitaaki", + "pad.editbar.clearcolors": "Momtu noone wallifɓe e fiilannde ndee fof? Ɗum waawaa firteede", + "pad.impexp.importbutton": "Jiggito Jooni", + "pad.impexp.importing": "Nana Jiggitoo...", + "pad.impexp.confirmimport": "Jiggitaade fiilde maa winndito e dow winndannde wonaande ndee. Aɗa yenanaa yiɗde jokkude?", + "pad.impexp.convertFailed": "Min koriima jiggitaade ndee fiilde. Tiiɗno huutoro mbayka fiilannde ngoɗka walla natto ɗakkiraa junngo", + "pad.impexp.padHasData": "Min koriima jiggitaade ndee fiilde sabu ngoo Faɗo meeɗii wayleede, tiiɗno jiggito faade e faɗo heso", + "pad.impexp.uploadFailed": "Jollugol woorii, tiiɗno fuɗɗito", + "pad.impexp.importfailed": "Jiggitol woorii", + "pad.impexp.copypaste": "Tiiɗno natto ɗakkaa", + "pad.impexp.exportdisabled": "Jiggitaade e mbayka {{type}} koko daaƴaa. Tuuɗno jokkondir e jiiloowo yuɓɓo maa ngam ɓeydude faamade.", + "pad.impexp.maxFileSize": "Fiilde ena mawni haa ɓurti. Jokkondir e jiiloowo ngam ɓeydude ɓetol fiilde jamirangol ngam jiggeede" +} diff --git a/src/locales/fi.json b/src/locales/fi.json index c0c827e78..8859f39a8 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -17,13 +17,15 @@ "Stryn", "Tomi Toivio", "Veikk0.ma", - "VezonThunder" + "VezonThunder", + "Yupik" ] }, "admin.page-title": "Ylläpitäjän kojelauta - Etherpad", "admin_plugins": "Lisäosien hallinta", "admin_plugins.available": "Saatavilla olevat liitännäiset", "admin_plugins.available_not-found": "Lisäosia ei löytynyt.", + "admin_plugins.available_fetching": "Noudetaan…", "admin_plugins.available_install.value": "Asenna", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.description": "Kuvaus", @@ -48,6 +50,7 @@ "admin_settings": "Asetukset", "admin_settings.current": "Nykyinen kokoonpano", "admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista", + "admin_settings.current_restart.value": "Käynnistä Etherpad uudelleen", "admin_settings.current_save.value": "Tallenna asetukset", "admin_settings.page-title": "asetukset - Etherpad", "index.newPad": "Uusi muistio", @@ -166,7 +169,7 @@ "pad.impexp.importing": "Tuodaan...", "pad.impexp.confirmimport": "Tiedoston tuonti korvaa kaiken muistiossa olevan tekstin. Haluatko varmasti jatkaa?", "pad.impexp.convertFailed": "TIedoston tuonti epäonnistui. Käytä eri tiedostomuotoa tai kopioi ja liitä käsin.", - "pad.impexp.padHasData": "Tiedostoa ei voitu lisätä lehtiöön, koska lehtiötä on jo muokattu – ole hyvä ja lisää tiedosto uuteen lehtiöön", + "pad.impexp.padHasData": "Tiedostoa ei voitu lisätä muistioon, koska muistiota on jo muokattu – ole hyvä ja lisää tiedosto uuteen muistioon", "pad.impexp.uploadFailed": "Lähetys epäonnistui. Yritä uudelleen.", "pad.impexp.importfailed": "Tuonti epäonnistui", "pad.impexp.copypaste": "Kopioi ja liitä", diff --git a/src/locales/fr.json b/src/locales/fr.json index f72a887ce..8f3192107 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -65,17 +65,17 @@ "index.newPad": "Nouveau bloc-notes", "index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé :", "index.openPad": "ouvrir un bloc-note existant avec le nom :", - "pad.toolbar.bold.title": "Gras (Ctrl+B)", - "pad.toolbar.italic.title": "Italique (Ctrl+I)", - "pad.toolbar.underline.title": "Souligné (Ctrl+U)", - "pad.toolbar.strikethrough.title": "Barré (Ctrl+5)", - "pad.toolbar.ol.title": "Liste ordonnée (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Liste non ordonnée (Ctrl+Shift+L)", + "pad.toolbar.bold.title": "Gras (Ctrl + B)", + "pad.toolbar.italic.title": "Italique (Ctrl + I)", + "pad.toolbar.underline.title": "Souligné (Ctrl + U)", + "pad.toolbar.strikethrough.title": "Barré (Ctrl + 5)", + "pad.toolbar.ol.title": "Liste ordonnée (Ctrl + Maj + N)", + "pad.toolbar.ul.title": "Liste non ordonnée (Ctrl + Maj + L)", "pad.toolbar.indent.title": "Indenter (TAB)", "pad.toolbar.unindent.title": "Désindenter (Maj+TAB)", - "pad.toolbar.undo.title": "Annuler (Ctrl+Z)", - "pad.toolbar.redo.title": "Rétablir (Ctrl+Y)", - "pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)", + "pad.toolbar.undo.title": "Annuler (Ctrl + Z)", + "pad.toolbar.redo.title": "Rétablir (Ctrl + Y)", + "pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl + Maj + C)", "pad.toolbar.import_export.title": "Importer/Exporter des formats de fichiers différents", "pad.toolbar.timeslider.title": "Historique dynamique", "pad.toolbar.savedRevision.title": "Enregistrer la révision", @@ -84,7 +84,7 @@ "pad.toolbar.showusers.title": "Afficher les utilisateurs du bloc-notes", "pad.colorpicker.save": "Enregistrer", "pad.colorpicker.cancel": "Annuler", - "pad.loading": "Chargement...", + "pad.loading": "Chargement en cours...", "pad.noCookie": "Un fichier témoin (ou ''cookie'') n’a pas pu être trouvé. Veuillez autoriser les fichiers témoins dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait qu’Etherpad est inclus dans un ''iFrame'' dans certains navigateurs. Veuillez vous assurer qu’Etherpad est dans le même sous-domaine/domaine que son ''iFrame'' parent.", "pad.permissionDenied": "Vous n’êtes pas autorisé à accéder à ce bloc-notes", "pad.settings.padSettings": "Paramètres du bloc-notes", @@ -177,11 +177,11 @@ "timeslider.unnamedauthors": "{{num}} {[plural(num) one: auteur anonyme, other: auteurs anonymes ]}", "pad.savedrevs.marked": "Cette révision est maintenant marquée comme révision enregistrée", "pad.savedrevs.timeslider": "Vous pouvez voir les révisions enregistrées en ouvrant l’historique", - "pad.userlist.entername": "Entrez votre nom", + "pad.userlist.entername": "Saisissez votre nom", "pad.userlist.unnamed": "anonyme", "pad.editbar.clearcolors": "Effacer le surlignage par auteur dans tout le document ? Cette action ne peut être annulée.", "pad.impexp.importbutton": "Importer maintenant", - "pad.impexp.importing": "Import en cours...", + "pad.impexp.importing": "Importation en cours...", "pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du bloc-notes. Êtes-vous sûr de vouloir le faire ?", "pad.impexp.convertFailed": "Nous ne pouvons pas importer ce fichier. Veuillez utiliser un autre format de document ou faire manuellement un copier/coller du texte brut", "pad.impexp.padHasData": "Nous n’avons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié ; veuillez l’importer vers un nouveau bloc-notes.", diff --git a/src/locales/gl.json b/src/locales/gl.json index c9593f043..352737d39 100644 --- a/src/locales/gl.json +++ b/src/locales/gl.json @@ -48,13 +48,13 @@ "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista sen ordenar (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Sangría (TAB)", - "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", + "pad.toolbar.indent.title": "Sangrar (TAB)", + "pad.toolbar.unindent.title": "Sen sangrar (Maiús.+TAB)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás participantes (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", - "pad.toolbar.timeslider.title": "Liña do tempo", + "pad.toolbar.timeslider.title": "Cronoloxía", "pad.toolbar.savedRevision.title": "Gardar a revisión", "pad.toolbar.settings.title": "Axustes", "pad.toolbar.embed.title": "Compartir e incorporar este documento", @@ -65,9 +65,9 @@ "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai", "pad.permissionDenied": "Non tes permiso para acceder a este documento", "pad.settings.padSettings": "Configuracións do documento", - "pad.settings.myView": "A miña vista", + "pad.settings.myView": "Ver", "pad.settings.stickychat": "Chat sempre visible", - "pad.settings.chatandusers": "Mostrar o chat e os usuarios", + "pad.settings.chatandusers": "Mostrar o chat e as usuarias", "pad.settings.colorcheck": "Cores de identificación", "pad.settings.linenocheck": "Números de liña", "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?", @@ -88,7 +88,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instala AbiWord.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando co teu documento...", + "pad.modals.reconnecting": "Reconectando co seu documento...", "pad.modals.forcereconnect": "Forzar a reconexión", "pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.cancel": "Cancelar", @@ -127,10 +127,10 @@ "pad.chat.stick.title": "Pegar a conversa á pantalla", "pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe", "timeslider.followContents": "Segue as actualizacións do contido", - "timeslider.pageTitle": "Liña do tempo de {{appTitle}}", + "timeslider.pageTitle": "Cronoloxía de {{appTitle}}", "timeslider.toolbar.returnbutton": "Volver ao documento", - "timeslider.toolbar.authors": "Autores:", - "timeslider.toolbar.authorsList": "Ningún autor", + "timeslider.toolbar.authors": "Editoras:", + "timeslider.toolbar.authorsList": "Sen Editoras", "timeslider.toolbar.exportlink.title": "Exportar", "timeslider.exportCurrent": "Exportar a versión actual en formato:", "timeslider.version": "Versión {{version}}", @@ -151,12 +151,12 @@ "timeslider.month.october": "outubro", "timeslider.month.november": "novembro", "timeslider.month.december": "decembro", - "timeslider.unnamedauthors": "{{num}} {[plural(num) one: autor anónimo, other: autores anónimos ]}", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: editora anónima, other: editora anónima ]}", "pad.savedrevs.marked": "Esta revisión está agora marcada como revisión gardada", - "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", + "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a cronoloxía", "pad.userlist.entername": "Insira o seu nome", "pad.userlist.unnamed": "anónimo", - "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar", + "pad.editbar.clearcolors": "Eliminar as cores relativas ás participantes en todo o documento? Non se poderán recuperar", "pad.impexp.importbutton": "Importar agora", "pad.impexp.importing": "Importando...", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", diff --git a/src/locales/he.json b/src/locales/he.json index b84e47cc6..458ad0ace 100644 --- a/src/locales/he.json +++ b/src/locales/he.json @@ -55,7 +55,7 @@ "pad.toolbar.undo.title": "ביטול (Ctrl-Z)", "pad.toolbar.redo.title": "ביצוע מחדש", "pad.toolbar.clearAuthorship.title": "ניקוי צבעי כותבים (Ctrl-Shift-C)", - "pad.toolbar.import_export.title": "ייבוא/ייצוא בתסדירי קבצים שונים", + "pad.toolbar.import_export.title": "יבוא/יצוא בתסדירי קבצים שונים", "pad.toolbar.timeslider.title": "גולל זמן", "pad.toolbar.savedRevision.title": "שמירת גרסה", "pad.toolbar.settings.title": "הגדרות", @@ -78,17 +78,17 @@ "pad.settings.language": "שפה:", "pad.settings.about": "על אודות", "pad.settings.poweredBy": "מופעל על גבי", - "pad.importExport.import_export": "ייבוא/ייצוא", + "pad.importExport.import_export": "יבוא/יצוא", "pad.importExport.import": "העלאת כל קובץ טקסט או מסמך", "pad.importExport.importSuccessful": "זה עבד!", - "pad.importExport.export": "ייצוא הפנקס הנוכחי בתור:", + "pad.importExport.export": "יצוא הפנקס הנוכחי בתור:", "pad.importExport.exportetherpad": "את'רפד", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "טקסט רגיל", "pad.importExport.exportword": "מיקרוסופט וורד", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "באפשרותך לייבא מטקסט פשוט או מ־HTML. לאפשרויות ייבוא מתקדמות יותר יש להתקין AbiWord או LibreOffice.", + "pad.importExport.abiword.innerHTML": "באפשרותך לייבא מטקסט פשוט או מ־HTML. לאפשרויות יבוא מתקדמות יותר יש להתקין AbiWord או LibreOffice.", "pad.modals.connected": "מחובר.", "pad.modals.reconnecting": "מתבצע חיבור מחדש למחברת שלך…", "pad.modals.forcereconnect": "לכפות חיבור מחדש", @@ -133,8 +133,8 @@ "timeslider.toolbar.returnbutton": "חזרה אל הפנקס", "timeslider.toolbar.authors": "כותבים:", "timeslider.toolbar.authorsList": "אין כותבים", - "timeslider.toolbar.exportlink.title": "ייצוא", - "timeslider.exportCurrent": "ייצוא הגרסה הנוכחית בתור:", + "timeslider.toolbar.exportlink.title": "יצוא", + "timeslider.exportCurrent": "יצוא הגרסה הנוכחית בתור:", "timeslider.version": "גרסה {{version}}", "timeslider.saved": "נשמרה ב־{{day}} ב{{month}} {{year}}", "timeslider.playPause": "לנגן / לעצור את תוכן הפנקס", @@ -160,13 +160,13 @@ "pad.userlist.unnamed": "ללא שם", "pad.editbar.clearcolors": "לנקות צבעים לסימון כותבים בכל המסמך? זו פעולה בלתי הפיכה", "pad.impexp.importbutton": "לייבא כעת", - "pad.impexp.importing": "מתבצע ייבוא…", - "pad.impexp.confirmimport": "ייבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. להמשיך?", + "pad.impexp.importing": "מתבצע יבוא...", + "pad.impexp.confirmimport": "יבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. להמשיך?", "pad.impexp.convertFailed": "לא הצלחנו לייבא את הקובץ הזה. נא להשתמש בתסדיר מסמך שונה או להעתיק ולהדביק ידנית", "pad.impexp.padHasData": "לא הצלחנו לייבא את הקובץ הזה, כי בפנקס הזה כבר יש שינויים. נא לייבא לפנקס חדש.", "pad.impexp.uploadFailed": "ההעלאה נכשלה, נא לנסות שוב", - "pad.impexp.importfailed": "הייבוא נכשל", + "pad.impexp.importfailed": "היבוא נכשל", "pad.impexp.copypaste": "נא להעתיק ולהדביק", - "pad.impexp.exportdisabled": "ייצוא בתסדיר {{type}} אינו פעיל. מנהל המערכת שלך יוכל לספר לך על זה עוד פרטים.", + "pad.impexp.exportdisabled": "יצוא בתסדיר {{type}} אינו פעיל. מנהל המערכת שלך יוכל לספר לך על זה עוד פרטים.", "pad.impexp.maxFileSize": "הקובץ גדול מדי. נא ליצור קשר עם הנהלת האתר כדי להגדיל את הגודל המרבי שמותר לייבא." } diff --git a/src/locales/hr.json b/src/locales/hr.json index f8b400bba..77b10674a 100644 --- a/src/locales/hr.json +++ b/src/locales/hr.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Bugoslav", - "Hmxhmx" + "Hmxhmx", + "Ponor" ] }, "index.newPad": "Novi blokić", @@ -89,7 +90,7 @@ "pad.chat.title": "Otvori čavrljanje uz ovaj blokić.", "pad.chat.loadmessages": "Učitaj više poruka", "pad.chat.stick.title": "Prilijepi razgovor na zaslon", - "pad.chat.writeMessage.placeholder": "Napišite Vašu poruku ovdje", + "pad.chat.writeMessage.placeholder": "Ovdje napišite svoju poruku", "timeslider.followContents": "Prati ažuriranja sadržaja blokića", "timeslider.pageTitle": "{{appTitle}} Vremenska lenta", "timeslider.toolbar.returnbutton": "Vrati se natrag na blokić", diff --git a/src/locales/hsb.json b/src/locales/hsb.json index 54ca439ef..62f70740f 100644 --- a/src/locales/hsb.json +++ b/src/locales/hsb.json @@ -4,8 +4,42 @@ "Michawiki" ] }, + "admin.page-title": "Administratorowa deska – Etherpad", + "admin_plugins": "Zrjadowak tykačow", + "admin_plugins.available": "K dispoziciji stejace tykače", + "admin_plugins.available_not-found": "Žane tykače namakane.", + "admin_plugins.available_fetching": "Wobstaruje so …", + "admin_plugins.available_install.value": "Instalować", + "admin_plugins.available_search.placeholder": "Tykače za instalaciju pytać", + "admin_plugins.description": "Wopisanje", + "admin_plugins.installed": "Instalowane tykače", + "admin_plugins.installed_fetching": "Instalowane tykače so wobstaruja …", + "admin_plugins.installed_nothing": "Hišće njejsće tykače instalował.", + "admin_plugins.installed_uninstall.value": "Wotinstalować", + "admin_plugins.last-update": "Poslednja aktualizacija", + "admin_plugins.name": "Mjeno", + "admin_plugins.page-title": "Zrjadowak tykačow – Etherpad", + "admin_plugins.version": "Wersija", + "admin_plugins_info": "Informacije wo rozrisanju problemow", + "admin_plugins_info.hooks": "Instalowane hoki", + "admin_plugins_info.hooks_client": "Hoki ze strony klienta", + "admin_plugins_info.hooks_server": "Hoki ze strony serwera", + "admin_plugins_info.parts": "Instalowane dźěle", + "admin_plugins_info.plugins": "Instalowane tykače", + "admin_plugins_info.page-title": "Tykačowe informacije – Ehterpad", + "admin_plugins_info.version": "Wersija Etherpad", + "admin_plugins_info.version_latest": "Najnowša wersija", + "admin_plugins_info.version_number": "Wersijowe čisło", + "admin_settings": "Nastajenja", + "admin_settings.current": "Aktualna konfiguracija", + "admin_settings.current_example-devel": "Přikładowa předłoha wuwiwanskich nastajenjow", + "admin_settings.current_example-prod": "Přikładowa předłoha produkciskich nastajenjow", + "admin_settings.current_restart.value": "Etherpad znowa startować", + "admin_settings.current_save.value": "Nastajenja składować", + "admin_settings.page-title": "Nastajenja – Etherpad", "index.newPad": "Nowy zapisnik", "index.createOpenPad": "abo wutwor/wočiń zapisnik z mjenom:", + "index.openPad": "wočińće eksistowacy Pad z mjenom:", "pad.toolbar.bold.title": "Tučny (Strg-B)", "pad.toolbar.italic.title": "Kursiwny (Strg-I)", "pad.toolbar.underline.title": "Podšmórnyć (Strg-U)", @@ -26,7 +60,7 @@ "pad.colorpicker.save": "Składować", "pad.colorpicker.cancel": "Přetorhnyć", "pad.loading": "Začituje so...", - "pad.noCookie": "Plack njeje so namakał. Prošu dopušćće placki w swojim wobhladowaku!", + "pad.noCookie": "Plack njeje so namakał. Prošu dowolće placki w swojim wobhladowaku! Waše posedźenje a nastajenja so mjez dwěmaj wopytomaj njeskładuja. To móže so stać, hdyž Etherpad je w někotrych wobhladowakach w iFrame wobsahowany. Prošu zawěsćće, zo Etherpad je na samsnej poddomenje/domenje kaž nadrjadowany iFrame", "pad.permissionDenied": "Nimaće prawo za přistup na tutón zapisnik.", "pad.settings.padSettings": "Nastajenja zapisnika", "pad.settings.myView": "Mój napohlad", @@ -38,6 +72,8 @@ "pad.settings.fontType": "Pismowa družina:", "pad.settings.fontType.normal": "Normalny", "pad.settings.language": "Rěč:", + "pad.settings.about": "Wo", + "pad.settings.poweredBy": "Spěchowany wot", "pad.importExport.import_export": "Import/Eksport", "pad.importExport.import": "Tekstowu dataju abo dokument nahrać", "pad.importExport.importSuccessful": "Wuspěšny!", @@ -48,10 +84,12 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Móžeš jenož z formatow luteho teksta abo z HTML-formata importować. Za bóle rozšěrjene importowanske funkcije instaluj prošu Abiword.", + "pad.importExport.abiword.innerHTML": "Móžeš jenož z formatow luteho teksta abo z HTML-formata importować. Za bóle rozšěrjene importowe funkcije instaluj prošu Abiword abo LibreOffice.", "pad.modals.connected": "Zwjazany.", "pad.modals.reconnecting": "Zwjazuje so znowa z twojim zapisnikom...", "pad.modals.forcereconnect": "Znowa zwjazać", + "pad.modals.reconnecttimer": "Spytaj so znowa zwjazać w", + "pad.modals.cancel": "Přetorhnyć", "pad.modals.userdup": "W druhim woknje wočinjeny", "pad.modals.userdup.explanation": "Zda so, zo tutón zapisnik je so we wjace hač jednym woknje wobhladowaka na tutym ličaku wočinił.", "pad.modals.userdup.advice": "Zwjazaj znowa, zo by tute wokno město toho wužiwał.", @@ -70,6 +108,10 @@ "pad.modals.corruptPad.cause": "To je so snano wopačneje serweroweje konfiguracije dla abo druheho njewočakowaneho zadźerženja dla stało. Prošu staj so ze słužbowym administratorom do zwiska.", "pad.modals.deleted": "Zhašany.", "pad.modals.deleted.explanation": "Tutón zapisnik je so wotstronił.", + "pad.modals.rateLimited": "Wobmjezowana rata.", + "pad.modals.rateLimited.explanation": "Sće přewjele powěsćow na zapisnik pósłał, tohodla je so zwisk dźělił.", + "pad.modals.rejected.explanation": "Serwer je powěsć wotpokazał, kotraž je so přez waš wobhladowak pósłał.", + "pad.modals.rejected.cause": "Serwer je so snano zaktualizował, mjeztym zo sy sej zapisnik wobhladał, abo je snano zmylk w Etherpad. Spytaj stronu znowa začitać.", "pad.modals.disconnected": "Zwisk je přetorhnjeny.", "pad.modals.disconnected.explanation": "Zwisk ze serwerom je so zhubił", "pad.modals.disconnected.cause": "Serwer k dispoziciji njesteji. Prošu informuj słužboweho administratora, jeli to so dale stawa.", @@ -80,6 +122,9 @@ "pad.chat": "Chat", "pad.chat.title": "Chat za tutón zapisnik wočinić", "pad.chat.loadmessages": "Dalše powěsće začitać", + "pad.chat.stick.title": "Chat k wobrazowce připjeć", + "pad.chat.writeMessage.placeholder": "Pisajće swoju powěsć tu", + "timeslider.followContents": "Aktualizacijam wobsaha zapisnika slědować", "timeslider.pageTitle": "{{appTitle}} - wersijowa historija", "timeslider.toolbar.returnbutton": "Wróćo k zapisnikej", "timeslider.toolbar.authors": "Awtorojo:", @@ -88,7 +133,7 @@ "timeslider.exportCurrent": "Aktualnu wersiju eksportować jako:", "timeslider.version": "Wersija {{version}}", "timeslider.saved": "Składowany {{day}}. {{month}} {{year}}", - "timeslider.playPause": "Wobdźěłanje wothrać/pawzować", + "timeslider.playPause": "Wobsah zapisnika wothrać/pawsěrować", "timeslider.backRevision": "Wo jednu wersiju w tutym dokumenće wróćo hić", "timeslider.forwardRevision": "Wo jednu wersiju w tutym dokumenće doprědka hić", "timeslider.dateformat": "{{day}}. {{month}} {{year}} {{hours}}:{{minutes}}:{{seconds}}", @@ -109,7 +154,7 @@ "pad.savedrevs.timeslider": "Móžeš sej składowane wersije wobhladować, wopytujo historiju dokumenta.", "pad.userlist.entername": "Zapodaj swoje mjeno", "pad.userlist.unnamed": "bjez mjena", - "pad.editbar.clearcolors": "Awtorowe barby w cyłym dokumenće zhašeć?", + "pad.editbar.clearcolors": "Awtorowe barby w cyłym dokumenće zhašeć? To njeda so cofnyć", "pad.impexp.importbutton": "Nětko importować", "pad.impexp.importing": "Importuje so...", "pad.impexp.confirmimport": "Importowanje dataje přepisa aktualny tekst zapisnika. Chceš woprawdźe pokročować?", @@ -118,5 +163,6 @@ "pad.impexp.uploadFailed": "Nahraće njeje so poradźiło, prošu spytaj hišće raz", "pad.impexp.importfailed": "Import njeje so poradźiło", "pad.impexp.copypaste": "Prošu kopěrować a zasadźić", - "pad.impexp.exportdisabled": "Eksport jako format {{type}} je znjemóžnjeny. Prošu staj so ze swojim systemowym administratorom za podrobnosće do zwiska." + "pad.impexp.exportdisabled": "Eksport jako format {{type}} je znjemóžnjeny. Prošu staj so ze swojim systemowym administratorom za podrobnosće do zwiska.", + "pad.impexp.maxFileSize": "Dataja je přewulka. Stajće so ze swojim sydłowym administratorom do zwiska, zo by dowolenu datajowu wulkosć za import powyšił" } diff --git a/src/locales/hy.json b/src/locales/hy.json index 66edde719..a842a6c24 100644 --- a/src/locales/hy.json +++ b/src/locales/hy.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Armenoid", "Kareyac" ] }, @@ -26,7 +27,7 @@ "pad.settings.fontType": "Տառատեսակի տեսակը", "pad.settings.language": "Լեզու", "pad.importExport.import_export": "Ներմուծում/արտահանում", - "pad.importExport.import": "Բեռնել ցանկացած տեքստային ֆայլը կամ փաստաթուղթ", + "pad.importExport.import": "Վերբեռնել ցանկացած տեքստային նիշք կամ փաստաթուղթ", "pad.importExport.importSuccessful": "Հաջողություն", "pad.importExport.export": "Արտահանել ընթացիկ փաստաթուղթ է որպես", "pad.importExport.exportplain": "Պարզ տեքստ", diff --git a/src/locales/ia.json b/src/locales/ia.json index ea5db836b..6d0c4f283 100644 --- a/src/locales/ia.json +++ b/src/locales/ia.json @@ -4,8 +4,42 @@ "McDutchie" ] }, + "admin.page-title": "Pannello administrative – Etherpad", + "admin_plugins": "Gestor de plug-ins", + "admin_plugins.available": "Plug-ins disponibile", + "admin_plugins.available_not-found": "Necun plug-in trovate.", + "admin_plugins.available_fetching": "Obtention…", + "admin_plugins.available_install.value": "Installar", + "admin_plugins.available_search.placeholder": "Cercar plug-ins a installar", + "admin_plugins.description": "Description", + "admin_plugins.installed": "Plug-ins installate", + "admin_plugins.installed_fetching": "Obtene plug-ins installate…", + "admin_plugins.installed_nothing": "Tu non ha ancora installate alcun plug-in.", + "admin_plugins.installed_uninstall.value": "Disinstallar", + "admin_plugins.last-update": "Ultime actualisation", + "admin_plugins.name": "Nomine", + "admin_plugins.page-title": "Gestor de plug-ins – Etherpad", + "admin_plugins.version": "Version", + "admin_plugins_info": "Resolution de problemas", + "admin_plugins_info.hooks": "Uncinos installate", + "admin_plugins_info.hooks_client": "Uncinos al latere del cliente", + "admin_plugins_info.hooks_server": "Uncinos al latere del servitor", + "admin_plugins_info.parts": "Partes installate", + "admin_plugins_info.plugins": "Plug-ins installate", + "admin_plugins_info.page-title": "Information sur le plug-in – Etherpad", + "admin_plugins_info.version": "Version de Etherpad", + "admin_plugins_info.version_latest": "Ultime version disponibile", + "admin_plugins_info.version_number": "Numero de version", + "admin_settings": "Parametros", + "admin_settings.current": "Configuration actual", + "admin_settings.current_example-devel": "Exemplo de patrono de parametros de disveloppamento", + "admin_settings.current_example-prod": "Exemplo de patrono de parametros de production", + "admin_settings.current_restart.value": "Reinitiar Etherpad", + "admin_settings.current_save.value": "Salveguardar parametros", + "admin_settings.page-title": "Parametros – Etherpad", "index.newPad": "Nove pad", "index.createOpenPad": "o crear/aperir un pad con le nomine:", + "index.openPad": "aperir un Pad existente con le nomine:", "pad.toolbar.bold.title": "Grasse (Ctrl-B)", "pad.toolbar.italic.title": "Italic (Ctrl-I)", "pad.toolbar.underline.title": "Sublinear (Ctrl-U)", @@ -38,6 +72,8 @@ "pad.settings.fontType": "Typo de litteras:", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Lingua:", + "pad.settings.about": "A proposito", + "pad.settings.poweredBy": "Actionate per", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Incargar qualcunque file de texto o documento", "pad.importExport.importSuccessful": "Succedite!", @@ -72,6 +108,10 @@ "pad.modals.corruptPad.cause": "Isto pote esser debite a un configuration incorrecte del servitor o a alcun altere comportamento impreviste. Per favor contacta le administrator del servicio.", "pad.modals.deleted": "Delite.", "pad.modals.deleted.explanation": "Iste pad ha essite removite.", + "pad.modals.rateLimited": "Frequentia limitate.", + "pad.modals.rateLimited.explanation": "Tu ha inviate troppo de messages a iste pad, dunque illo te ha disconnectite.", + "pad.modals.rejected.explanation": "Le servitor ha rejectate un message que tu navigator ha inviate.", + "pad.modals.rejected.cause": "Es possibile que le servitor ha essite actualisate durante que tu legeva le pad, o que il ha un falta in Etherpad. Tenta recargar le pagina.", "pad.modals.disconnected": "Tu ha essite disconnectite.", "pad.modals.disconnected.explanation": "Le connexion al servitor ha essite perdite.", "pad.modals.disconnected.cause": "Le servitor pote esser indisponibile. Per favor notifica le administrator del servicio si isto continua a producer se.", @@ -82,6 +122,9 @@ "pad.chat": "Chat", "pad.chat.title": "Aperir le chat pro iste pad.", "pad.chat.loadmessages": "Cargar plus messages", + "pad.chat.stick.title": "Ancorar le chat sur le schermo", + "pad.chat.writeMessage.placeholder": "Scribe tu message hic", + "timeslider.followContents": "Sequer le actualisationes de contento del pad", "timeslider.pageTitle": "Glissa-tempore pro {{appTitle}}", "timeslider.toolbar.returnbutton": "Retornar al pad", "timeslider.toolbar.authors": "Autores:", @@ -120,5 +163,6 @@ "pad.impexp.uploadFailed": "Le incargamento ha fallite. Per favor reproba.", "pad.impexp.importfailed": "Importation fallite", "pad.impexp.copypaste": "Per favor copia e colla", - "pad.impexp.exportdisabled": "Le exportation in formato {{type}} es disactivate. Per favor contacta le administrator del systema pro detalios." + "pad.impexp.exportdisabled": "Le exportation in formato {{type}} es disactivate. Per favor contacta le administrator del systema pro detalios.", + "pad.impexp.maxFileSize": "Le file es troppo grande. Contacta le administrator de tu sito pro augmentar le grandor de file autorisate pro importation." } diff --git a/src/locales/id.json b/src/locales/id.json index b67bcfc65..49b8f34fa 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -3,9 +3,18 @@ "authors": [ "Bennylin", "IvanLanin", - "Marwan Mohamad" + "Marwan Mohamad", + "Veracious" ] }, + "admin.page-title": "Dasbor Pengurus - Etherpad", + "admin_plugins": "Manajer plugin", + "admin_plugins.available": "Plugin yang tersedia", + "admin_plugins.available_install.value": "Instal", + "admin_plugins.version": "Versi", + "admin_settings": "Pengaturan", + "admin_settings.current_save.value": "Simpan pengaturan", + "admin_settings.page-title": "Pengaturan - Etherpad", "index.newPad": "Pad baru", "index.createOpenPad": "atau buat/buka Pad dengan nama:", "pad.toolbar.bold.title": "Tebal (Ctrl-B)", @@ -39,6 +48,7 @@ "pad.settings.rtlcheck": "Membaca dari kanan ke kiri?", "pad.settings.fontType": "Jenis fonta:", "pad.settings.language": "Bahasa:", + "pad.settings.about": "Tentang", "pad.importExport.import_export": "Impor/Ekspor", "pad.importExport.import": "Unggah setiap berkas teks atau dokumen", "pad.importExport.importSuccessful": "Berhasil!", diff --git a/src/locales/is.json b/src/locales/is.json index 15d8cd5b6..0ff9ec5d6 100644 --- a/src/locales/is.json +++ b/src/locales/is.json @@ -5,8 +5,39 @@ "Sveinn í Felli" ] }, + "admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad", + "admin_plugins": "Stýring viðbóta", + "admin_plugins.available": "Tiltækar viðbætur", + "admin_plugins.available_not-found": "Engar viðbætur fundust.", + "admin_plugins.available_fetching": "Sæki…", + "admin_plugins.available_install.value": "Setja upp", + "admin_plugins.available_search.placeholder": "Leita að viðbótum til uppsetningar", + "admin_plugins.description": "Lýsing", + "admin_plugins.installed": "Uppsettar viðbætur", + "admin_plugins.installed_fetching": "Sæki uppsettar viðbætur…", + "admin_plugins.installed_nothing": "Þú hefur ekki ennþá sett upp neinarar viðbætur.", + "admin_plugins.installed_uninstall.value": "Taka út", + "admin_plugins.last-update": "Síðast uppfært", + "admin_plugins.name": "Heiti", + "admin_plugins.page-title": "Stýring viðbóta - Etherpad", + "admin_plugins.version": "Útgáfa", + "admin_plugins_info": "Upplýsingar fyrir úrræðaleit", + "admin_plugins_info.parts": "Uppsettir hlutar", + "admin_plugins_info.plugins": "Uppsettar viðbætur", + "admin_plugins_info.page-title": "Upplýsingar um viðbætur - Etherpad", + "admin_plugins_info.version": "Útgáfa Etherpad", + "admin_plugins_info.version_latest": "Nýjasta tiltæka útgáfa", + "admin_plugins_info.version_number": "Útgáfunúmer", + "admin_settings": "Stillingar", + "admin_settings.current": "Fyrirliggjandi uppsetning", + "admin_settings.current_example-devel": "Sniðmát með dæmigerðum þróunarstillingum", + "admin_settings.current_example-prod": "Sniðmát með dæmigerðum keyrslustillingum", + "admin_settings.current_restart.value": "Endurræsa Etherpad", + "admin_settings.current_save.value": "Vista stillingar", + "admin_settings.page-title": "Stillingar - Etherpad", "index.newPad": "Ný skrifblokk", "index.createOpenPad": "eða búa til/opna skrifblokk með heitinu:", + "index.openPad": "opna skrifblokk með heitinu:", "pad.toolbar.bold.title": "Feitletrað (Ctrl+B)", "pad.toolbar.italic.title": "Skáletrað (Ctrl+I)", "pad.toolbar.underline.title": "Undirstrikað (Ctrl+U)", @@ -27,7 +58,7 @@ "pad.colorpicker.save": "Vista", "pad.colorpicker.cancel": "Hætta við", "pad.loading": "Hleð inn...", - "pad.noCookie": "Smákaka fannst ekki. Þú verður að leyfa smákökur í vafranum þínum!", + "pad.noCookie": "Vefkaka fannst ekki. Þú verður að leyfa vefkökur í vafranum þínum! Setan þín og stillingar verða ekki vistaðar á milli heimsókna. Þetta gæti stafað af því Etherpad sé innan í iFrame-ramma í sumum vöfrum. Gakktu úr skugga um að Etherpad sé á sama undirléni/léni eins og yfir-iFrame-ramminn", "pad.permissionDenied": "Þú hefur ekki réttindi til að nota þessa skrifblokk", "pad.settings.padSettings": "Stillingar skrifblokkar", "pad.settings.myView": "Mitt yfirlit", @@ -53,7 +84,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið eða LibreOffice.", "pad.modals.connected": "Tengt.", - "pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni...", + "pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni…", "pad.modals.forcereconnect": "Þvinga endurtengingu", "pad.modals.reconnecttimer": "Reyni aftur að tengjast eftir", "pad.modals.cancel": "Hætta við", @@ -75,6 +106,9 @@ "pad.modals.corruptPad.cause": "Þetta gæti verið vegna rangrar uppsetningar á þjóninum eða annarar óvæntrar hegðunar. Hafðu samband við stjórnanda þjónustunnar.", "pad.modals.deleted": "Eytt.", "pad.modals.deleted.explanation": "Þessi skrifblokk hefur verið fjarlægð.", + "pad.modals.rateLimited": "Með takmörkum.", + "pad.modals.rateLimited.explanation": "Þú hefur sent of mörg skilaboð á þessa skrifblokk, þannig að hún aftengdi þig.", + "pad.modals.rejected.explanation": "Þjónninn hafnaði skilaboðum sem vafrinn þinn sendi.", "pad.modals.disconnected": "Þú hefur verið aftengd(ur).", "pad.modals.disconnected.explanation": "Missti tengingu við miðlara", "pad.modals.disconnected.cause": "Miðlarinn gæti verið ekki tiltækur. Láttu kerfisstjóra vita ef þetta heldur áfram að gerast.", @@ -87,6 +121,7 @@ "pad.chat.loadmessages": "Hlaða inn fleiri skeytum", "pad.chat.stick.title": "Festa spjallið á skjáinn", "pad.chat.writeMessage.placeholder": "Skrifaðu skilaboðin þín hér", + "timeslider.followContents": "Fylgja uppfærslum á efni skrifblokkar", "timeslider.pageTitle": "Tímalína {{appTitle}}", "timeslider.toolbar.returnbutton": "Fara til baka í skrifblokk", "timeslider.toolbar.authors": "Höfundar:", @@ -125,5 +160,6 @@ "pad.impexp.uploadFailed": "Sending mistókst, endilega reyndu aftur", "pad.impexp.importfailed": "Innflutningur mistókst", "pad.impexp.copypaste": "Afritaðu og límdu", - "pad.impexp.exportdisabled": "Útflutningur á {{type}} sniði er óvirkur. Hafðu samband við kerfisstjóra til að fá frekari aðstoð." + "pad.impexp.exportdisabled": "Útflutningur á {{type}} sniði er óvirkur. Hafðu samband við kerfisstjóra til að fá frekari aðstoð.", + "pad.impexp.maxFileSize": "Of stór skrá. Hafðu samband við kerfisstjóra til að láta auka leyfilega stærð skráa í innflutningi" } diff --git a/src/locales/it.json b/src/locales/it.json index cd3af6a97..0774f6ad5 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -8,15 +8,21 @@ "Macofe", "Muxator", "Nivit", + "VamosErik88", "Vituzzu" ] }, + "admin.page-title": "Pannello amministrativo - Etherpad", + "admin_plugins": "Gestione plugin", + "admin_plugins.available": "Plugin disponibili", + "admin_plugins.available_not-found": "Nessun plugin trovato.", "admin_plugins.available_install.value": "Installa", "admin_plugins.installed_uninstall.value": "Disinstalla", "admin_plugins.last-update": "Ultimo aggiornamento", "admin_plugins.name": "Nome", "admin_plugins.version": "Versione", "admin_settings": "Impostazioni", + "admin_settings.current_save.value": "Salva impostazioni", "index.newPad": "Nuovo pad", "index.createOpenPad": "o crea/apre un pad con il nome:", "index.openPad": "apri un Pad esistente col nome:", diff --git a/src/locales/kn.json b/src/locales/kn.json new file mode 100644 index 000000000..f1f109c07 --- /dev/null +++ b/src/locales/kn.json @@ -0,0 +1,65 @@ +{ + "@metadata": { + "authors": [ + "Nayvik", + "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ" + ] + }, + "admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್‌ಗಳು", + "admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್‌ಗಳು ಸಿಗಲಿಲ್ಲ", + "admin_plugins.available_fetching": "ಪಡೆಯಲಾಗುತ್ತಿದೆ...", + "admin_plugins.available_install.value": "ಅಳವಡಿಸು", + "admin_plugins.available_search.placeholder": "ಅಳವಡಿಸಲು ಪ್ಲಗಿನ್‌ಗಳನ್ನು ಹುಡುಕಿ", + "admin_plugins.description": "ವಿವರ", + "admin_plugins.installed": "ಅಳವಡಿಸಿದ ಪ್ಲಗಿನ್‌ಗಳು", + "admin_plugins.installed_fetching": "ಅಳವಡಿಸಿದ ಪ್ಲಗಿನ್‌ಗಳನ್ನು ಪಡೆಯಲಾಗುತ್ತಿದೆ...", + "admin_plugins.installed_nothing": "ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಪ್ಲಗಿನ್‌ಗಳನ್ನು ಅಳವಡಿಸಿಲ್ಲ.", + "admin_plugins.name": "ಹೆಸರು", + "admin_plugins.version": "ಆವೃತ್ತಿ", + "admin_plugins_info.plugins": "ಅಳವಡಿಸಿದ ಪ್ಲಗಿನ್‌ಗಳು", + "admin_plugins_info.page-title": "ಪ್ಲಗಿನ್ ಮಾಹಿತಿ - ಈಥರ್‌ಪ್ಯಾಡ್", + "admin_plugins_info.version": "ಈಥರ್‌ಪ್ಯಾಡ್ ಆವೃತ್ತಿ", + "admin_plugins_info.version_number": "ಆವೃತ್ತಿ ಸಂಖ್ಯೆ", + "admin_settings": "ವ್ಯವಸ್ಥೆಗಳು", + "admin_settings.page-title": "ವ್ಯವಸ್ಥೆಗಳು - ಈಥರ್‌ಪ್ಯಾಡ್", + "index.newPad": "ಹೊಸ ಪ್ಯಾಡ್", + "index.createOpenPad": "ಅಥವಾ ಈ ಹೆಸರಿನ ಪ್ಯಾಡನ್ನು ಸೃಷ್ಟಿಸು/ತೆರೆ:", + "pad.toolbar.bold.title": "ದಟ್ಟ (Ctrl-B)", + "pad.toolbar.italic.title": "ಓರೆ (Ctrl-I)", + "pad.toolbar.underline.title": "ಕೆಳಗೆರೆ (Ctrl-U)", + "pad.toolbar.settings.title": "ವ್ಯವಸ್ಥೆಗಳು", + "pad.colorpicker.save": "ಉಳಿಸಿ", + "pad.colorpicker.cancel": "ರದ್ದು ಮಾಡು", + "pad.loading": "ತುಂಬಿಸಲಾಗುತ್ತಿದೆ....", + "pad.settings.myView": "ನನ್ನ ನೋಟ", + "pad.settings.linenocheck": "ಗೆರೆ ಸಂಖ್ಯೆಗಳು", + "pad.settings.language": "ಭಾಷೆ:", + "pad.settings.about": "ಕುರಿತು", + "pad.importExport.import_export": "ಆಮದು/ರಫ್ತು", + "pad.importExport.importSuccessful": "ಯಶಸ್ವಿ!", + "pad.importExport.exportetherpad": "ಈಥರ್‌ಪ್ಯಾಡ್", + "pad.importExport.exporthtml": "ಎಚ್‍ಟಿಎಂಎಲ್", + "pad.importExport.exportplain": "ಸಾದಾ ಪಠ್ಯ", + "pad.importExport.exportword": "ಮೈಕ್ರೋಸಾಫ್ಟ್ ವರ್ಡ್", + "pad.importExport.exportpdf": "ಪಿಡಿಎಫ಼್", + "pad.importExport.exportopen": "ಓಡಿಫ಼್ (ಓಪನ್ ಡಾಕ್ಯುಮೆಂಟ್ ಫ಼ಾರ್ಮ್ಯಾಟ್)", + "pad.modals.cancel": "ರದ್ದು ಮಾಡು", + "pad.share.link": "ಕೊಂಡಿ", + "timeslider.toolbar.authors": "ಕರ್ತೃಗಳು:", + "timeslider.toolbar.exportlink.title": "ರಫ್ತು ಮಾಡು", + "timeslider.version": "ಆವೃತ್ತಿ {{version}}", + "timeslider.saved": "ಉಳಿಸಲಾಗಿದೆ {{month}} {{day}}, {{year}}", + "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "ಜನವರಿ", + "timeslider.month.february": "ಫೆಬ್ರವರಿ", + "timeslider.month.march": "ಮಾರ್ಚ್", + "timeslider.month.april": "ಏಪ್ರಿಲ್", + "timeslider.month.may": "ಮೇ", + "timeslider.month.june": "ಜೂನ್", + "timeslider.month.july": "ಜುಲೈ", + "timeslider.month.august": "ಆಗಸ್ಟ್", + "timeslider.month.september": "ಸೆಪ್ಟೆಂಬರ್", + "timeslider.month.october": "ಅಕ್ಟೋಬರ್", + "timeslider.month.november": "ನವೆಂಬರ್", + "timeslider.month.december": "ಡಿಸೆಂಬರ್" +} diff --git a/src/locales/ko.json b/src/locales/ko.json index 3fae86e93..d8bcc3e97 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -59,7 +59,7 @@ "pad.toolbar.unindent.title": "내어쓰기 (Shift+TAB)", "pad.toolbar.undo.title": "실행 취소 (Ctrl+Z)", "pad.toolbar.redo.title": "다시 실행 (Ctrl+Y)", - "pad.toolbar.clearAuthorship.title": "저자의 색 지우기 (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "작성자 표시 색상 지우기 (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "다른 파일 형식으로 가져오기/내보내기", "pad.toolbar.timeslider.title": "시간슬라이더", "pad.toolbar.savedRevision.title": "판 저장", @@ -75,7 +75,7 @@ "pad.settings.myView": "내 보기", "pad.settings.stickychat": "화면에 항상 대화 보기", "pad.settings.chatandusers": "대화와 사용자 보기", - "pad.settings.colorcheck": "저자 색", + "pad.settings.colorcheck": "작성자 표시 색상", "pad.settings.linenocheck": "줄 번호", "pad.settings.rtlcheck": "우횡서(오른쪽에서 왼쪽으로)입니까?", "pad.settings.fontType": "글꼴 종류:", @@ -136,7 +136,7 @@ "timeslider.followContents": "패드 콘텐츠의 갱신 주시하기", "timeslider.pageTitle": "{{appTitle}} 시간슬라이더", "timeslider.toolbar.returnbutton": "패드로 돌아가기", - "timeslider.toolbar.authors": "저자:", + "timeslider.toolbar.authors": "작성자:", "timeslider.toolbar.authorsList": "저자 없음", "timeslider.toolbar.exportlink.title": "내보내기", "timeslider.exportCurrent": "현재 버전으로 내보내기:", @@ -158,12 +158,12 @@ "timeslider.month.october": "10월", "timeslider.month.november": "11월", "timeslider.month.december": "12월", - "timeslider.unnamedauthors": "이름 없는 {[plural(num) one: 저자, other: 저자 ]} {{num}}명", + "timeslider.unnamedauthors": "이름 없는 {[plural(num) one: 작성자, other: 작성자]} {{num}}명", "pad.savedrevs.marked": "이 판은 이제 저장한 판으로 표시합니다.", "pad.savedrevs.timeslider": "당신은 타임슬라이더를 통해 저장된 버전을 볼 수 있습니다", "pad.userlist.entername": "이름을 입력하세요", "pad.userlist.unnamed": "이름없음", - "pad.editbar.clearcolors": "전체 문서의 저자 색을 지우시겠습니까? 이 작업은 취소할 수 없습니다", + "pad.editbar.clearcolors": "전체 문서의 작성자 표시 색상을 지우시겠습니까? 이 작업은 취소할 수 없습니다", "pad.impexp.importbutton": "지금 가져오기", "pad.impexp.importing": "가져오는 중...", "pad.impexp.confirmimport": "파일을 가져오면 패드의 현재 텍스트를 덮어쓰게 됩니다. 진행하시겠습니까?", diff --git a/src/locales/krc.json b/src/locales/krc.json index 408b78630..e90f9520c 100644 --- a/src/locales/krc.json +++ b/src/locales/krc.json @@ -1,26 +1,142 @@ { "@metadata": { "authors": [ - "Ernác" + "Ernác", + "Къарачайлы" ] }, + "admin.page-title": "Администраторну панели — Etherpad", + "admin_plugins": "Плагин менеджер", + "admin_plugins.available": "Киришли плагинле", + "admin_plugins.available_not-found": "Плагинле табылмадыла.", + "admin_plugins.available_fetching": "Келтириле турады...", + "admin_plugins.available_install.value": "Къур", + "admin_plugins.available_search.placeholder": "Къурур ючюн плагинлени изле", + "admin_plugins.description": "Ачыкълау", + "admin_plugins.installed": "Къурулгъан плагинле", + "admin_plugins.installed_fetching": "Къурулгъан плагинле алына турадыла...", + "admin_plugins.installed_nothing": "Алкъын бир плагин да къурмагъансыз.", + "admin_plugins.installed_uninstall.value": "Къорат", + "admin_plugins.last-update": "Ахыр джангыртыу", + "admin_plugins.name": "Ат", + "admin_plugins.page-title": "Плагин менеджер - Etherpad", + "admin_plugins.version": "Версия", + "admin_plugins_info": "Бузукъланы кетериулени юсюнден информация", + "admin_plugins_info.hooks": "Къурулгъан ыргъакъла", + "admin_plugins_info.hooks_client": "Клиентни джанындагъы ыргъакъла", + "admin_plugins_info.hooks_server": "Сервер джанындагъы ыргъакъла", + "admin_plugins_info.parts": "Къурулгъан юлюшле", + "admin_plugins_info.plugins": "Къурулгъан плагинле", + "admin_plugins_info.page-title": "Плагин информация — Etherpad", + "admin_plugins_info.version": "Etherpad версия", + "admin_plugins_info.version_latest": "Ахыр киришли версия", + "admin_plugins_info.version_number": "Версияны номери", + "admin_settings": "Джарашдырыўла", + "admin_settings.current": "Баргъан конфигурация", + "admin_settings.current_example-devel": "Юлгю хазырлау джарашдырыуланы шаблону", + "admin_settings.current_example-prod": "Юлгю чыгъарыу джарашдырыуланы шаблону", + "admin_settings.current_restart.value": "Etherpad-ны джангыдан башлат", + "admin_settings.current_save.value": "Джарашдырыуланы Сакъла", + "admin_settings.page-title": "Джарашдырыула — Etherpad", + "index.newPad": "Джангы Блокнот", + "index.createOpenPad": "неда бу ат бла Блокнот болдур/ач:", + "index.openPad": "бу ат бла бар болгъан Блокнотну ачыгъыз:", + "pad.toolbar.bold.title": "Къалын (Ctrl+B)", + "pad.toolbar.italic.title": "Курсив (Ctrl-I)", + "pad.toolbar.underline.title": "Тюбю чертилген (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Юсю сызылгъан (Ctrl+5)", + "pad.toolbar.ol.title": "Кёзюулю тизме (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Кёзюуге этилмеген тизме (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Абзац (TAB)", + "pad.toolbar.unindent.title": "Чыгъыш (Shift+TAB)", + "pad.toolbar.undo.title": "Кери ал (Ctrl+Z)", + "pad.toolbar.redo.title": "Къайтар (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Авторлукъну боялурын тазала (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Файлланы башха форматларын (а/дан) импорт/экспорт", + "pad.toolbar.timeslider.title": "Заман шкала", + "pad.toolbar.savedRevision.title": "Версияны сакъла", "pad.toolbar.settings.title": "Джарашдырыула", + "pad.toolbar.embed.title": "Бу блокнотну Джай эмда Ичине сал", + "pad.toolbar.showusers.title": "Хайырланыучуланы бу блокнотда кёргюзт", "pad.colorpicker.save": "Сакъла", + "pad.colorpicker.cancel": "Ызына ал", "pad.loading": "Джюклениу...", + "pad.noCookie": "Куки табылмадыла. Бразуеригизде кукилени бир джандырсагъыз! Сизни кириулеригизни арасында сессиягъыз эмда джарашдырыуларыгъыз сакъланныкъ тюлдюле. Буну чуруму, бир къауум браузерледе Etherpad iFrame ичинде болгъаны болургъа болур. Тилейбиз, Etherpad эмда аны башындагъы iFrame бир тюбдоменде/доменде болгъанындан ишексиз болугъуз.", + "pad.permissionDenied": "Бу блокнотха кириш эркинлигигиз джокъду", + "pad.settings.padSettings": "Блокнотну джарашдырыулары", + "pad.settings.myView": "Кёрюнюмюм", + "pad.settings.stickychat": "Ушакъны хар заман да экранда кёргюзт", + "pad.settings.chatandusers": "Ушакъ бла къошулуучуланы кёргюзт", + "pad.settings.colorcheck": "Авторлукъ бояула", + "pad.settings.linenocheck": "Сатырланы номерлери", + "pad.settings.rtlcheck": "Ичиндеги онгдан солгъа окъулсунму?", + "pad.settings.fontType": "Шрифтни типи:", "pad.settings.fontType.normal": "Нормал", "pad.settings.language": "Тил:", + "pad.settings.about": "Юсюнден", + "pad.settings.poweredBy": "Этген:", "pad.importExport.import_export": "Импорт/экспорт", + "pad.importExport.import": "Къаллай болса да текст файл неда документ джюкле", "pad.importExport.importSuccessful": "Тыйыншлы!", + "pad.importExport.export": "Баргъан блокнотну бу шекилде экспорт эт:", + "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Тюз текст", "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (OpenOffice'ни документи)", + "pad.importExport.abiword.innerHTML": "Сиз къуру тюз текстни неда HTML импорт этерге боллукъсуз. Импортну андан кенг функциялары ючюн AbiWord не LibreOffice къуругъуз.", + "pad.modals.connected": "Байланды.", + "pad.modals.reconnecting": "Блокнотугъузгъа джангыдан байлана турады...", + "pad.modals.forcereconnect": "Джангыдан зор бла байланыу", + "pad.modals.reconnecttimer": "Джангыдан байланыргъа кюрешеди", + "pad.modals.cancel": "Ызына алыу", + "pad.modals.userdup": "Башха терезеде ачыкъды", + "pad.modals.userdup.explanation": "Бу блокнот, бу компьютерде бирден аслам бразуре терезеде ачылгъаннга ушайды.", + "pad.modals.userdup.advice": "Бу терезени хайырланыб джангыдан байлан", + "pad.modals.unauth": "Авторизацияны ётмегенди", + "pad.modals.unauth.explanation": "Бу бетни къарагъан заманда, эркинликлеригиз тюрленнгедиле. Джангыдан байланыб кёрюгюз.", + "pad.modals.looping.explanation": "Синхронизация сервер бла байлам проблемала боладыла.", + "pad.modals.looping.cause": "Келишмеген фаерволл неда прокси бла байланнган болурсуз.", + "pad.modals.initsocketfail": "Серверге джетишилимейди.", + "pad.modals.initsocketfail.explanation": "Синхронизация серверге байланылалмады.", + "pad.modals.initsocketfail.cause": "Бу проблема браузеригиз бла, неда интернет байламыгъыз бла чурумланады.", + "pad.modals.slowcommit.explanation": "Сервер джууаб бермейди.", + "pad.modals.slowcommit.cause": "Бу болум ау байлам бла болгъан проблемала ючюн чыгъаргъа боллукъду.", + "pad.modals.badChangeset.explanation": "Этген тюзетиуюгюзню, синхронизация сервер джараусузча класслагъанды.", + "pad.modals.badChangeset.cause": "Буну чуруму джангылыч сервер конфигурация неда башха сакъланмагъан этиу болургъа боллукъду. Тилейбиз, буну халатха санай эсегиз, къуллукъну администратору бла байланыгъыз. Тюзетиуню андан ары бардырыр ючюн, джангыдан байланыб кёрюгюз.", + "pad.modals.corruptPad.explanation": "Кириш алыргъа излеген блокнот бузукъду.", + "pad.modals.corruptPad.cause": "Буну чуруму джангылыч сервер конфигурация неда башха сакъланмагъан этиу болургъа боллукъду. Тилейбиз, къуллукъну администратору бла байланыгъыз.", + "pad.modals.deleted": "Кетерилди.", + "pad.modals.deleted.explanation": "Бу блокнот къоратылгъанды.", + "pad.modals.rateLimited": "Терклик чеклендирилгенди.", + "pad.modals.rateLimited.explanation": "Бу блокнотха асыры кёб билдириу джибергенигиз ючюн, байлам кесилди.", + "pad.modals.rejected.explanation": "Браузеригиз джибергени билдириуюгюзню алыргъа унамады.", + "pad.modals.rejected.cause": "Блоконтха къарай тургъанлайыгъызгъа, сервер джангыртылыргъа болур неда Etherpad халатлы болургъа болур. Бетни джангыдан джюклеб кёрюгюз.", + "pad.modals.disconnected": "Байламыгъыз кесилди.", + "pad.modals.disconnected.explanation": "Серверге байлам кесилди.", + "pad.modals.disconnected.cause": "Сервер хайырланалмаз халда болургъа болур. Тилейбиз, былай андан ары барса, къуллукъну администраторуна билдиригизю.", + "pad.share": "Бу блокнотну ортагъа сал", + "pad.share.readonly": "Къуру окъу", + "pad.share.link": "Джибериу", + "pad.share.emebdcode": "URL сал", "pad.chat": "Чат", + "pad.chat.title": "Бу блокнот ючюн ушакъны ач", + "pad.chat.loadmessages": "Мындан аслам билдириу джюкле", + "pad.chat.stick.title": "Ушакъны экраннга джабышдыр", + "pad.chat.writeMessage.placeholder": "Билдириуюгюзню былайда джазыгъыз", + "timeslider.followContents": "Блокнот ичин джангыртыуун марагъыз", + "timeslider.pageTitle": "{{appTitle}} Заман Шкала", "timeslider.toolbar.returnbutton": "Документге", "timeslider.toolbar.authors": "Авторла:", + "timeslider.toolbar.authorsList": "Автор джокъду", "timeslider.toolbar.exportlink.title": "Эспорт эт", + "timeslider.exportCurrent": "Баргъан версияны бу шекилде экспорт эт:", "timeslider.version": "{{version}} версия", + "timeslider.saved": "{{day}} {{month}} {{year}} датада сакъланнганды", + "timeslider.playPause": "Блокнотну ичин Ойнат / Пауза", + "timeslider.backRevision": "Бу блокнотдагъы версиягъа кери къайт", + "timeslider.forwardRevision": "Блокнотда эндиги версиягъа бар", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}", "timeslider.month.january": "январь", "timeslider.month.february": "февраль", @@ -34,5 +150,20 @@ "timeslider.month.october": "октябрь", "timeslider.month.november": "ноябрь", "timeslider.month.december": "декабрь", - "pad.impexp.importing": "Импорт этиу…" + "timeslider.unnamedauthors": "{{num}} атсыз {[plural(num) one: автор, other: автор ]}", + "pad.savedrevs.marked": "Бу версия, артыкъ сакъланнган версия болуб белгиленнгенди", + "pad.savedrevs.timeslider": "Заман шкалагъа кириб, сакъланнган версияланы кёрюрге боллукъсуз", + "pad.userlist.entername": "Атынгы киргизт", + "pad.userlist.unnamed": "атсыз", + "pad.editbar.clearcolors": "Буютеу документдеги автор бояула сюртюлсюнмю? Бу этиу кери алынамаз", + "pad.impexp.importbutton": "Энди импорт эт", + "pad.impexp.importing": "Импорт этиу…", + "pad.impexp.confirmimport": "Файлны импорту баргъан текстни джангыдан джазарыкъды. Андан ары бардырыргъа излегенигизден ишексизмисиз?", + "pad.impexp.convertFailed": "Бу файлны импорт эталмадыкъ. Тилейбиз, башха форматны хайырланыгъыз, неда къол бла копия этиб джабышдырыгъыз", + "pad.impexp.padHasData": "Бу блокнотда алайсыз да тюрлениуле болгъаны ючюн бу файлны импорт эталмадыкъ, тилейбиз джангы блокнотха импорт этигиз", + "pad.impexp.uploadFailed": "Джюклеу джетишимсиз болду, тилейбизщ джангыдан сынагъыз", + "pad.impexp.importfailed": "Импорт этилалмады", + "pad.impexp.copypaste": "Тилейбиз, копия этиб джабышдыргъыз", + "pad.impexp.exportdisabled": "{{type}} форматда экспорт джукъланыбды. Ачыкълаула ючюн система администраторлагъа байланыгъыз.", + "pad.impexp.maxFileSize": "Файл асыры уллуду. Импорт ючюн эркинлик берилген файл ёлчемини уллу этер ючюн сайтны администратору бла байланыгъыз" } diff --git a/src/locales/ku-latn.json b/src/locales/ku-latn.json index 537c4074f..0d1525080 100644 --- a/src/locales/ku-latn.json +++ b/src/locales/ku-latn.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Balyozxane", "Bikarhêner", "Dilyaramude", "George Animal", @@ -59,7 +60,7 @@ "timeslider.toolbar.returnbutton": "Vegere bloknotê", "timeslider.toolbar.authors": "Nivîser:", "timeslider.toolbar.authorsList": "Nivîser Tine", - "timeslider.version": "Guhertoya {{version}}", + "timeslider.version": "Guhartoya {{version}}", "timeslider.saved": "Di dîroka {{day}} {{month}} {{year}} de hate tomarkirin", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}", "timeslider.month.january": "kanûna paşîn", diff --git a/src/locales/lb.json b/src/locales/lb.json index c7769ccdd..0c5370021 100644 --- a/src/locales/lb.json +++ b/src/locales/lb.json @@ -3,12 +3,21 @@ "authors": [ "Gromper", "Robby", - "Soued031" + "Soued031", + "Volvox" ] }, - "admin_plugins_info.version": "Etherpad Versioun", + "admin_plugins.available_install.value": "Installéieren", + "admin_plugins.description": "Beschreiwung", + "admin_plugins.installed_uninstall.value": "Desinstalléieren", + "admin_plugins.name": "Numm", + "admin_plugins.version": "Versioun", + "admin_plugins_info.version": "Etherpad-Versioun", "admin_plugins_info.version_latest": "Lescht disponibel Versioun", + "admin_plugins_info.version_number": "Versiounsnummer", "admin_settings": "Astellungen", + "admin_settings.current_save.value": "Astellunge späicheren", + "admin_settings.page-title": "Astellungen - Etherpad", "index.newPad": "Neie Pad", "index.createOpenPad": "oder maacht ee Pad mat dësem Numm op:", "pad.toolbar.bold.title": "Fett (Strg-B)", @@ -29,7 +38,8 @@ "pad.loading": "Lueden...", "pad.noCookie": "Cookie gouf net fonnt. Erlaabt wgl. Cookien an Ärem Browser! Är Sessioun an Är Astellungen ginn net tëscht de Visitte gespäichert. Dëst kann doduerch bedéngt sinn datt Etherpad a verschiddene Browser an iFrameën agebaut ass. Vergewëssert Iech wgl. datt Etherpad am selwechten Subdomain/Domain ass wéi den iwwergeuerdneten iFrame", "pad.permissionDenied": "Dir hutt net déi néideg Rechter fir dëse Pad opzemaachen", - "pad.settings.myView": "Méng Usiicht", + "pad.settings.padSettings": "Pad-Astellungen", + "pad.settings.myView": "Meng Usiicht", "pad.settings.linenocheck": "Zeilennummeren", "pad.settings.rtlcheck": "Inhalt vu riets no lénks liesen?", "pad.settings.fontType": "Schrëftart:", @@ -49,17 +59,21 @@ "pad.modals.connected": "Verbonnen.", "pad.modals.cancel": "Ofbriechen", "pad.modals.userdup": "An enger anerer Fënster opgemaach", + "pad.modals.userdup.explanation": "Et schéngt, datt dëse Pad a méi wéi enger Browserfënster op dësem Computer opgemaach ginn ass.", "pad.modals.unauth": "Net autoriséiert", "pad.modals.unauth.explanation": "Är Rechter hu geännert während deem Dir dës säit gekuckt hutt. Probéiert fir Iech nei ze connectéieren.", "pad.modals.looping.explanation": "Et gëtt Kommunikatiounsproblemer mam Synchronisatiouns-Server.", "pad.modals.initsocketfail": "De Server kann net erreecht ginn.", "pad.modals.slowcommit.explanation": "De Server äntwert net.", "pad.modals.deleted": "Geläscht.", + "pad.modals.deleted.explanation": "Dëse Pad gouf geläscht.", "pad.modals.disconnected": "Äre Verbindung ass ofgebrach.", "pad.modals.disconnected.explanation": "D'Verbindung mam Server ass verluergaang.", + "pad.share": "Dëse Pad deelen", "pad.share.readonly": "Nëmme liesen", "pad.share.link": "Link", "pad.chat": "Chat", + "pad.chat.title": "Den Chat fir dëse Pad opmaachen.", "pad.chat.loadmessages": "Méi Message lueden", "pad.chat.writeMessage.placeholder": "Schreift Äre Message hei", "timeslider.toolbar.authors": "Auteuren:", diff --git a/src/locales/lt.json b/src/locales/lt.json index 8e818a7bb..43660f72d 100644 --- a/src/locales/lt.json +++ b/src/locales/lt.json @@ -5,10 +5,17 @@ "I-svetaines", "Mantak111", "Naktis", + "Nokeoo", "Vogone", "Zygimantus" ] }, + "admin.page-title": "Administratoriaus prietaisų skydelis – Etherpad", + "admin_plugins": "Įskiepių tvarkyklė", + "admin_plugins.available": "Galimi įskiepiai", + "admin_plugins.available_not-found": "Įskiepių nerasta.", + "admin_plugins.available_fetching": "Gaunama…", + "admin_plugins.available_install.value": "Įdiegti", "index.newPad": "Naujas bloknotas", "index.createOpenPad": "arba sukurkite/atidarykite Bloknotą su pavadinimu:", "pad.toolbar.bold.title": "Paryškintasis (Ctrl-B)", diff --git a/src/locales/nah.json b/src/locales/nah.json index 3f07c7840..a5c40db10 100644 --- a/src/locales/nah.json +++ b/src/locales/nah.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Akapochtli", + "Languaeditor", "Taresi" ] }, @@ -15,7 +16,7 @@ "pad.toolbar.redo.title": "Occeppa (Ctrl+Y)", "pad.toolbar.settings.title": "Tlatlālīliztli", "pad.colorpicker.save": "Xicpiya", - "pad.colorpicker.cancel": "Moxitiniz", + "pad.colorpicker.cancel": "Xikxolewa", "pad.settings.padSettings": "Pad Ītlatlālīliz", "pad.settings.myView": "Notlachiyaliz", "pad.settings.language": "Tlahtolli:", @@ -24,20 +25,20 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.modals.cancel": "Moxitiniz", + "pad.modals.cancel": "Xikxolewa", "pad.modals.deleted": "Omopohpoloh.", "pad.modals.deleted.explanation": "Ōmopoloh inīn Pad.", "timeslider.version": "Inīc {{version}} Cuepaliztli", - "timeslider.month.january": "Īccēmētztli", - "timeslider.month.february": "Īcōmemētztli", - "timeslider.month.march": "Īcyēyimētztli", - "timeslider.month.april": "Īcnāhuimētztli", - "timeslider.month.may": "Īcmācuīllimētztli", - "timeslider.month.june": "Īcchicuacemmētztli", - "timeslider.month.july": "Īcchicōmemētztli", - "timeslider.month.august": "Īcchicuēyimētztli", - "timeslider.month.september": "Īcchiucnāhuimētztli", - "timeslider.month.october": "Īcmahtlactlimētztli", - "timeslider.month.november": "Īcmahtlactlioncēmētztli", - "timeslider.month.december": "Īcmahtlactliomōmemētztli" + "timeslider.month.january": "Eneroh", + "timeslider.month.february": "Febreroh", + "timeslider.month.march": "Marsoh", + "timeslider.month.april": "April", + "timeslider.month.may": "Mayoh", + "timeslider.month.june": "Honioh", + "timeslider.month.july": "Holioh", + "timeslider.month.august": "Ahostoh", + "timeslider.month.september": "Septiempreh", + "timeslider.month.october": "Oktopreh", + "timeslider.month.november": "Noviempreh", + "timeslider.month.december": "Tisiempreh" } diff --git a/src/locales/nap.json b/src/locales/nap.json index 1c640a023..70f615b38 100644 --- a/src/locales/nap.json +++ b/src/locales/nap.json @@ -3,9 +3,11 @@ "authors": [ "C.R.", "Chelin", + "Finizio", "Ruthven" ] }, + "admin_plugins.name": "Nomme", "index.newPad": "Nuovo Pad", "index.createOpenPad": "o crià o arape nu Pad cu 'o nomme:", "pad.toolbar.bold.title": "Grassetto (Ctrl-B)", diff --git a/src/locales/nb.json b/src/locales/nb.json index d62bf3dff..cca06a541 100644 --- a/src/locales/nb.json +++ b/src/locales/nb.json @@ -3,12 +3,17 @@ "authors": [ "Chameleon222", "Cocu", + "EdoAug", "Jon Harald Søby", "Laaknor", "Orjanmen", "SuperPotato" ] }, + "admin_plugins.available_install.value": "Installer", + "admin_plugins.version": "Versjon", + "admin_settings": "Innstillinger", + "admin_settings.current_save.value": "Lagre innstillinger", "index.newPad": "Ny pad", "index.createOpenPad": "eller opprett/åpne en pad med dette navnet:", "index.openPad": "åpne en eksisterende Pad med følgende navn:", diff --git a/src/locales/ne.json b/src/locales/ne.json index 39e813744..93b31c0eb 100644 --- a/src/locales/ne.json +++ b/src/locales/ne.json @@ -1,8 +1,10 @@ { "@metadata": { "authors": [ + "Bada Kaji", "Nirajan pant", "Nirjal stha", + "बडा काजी", "राम प्रसाद जोशी", "सरोज कुमार ढकाल", "हिमाल सुबेदी" @@ -23,14 +25,14 @@ "pad.toolbar.clearAuthorship.title": "लेखकीय रङ्ग हटाउने (Ctrl+Shift+C)", "pad.toolbar.timeslider.title": "टाइमस्लाइडर", "pad.toolbar.savedRevision.title": "पुनरावलोकन संग्रहगर्ने", - "pad.toolbar.settings.title": "सेटिङ्गहरू", + "pad.toolbar.settings.title": "अभिरुचिहरू", "pad.toolbar.embed.title": "यस प्याडलाई बाड्ने या इम्बेड गर्ने", "pad.toolbar.showusers.title": "यस प्याडमा रहेका प्रयोगकर्ता देखाउने", "pad.colorpicker.save": "सङ्ग्रह गर्ने", - "pad.colorpicker.cancel": "रद्द", + "pad.colorpicker.cancel": "रद्द गर्नुहोस्", "pad.loading": "खुल्दै छ…", "pad.permissionDenied": "तपाईंलाई यो प्याड खोल्न अनुमति छैन", - "pad.settings.padSettings": "प्याड सेटिङ्गहरू", + "pad.settings.padSettings": "प्याड अभिरुचिहरू", "pad.settings.myView": "मेरो दृष्य", "pad.settings.stickychat": "पर्दामा सधै च्याट गर्ने", "pad.settings.chatandusers": "वार्ता तथा प्रयोगकर्ताहरू देखाउने", @@ -65,7 +67,7 @@ "pad.modals.disconnected.explanation": "तपाईंको सर्भरसँगको जडान अवरुद्ध भयो", "pad.share": "यस प्यडलाई बाड्ने", "pad.share.readonly": "पढ्ने मात्र", - "pad.share.link": "लिङ्क", + "pad.share.link": "कडी", "pad.share.emebdcode": "URL थप्ने", "pad.chat": "कुराकानी", "pad.chat.title": "यस प्याडको लागि कुराकानी खोल्ने", @@ -78,7 +80,7 @@ "timeslider.exportCurrent": "हालको संस्करण निम्म रुपमा निर्यात गर्ने :", "timeslider.version": "संस्करण {{version}}", "timeslider.saved": "सङ्ग्रह गरिएको {{month}} {{day}}, {{year}}", - "timeslider.playPause": "प्याडको सामग्रीहरूलाई चालु/बन्द गर्नुहोस", + "timeslider.playPause": "प्याडको सामग्रीहरूलाई चालु / बन्द गर्नुहोस्", "timeslider.backRevision": "यो प्याडको एक संस्करण पहिले जानुहोस्", "timeslider.forwardRevision": "यो प्याडको एक संस्करण पछि जानुहोस्", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", diff --git a/src/locales/nl.json b/src/locales/nl.json index 303bded94..5fb4347a8 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -13,6 +13,7 @@ "Robin van der Vliet", "Robin0van0der0vliet", "Siebrand", + "Spinster", "woeterman94" ] }, @@ -32,7 +33,7 @@ "admin_plugins.name": "Naam", "admin_plugins.page-title": "Plugin beheer - Etherpad", "admin_plugins.version": "Versie", - "admin_plugins_info": "Info", + "admin_plugins_info": "Probleemoplossingsinformatie", "admin_plugins_info.hooks": "Geïnstalleerde hooks", "admin_plugins_info.hooks_client": "Client-side hooks", "admin_plugins_info.hooks_server": "Server-side hooks", @@ -44,13 +45,14 @@ "admin_plugins_info.version_number": "Versie nummer", "admin_settings": "Instellingen", "admin_settings.current": "Huidige configuratie", - "admin_settings.current_example-devel": "Development instellingen template", + "admin_settings.current_example-devel": "Voorbeeldsjabloon voor ontwikkelingsinstellingen", "admin_settings.current_example-prod": "Productie instellingen template", "admin_settings.current_restart.value": "Herstart Etherpad", "admin_settings.current_save.value": "Bewaar instellingen", "admin_settings.page-title": "Instellingen - Etherpad", "index.newPad": "Nieuw pad", "index.createOpenPad": "of maak/open een pad met de naam:", + "index.openPad": "open een bestaande Pad met de naam:", "pad.toolbar.bold.title": "Vet (Ctrl-B)", "pad.toolbar.italic.title": "Cursief (Ctrl-I)", "pad.toolbar.underline.title": "Onderstrepen (Ctrl-U)", @@ -84,6 +86,7 @@ "pad.settings.fontType.normal": "Normaal", "pad.settings.language": "Taal:", "pad.settings.about": "Over", + "pad.settings.poweredBy": "Aangedreven door", "pad.importExport.import_export": "Importeren/exporteren", "pad.importExport.import": "Tekstbestand of document uploaden", "pad.importExport.importSuccessful": "Afgerond", @@ -118,6 +121,10 @@ "pad.modals.corruptPad.cause": "Dit kan komen door een onjuiste serverinstelling of door ander onverwacht gedrag. Neem contact op met de servicebeheerder.", "pad.modals.deleted": "Verwijderd.", "pad.modals.deleted.explanation": "Dit pad is verwijderd.", + "pad.modals.rateLimited": "Snelheid begrensd.", + "pad.modals.rateLimited.explanation": "Je hebt te veel berichten naar deze pad gestuurd. Daarom is je verbinding verbroken.", + "pad.modals.rejected.explanation": "De server heeft een bericht afgewezen dat door je browser is verzonden.", + "pad.modals.rejected.cause": "Mogelijks is de server bijgewerkt terwijl je de pad aan het bekijken was. Of misschien is er een bug in Etherpad. Probeer de pagina opnieuw te laden.", "pad.modals.disconnected": "Uw verbinding is verbroken.", "pad.modals.disconnected.explanation": "De verbinding met de server is verbroken", "pad.modals.disconnected.cause": "De server is mogelijk niet beschikbaar. Stel de servicebeheerder op de hoogte.", @@ -128,8 +135,9 @@ "pad.chat": "Chatten", "pad.chat.title": "Chat voor dit pad opnenen", "pad.chat.loadmessages": "Meer berichten laden", - "pad.chat.stick.title": "Zet de chat op het scherm", + "pad.chat.stick.title": "Chat op scherm vastzetten", "pad.chat.writeMessage.placeholder": "Schrijf je bericht hier", + "timeslider.followContents": "Volg de inhoudelijke updates van de pad", "timeslider.pageTitle": "Tijdlijn voor {{appTitle}}", "timeslider.toolbar.returnbutton": "Terug naar pad", "timeslider.toolbar.authors": "Auteurs:", @@ -168,5 +176,6 @@ "pad.impexp.uploadFailed": "Het uploaden is mislukt. Probeer het opnieuw", "pad.impexp.importfailed": "Importeren is mislukt", "pad.impexp.copypaste": "Gebruik kopiëren en plakken", - "pad.impexp.exportdisabled": "Exporteren als {{type}} is uitgeschakeld. Neem contact op met de systeembeheerder voor details." + "pad.impexp.exportdisabled": "Exporteren als {{type}} is uitgeschakeld. Neem contact op met de systeembeheerder voor details.", + "pad.impexp.maxFileSize": "Het bestand is te groot. Neem contact op met je sitebeheerder om de toegestane bestandsgrootte voor importeren te vergroten." } diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index 0e6d9993a..98c660319 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -18,7 +18,8 @@ "Titoncio", "Tuliouel", "Walesson", - "Webysther" + "Webysther", + "YuriNikolai" ] }, "admin.page-title": "Painel administrativo - Etherpad", @@ -55,18 +56,18 @@ "admin_settings.current_save.value": "Salvar configurações", "admin_settings.page-title": "Configurações - Etherpad", "index.newPad": "Nova Nota", - "index.createOpenPad": "ou criar-abrir uma Nota com o nome:", - "index.openPad": "abra um bloco existente com o nome:", - "pad.toolbar.bold.title": "Negrito (Ctrl-B)", - "pad.toolbar.italic.title": "Itálico (Ctrl-I)", - "pad.toolbar.underline.title": "Sublinhado (Ctrl-U)", + "index.createOpenPad": "ou criar/abrir uma Nota com o nome:", + "index.openPad": "abra uma Nota existente com o nome:", + "pad.toolbar.bold.title": "Negrito (Ctrl+B)", + "pad.toolbar.italic.title": "Itálico (Ctrl+I)", + "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", "pad.toolbar.strikethrough.title": "Tachado (Ctrl+5)", "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista não ordenada (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Aumentar Recuo (TAB)", "pad.toolbar.unindent.title": "Diminuir Recuo (Shift+TAB)", - "pad.toolbar.undo.title": "Desfazer (Ctrl-Z)", - "pad.toolbar.redo.title": "Refazer (Ctrl-Y)", + "pad.toolbar.undo.title": "Desfazer (Ctrl+Z)", + "pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Limpar as cores de identificação de autoria (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/Exportar de/para diferentes formatos de arquivo", "pad.toolbar.timeslider.title": "Linha do tempo", @@ -103,7 +104,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só é possível importar texto sem formatação ou HTML. Para obter funcionalidades de importação mais avançadas, por favor instale o AbiWordor ou LibreOffice.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando à sua nota...", + "pad.modals.reconnecting": "Reconectando à sua nota…", "pad.modals.forcereconnect": "Forçar reconexão", "pad.modals.reconnecttimer": "Tentando se reconectar", "pad.modals.cancel": "Cancelar", @@ -128,6 +129,7 @@ "pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para este pad por isso será desconectado.", "pad.modals.rejected.explanation": "O servidor rejeitou uma mensagem que foi enviada pelo seu navegador.", + "pad.modals.rejected.cause": "O server pode ter sido atualizado enquanto visualizava esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.", "pad.modals.disconnected": "Você foi desconectado.", "pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador caso isso continue.", diff --git a/src/locales/sc.json b/src/locales/sc.json new file mode 100644 index 000000000..468fe7358 --- /dev/null +++ b/src/locales/sc.json @@ -0,0 +1,168 @@ +{ + "@metadata": { + "authors": [ + "Adr mm" + ] + }, + "admin.page-title": "Pannellu de amministratzione - Etherpad", + "admin_plugins": "Gestore de connetores", + "admin_plugins.available": "Connetores a disponimentu", + "admin_plugins.available_not-found": "Nissunu connetore a disponimentu", + "admin_plugins.available_fetching": "Recuperende...", + "admin_plugins.available_install.value": "Installa", + "admin_plugins.available_search.placeholder": "Chirca connetores de installare", + "admin_plugins.description": "Descritzione", + "admin_plugins.installed": "Connetores installados", + "admin_plugins.installed_fetching": "Recuperende connetores installados...", + "admin_plugins.installed_nothing": "No as installadu ancora nissunu connetore.", + "admin_plugins.installed_uninstall.value": "Disinstalla", + "admin_plugins.last-update": "Ùrtima atualizatzione", + "admin_plugins.name": "Nòmine", + "admin_plugins.page-title": "Gestore de connetores - Etherpad", + "admin_plugins.version": "Versione", + "admin_plugins_info": "Informatzione pro sa risolutzione de problemas", + "admin_plugins_info.hooks": "Gantzos installados", + "admin_plugins_info.hooks_client": "Gantzos dae su costadu de su cliente", + "admin_plugins_info.hooks_server": "Gantzos dae su costadu de su serbidore", + "admin_plugins_info.parts": "Partes installadas", + "admin_plugins_info.plugins": "Connetores installados", + "admin_plugins_info.page-title": "Informatzione de su connetore - Etherpad", + "admin_plugins_info.version": "Versione de Etherpad", + "admin_plugins_info.version_latest": "Ùrtima versione a disponimentu", + "admin_plugins_info.version_number": "Nùmeru de versione", + "admin_settings": "Cunfiguratzione", + "admin_settings.current": "Cunfiguratzione atuale", + "admin_settings.current_example-devel": "Modellu de esempru de cunfiguratzione de isvilupu", + "admin_settings.current_example-prod": "Modellu de esempru de cunfiguratzione de produtzione", + "admin_settings.current_restart.value": "Torra a aviare Etherpad", + "admin_settings.current_save.value": "Sarva sa cunfiguratzione", + "admin_settings.page-title": "Cunfiguratzione - Etherpad", + "index.newPad": "Pad nou", + "index.createOpenPad": "o crea/aberi unu pad cun su nòmine:", + "index.openPad": "aberi unu pad esistente cun su nòmine:", + "pad.toolbar.bold.title": "Grassetu (Ctrl+B)", + "pad.toolbar.italic.title": "Cursivu (Ctrl+I)", + "pad.toolbar.underline.title": "Sutaliniadu (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Istangadu (Ctrl+5)", + "pad.toolbar.ol.title": "Lista numerada (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Lista cun puntos (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Indentatzione a dereta (Tab)", + "pad.toolbar.unindent.title": "Indentatzione a manca (Shift+Tab)", + "pad.toolbar.undo.title": "Iscontza (Ctrl+Z)", + "pad.toolbar.redo.title": "Torra a fàghere (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Lìmpia is colores de autoria (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Importa/esporta dae/a formados de archìviu diferentes", + "pad.toolbar.timeslider.title": "Presentatzione cronologia", + "pad.toolbar.savedRevision.title": "Sarva sa versione", + "pad.toolbar.settings.title": "Cunfiguratzione", + "pad.toolbar.embed.title": "Cumpartzi e incòrpora custu pad", + "pad.toolbar.showusers.title": "Ammustra is utentes in custu pad", + "pad.colorpicker.save": "Sarva", + "pad.colorpicker.cancel": "Annulla", + "pad.loading": "Carrighende...", + "pad.noCookie": "Su testimòngiu no est istètiu agatadu. Permite is testimòngios in su navigadore tuo. Sa sessione e sa cunfiguratzione tuas no ant a èssere sarvadas intre bìsitas. Podet èssere pro more de s'inclusione de Etherpad comente iFrame in tzertos navigadores. Assegura·ti chi Etherpad s'agatat in su pròpiu sutadomìniu/domìniu chi s'iFrame printzipale", + "pad.permissionDenied": "Non tenes permissu pro atzèdere a custu pad", + "pad.settings.padSettings": "Cunfiguratzione de su pad", + "pad.settings.myView": "Sa visualizatzione mia", + "pad.settings.stickychat": "Ammustra semper sa tzarrada", + "pad.settings.chatandusers": "Ammustra sa tzarrada e is utentes", + "pad.settings.colorcheck": "Colores de autoria", + "pad.settings.linenocheck": "Nùmeros de lìnia", + "pad.settings.rtlcheck": "Cuntenutu dae manca a dereta", + "pad.settings.fontType": "Tipu de caràtere:", + "pad.settings.fontType.normal": "Normale", + "pad.settings.language": "Lìngua:", + "pad.settings.about": "Informatziones", + "pad.settings.poweredBy": "Realizadu cun", + "pad.importExport.import_export": "Importatzione/esportatzione", + "pad.importExport.import": "Càrriga un'archìviu de testu o unu documentu", + "pad.importExport.importSuccessful": "Carrigadu.", + "pad.importExport.export": "Esporta su pad atuale comente:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Testu sèmplitze", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Open Documentu Formadu)", + "pad.importExport.abiword.innerHTML": "Isceti is formados de testu sèmplitze o HTML podent èssere importados. Pro mètodos avantzados de importatzione, installa AbiWord o LibreOffice.", + "pad.modals.connected": "Connètidu.", + "pad.modals.reconnecting": "Connetende a su pad tuo...", + "pad.modals.forcereconnect": "Fortza sa connesione", + "pad.modals.reconnecttimer": "Torrende a connètere in", + "pad.modals.cancel": "Annulla", + "pad.modals.userdup": "Abertu in una àtera ventana", + "pad.modals.userdup.explanation": "Podet dare chi custu pad siat abertu in un'àtera ischeda de custu navigadore in custu ordinadore.", + "pad.modals.userdup.advice": "Torra a connètere pro impreare custa ventana.", + "pad.modals.unauth": "Chena autorizatzione", + "pad.modals.unauth.explanation": "Is permissos tuos sunt istados cambiados in su mentras chi fias bidende custa pàgina. Prova de ti torrare a connètere.", + "pad.modals.looping.explanation": "Bi sunt problemas de comunicatzione cun su serbidore de sincronizatzione.", + "pad.modals.looping.cause": "Forsis sa connessione at impreadu unu serbidore intermediàriu (proxy) o unu firewall chi no est cumpatìbile.", + "pad.modals.initsocketfail": "Su serbidore no est atzessìbile.", + "pad.modals.initsocketfail.explanation": "Impossìbile connètere cun su serbidore de sincronizatzione.", + "pad.modals.initsocketfail.cause": "Podet èssere a càusa de unu problema cun su navigadore tuo o cun sa connessione de internet.", + "pad.modals.slowcommit.explanation": "Su serbidore non rispondet.", + "pad.modals.slowcommit.cause": "Podet èssere a càusa de problemas cun sa connessione de internet.", + "pad.modals.badChangeset.explanation": "Una modìfica tua est istada cunsiderada illegale dae su serbidore de sincronizatzione.", + "pad.modals.badChangeset.cause": "Podet èssere a càusa de una cunfiguratzione de serbidore isballiada o calicunu àteru cumportamentu imprevistu. Iscrie a s'amministratzione de su servìtziu si pensas chi siat un'errore. Prova a connètere torra pro sighire a modificare.", + "pad.modals.corruptPad.explanation": "Su pad a su chi ses chirchende de atzèdere est dannadu.", + "pad.modals.corruptPad.cause": "Podet èssere a càusa de una cunfiguratzione de serbidore non curreta o pro unu cumportamentu imprevistu. Iscrie a s'amministratzione de su servìtziu.", + "pad.modals.deleted": "Cantzelladu.", + "pad.modals.deleted.explanation": "Pad cantzelladu.", + "pad.modals.rateLimited": "Frecuèntzia limitada.", + "pad.modals.rateLimited.explanation": "As imbiadu tropu messàgios a custu pad e t'at disconnètidu.", + "pad.modals.rejected.explanation": "Su serbidore at rifiutadu unu messàgiu imbiadu dae su navigadore tuo.", + "pad.modals.rejected.cause": "Podet èssere chi su serbidore siat istadu atualizadu in su mentras chi fias bidende su pad, o podet èssere chi bi siat un'errore in Etherpad. Prova a atualizare sa pàgina.", + "pad.modals.disconnected": "Disconnètidu.", + "pad.modals.disconnected.explanation": "Connessione cun su serbidore pèrdida.", + "pad.modals.disconnected.cause": "Su serbidore no est a disponimentu. Iscrie a s'amministratzione de su servìtziu si su problema persistet.", + "pad.share": "Cumpartzi custu pad", + "pad.share.readonly": "Letura isceti", + "pad.share.link": "Ligàmene", + "pad.share.emebdcode": "Incòrpora URL", + "pad.chat": "Tzarrada", + "pad.chat.title": "Aberi sa tzarrada pro custu pad.", + "pad.chat.loadmessages": "Càrriga àteros messàgios", + "pad.chat.stick.title": "Apica sa tzarrada in s'ischermu", + "pad.chat.writeMessage.placeholder": "Iscrie su messàgiu tuo inoghe", + "timeslider.followContents": "Sighi sas atualizatziones de cuntenutu de su pad", + "timeslider.pageTitle": "Cronologia {{appTitle}}", + "timeslider.toolbar.returnbutton": "Torra a su pad", + "timeslider.toolbar.authors": "Autores:", + "timeslider.toolbar.authorsList": "Nissunu autore", + "timeslider.toolbar.exportlink.title": "Esporta", + "timeslider.exportCurrent": "Esporta sa versione atuale comente:", + "timeslider.version": "Versione {{version}}", + "timeslider.saved": "Sarvadu su {{day}} de {{month}} de su {{year}}", + "timeslider.playPause": "Riprodutzione/pàusa de is cuntenutos de su pad", + "timeslider.backRevision": "Bae a una versione pretzedente de custu pad", + "timeslider.forwardRevision": "Bae a una versione imbeniente de custu pad", + "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "Ghennàrgiu", + "timeslider.month.february": "Freàrgiu", + "timeslider.month.march": "Martzu", + "timeslider.month.april": "Abrile", + "timeslider.month.may": "Maju", + "timeslider.month.june": "Làmpadas", + "timeslider.month.july": "Mese de argiolas", + "timeslider.month.august": "Austu", + "timeslider.month.september": "Cabudanni", + "timeslider.month.october": "Ledàmine", + "timeslider.month.november": "Onniasantu", + "timeslider.month.december": "Mese de idas", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: autore, other: autores ]} chena nòmine", + "pad.savedrevs.marked": "Custa revisione est istada marcada comente revisione sarvada", + "pad.savedrevs.timeslider": "Podes bìdere is versiones sarvadas bisitende sa cronologia", + "pad.userlist.entername": "Inserta su nòmine tuo", + "pad.userlist.unnamed": "Chena nòmine", + "pad.editbar.clearcolors": "Seguru chi boles limpiare is colores de autoria de totu su documentu? Custa atzione no dda podes annullare", + "pad.impexp.importbutton": "Importa immoe", + "pad.impexp.importing": "Importende...", + "pad.impexp.confirmimport": "S'importatzione de un'archìviu at a subraiscrìere su testu atuale de su pad. Seguru chi boles sighire?", + "pad.impexp.convertFailed": "Impossìbile importare custu archìviu. Imprea unu formadu de documentu diferente o còpia e incolla a manu", + "pad.impexp.padHasData": "Impossìbile importare custu archìviu pro ite custu pad est istadu giai modificadu. Importa·ddu in unu pad nou", + "pad.impexp.uploadFailed": "Errore in sa càrriga. Torra a provare", + "pad.impexp.importfailed": "Errore de importatzione", + "pad.impexp.copypaste": "Còpia e incolla", + "pad.impexp.exportdisabled": "S'esportatzione comente {{type}} est disativada. Iscrie a s'amministratzione de su sistema pro àteras informatziones.", + "pad.impexp.maxFileSize": "S'archìviu est tropu manu. Iscrie a s'amministratzione pro ismanniare sa dimensione permìtida pro s'importatzione" +} diff --git a/src/locales/sh.json b/src/locales/sh.json index 659398e27..f5c045c55 100644 --- a/src/locales/sh.json +++ b/src/locales/sh.json @@ -5,8 +5,16 @@ "Vlad5250" ] }, + "admin_plugins.available_not-found": "Nijedan plugin nije pronađen.", + "admin_plugins.description": "Opis", + "admin_plugins.installed_uninstall.value": "Deinstaliraj", + "admin_plugins.last-update": "Posljednja podnova", + "admin_plugins.name": "Naziv", + "admin_plugins.version": "Verzija", + "admin_settings.current_save.value": "Sačuvaj podešavanja", "index.newPad": "Novi blokić", "index.createOpenPad": "ili napravite/otvorite blokić s imenom:", + "index.openPad": "otvori postojeći blokić Etherpada s imenom:", "pad.toolbar.bold.title": "Podebljano (Ctrl+B)", "pad.toolbar.italic.title": "Ukošeno (Ctrl+I)", "pad.toolbar.underline.title": "Podcrtano (Ctrl+U)", @@ -14,7 +22,7 @@ "pad.toolbar.ol.title": "Poredani spisak (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Neporedani spisak (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Uvlaka (TAB)", - "pad.toolbar.unindent.title": "Izvlaka (Shift+TAB)", + "pad.toolbar.unindent.title": "Izvlačenje (Shift+TAB)", "pad.toolbar.undo.title": "Vrati (Ctrl+Z)", "pad.toolbar.redo.title": "Ponovi (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Ukloni boje autorstva (Ctrl+Shift+C)", @@ -27,7 +35,7 @@ "pad.colorpicker.save": "Snimi", "pad.colorpicker.cancel": "Otkaži", "pad.loading": "Učitavam...", - "pad.noCookie": "Nisam mogao pronaći kolačić. Omogućite kolačiće u vašem pregledniku!", + "pad.noCookie": "Kolačić nije pronađen. Molimo Vas, omogućite kolačiće u Vašem pregledniku! Sesija i podešavanja neće biti sačuvana uz sljedeće posjećivanje. Razlog može biti uključenost Etherpada u iFrame u nekim preglednicima. Molimo Vas, osigurajte da je Etherpad na istoj poddomeni/domeni kao i roditeljski iFrame", "pad.permissionDenied": "Za ovdje nije potrebna dozvola za pristup", "pad.settings.padSettings": "Postavke blokića", "pad.settings.myView": "Moj prikaz", @@ -38,6 +46,8 @@ "pad.settings.rtlcheck": "Da prikažem sadržaj zdesna ulijevo?", "pad.settings.fontType": "Tip fonta:", "pad.settings.language": "Jezik:", + "pad.settings.about": "O projektu", + "pad.settings.poweredBy": "Omogućeno od strane", "pad.importExport.import_export": "Uvoz/Izvoz", "pad.importExport.import": "Otpremanje bilo koje tekstualne datoteke ili dokumenta", "pad.importExport.importSuccessful": "Uspješno!", @@ -48,7 +58,7 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Možete uvoziti samo iz običnog teksta te HTML-formata. Naprednije mogućnosti uvoza dobit ćete ako uspostavite AbiWord.", + "pad.importExport.abiword.innerHTML": "Možete uvoziti samo iz običnog teksta te datoteke u formatima HTML-a. Naprednije mogućnosti uvoza dobit ćete ako instalirajte AbiWord ili LibreOffice.", "pad.modals.connected": "Povezano.", "pad.modals.reconnecting": "Prepovezujemo Vas s blokićem...", "pad.modals.forcereconnect": "Nametni prepovezivanje", @@ -72,6 +82,10 @@ "pad.modals.corruptPad.cause": "Ovo može biti zbog pogrešne postavljenosti poslužitelja ili nekog drugog neočekivanog ponašanja. Obratite se administratoru.", "pad.modals.deleted": "Obrisano.", "pad.modals.deleted.explanation": "Ovaj blokić je uklonjen.", + "pad.modals.rateLimited": "Ograničenje stopa.", + "pad.modals.rateLimited.explanation": "Poslali ste previše poruka na ovaj blokić, te ste stoga odspojeni.", + "pad.modals.rejected.explanation": "Poslužitelj je odbio poruku koju je poslao vaš preglednik.", + "pad.modals.rejected.cause": "Poslužitelj je možda podnovljen dok ste gledali blokić, ili možda postoji greška u Etherpadu. Pokušajte ponovo učitati stranicu.", "pad.modals.disconnected": "Veza je prekinuta.", "pad.modals.disconnected.explanation": "Veza s poslužiteljem je prekinuta", "pad.modals.disconnected.cause": "Moguće je da server nije dostupan. Obavijestite administratora ako se ovo nastavi događati.", @@ -84,6 +98,7 @@ "pad.chat.loadmessages": "Učitaj više poruka", "pad.chat.stick.title": "Zalijepi ćaskanje na ekranu", "pad.chat.writeMessage.placeholder": "Ovdje napišite poruku", + "timeslider.followContents": "Prati podnove sadržaja blokića", "timeslider.pageTitle": "{{appTitle}} Historijski pregled", "timeslider.toolbar.returnbutton": "Natrag na blokić", "timeslider.toolbar.authors": "Autori:", @@ -113,7 +128,7 @@ "pad.savedrevs.timeslider": "Možete pogledati spremljene inačice rabeći vremesledni klizač", "pad.userlist.entername": "Upišite svoje ime", "pad.userlist.unnamed": "bez imena", - "pad.editbar.clearcolors": "Ukloniti boje autorstva sa cijelog dokumenta?", + "pad.editbar.clearcolors": "Ukloniti boje autorstva sa cijelog dokumenta? Radnju nije moguće poništiti", "pad.impexp.importbutton": "Uvezi odmah", "pad.impexp.importing": "Uvozim...", "pad.impexp.confirmimport": "Uvoženje datoteke presnimit će trenutni sadržaj blokića.\nJeste li sigurni da želite nastaviti?", @@ -122,5 +137,6 @@ "pad.impexp.uploadFailed": "Postavljanje nije uspjelo. Pokušajte ponovo.", "pad.impexp.importfailed": "Uvoz nije uspio", "pad.impexp.copypaste": "Prekopirajte", - "pad.impexp.exportdisabled": "Izvoz u formatu {{type}} je onemogućen. Ako želite saznati više o ovome, obratite se administratoru sustava." + "pad.impexp.exportdisabled": "Izvoz u formatu {{type}} je onemogućen. Ako želite saznati više o ovome, obratite se administratoru sustava.", + "pad.impexp.maxFileSize": "Datoteka je prevelika. Kontaktirajte administratora kako biste zatražili povećanje dopuštene veličine datoteke za uvoz" } diff --git a/src/locales/sk.json b/src/locales/sk.json index bc3f45462..ea1e47d60 100644 --- a/src/locales/sk.json +++ b/src/locales/sk.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Jose1711", "Kusavica", "Lexected", "Mark", @@ -39,7 +40,7 @@ "admin_settings.current": "Aktuálne nastavenia", "admin_settings.current_example-devel": "Príklad šablóny vývojárskeho nastavenia", "admin_settings.current_example-prod": "Príklad šablóny výrobného nastavenia", - "admin_settings.current_restart.value": "Reštartovať Ehterpad", + "admin_settings.current_restart.value": "Reštartovať Etherpad", "admin_settings.current_save.value": "Uložiť nastavenia", "admin_settings.page-title": "Nastavenia - Etherpad", "index.newPad": "Nový poznámkový blok", @@ -154,7 +155,7 @@ "timeslider.month.october": "októbra", "timeslider.month.november": "novembra", "timeslider.month.december": "decembra", - "timeslider.unnamedauthors": "{[plural(num) one:Počet nemenovaných autorov:, other: Počet nemenovaných autorov:]} {{num}}", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: nemenovaný autor, few: nemenovaní autori, other: nemenovaných autorov ]}", "pad.savedrevs.marked": "Táto revízia bola označená ako uložená", "pad.savedrevs.timeslider": "Návštevou časovej osi môžete zobraziť uložené revízie", "pad.userlist.entername": "Zadajte svoje meno", diff --git a/src/locales/skr-arab.json b/src/locales/skr-arab.json index 0640b249a..67abc7aa8 100644 --- a/src/locales/skr-arab.json +++ b/src/locales/skr-arab.json @@ -4,10 +4,29 @@ "Saraiki" ] }, + "admin_plugins": "پلگ ان منیجر", + "admin_plugins.available": "دستیاب پلگ ان", + "admin_plugins.available_not-found": "کوئی پلگ ان کائنی لبھے۔", + "admin_plugins.available_install.value": "انسٹال", + "admin_plugins.description": "تفصیل", + "admin_plugins.installed_uninstall.value": "ان انسٹال", + "admin_plugins.last-update": "چھیکڑی تبدیلی", + "admin_plugins.name": "ناں", + "admin_plugins.version": "ورژن", + "admin_plugins_info.parts": "انسٹال تھئے حصے", + "admin_plugins_info.plugins": "انسٹال تھئے پلگ ان", + "admin_plugins_info.version_number": "ورشن نمبر", + "admin_settings": "ترتیباں", + "admin_settings.current_save.value": "ترتیباں محفوظ کرو", + "admin_settings.page-title": "ترتیباں ــ ایتھرپیڈ", "index.newPad": "نواں پیڈ", "pad.toolbar.bold.title": "بولڈ(Ctrl+B)", "pad.toolbar.italic.title": "ترچھے (Ctrl+I)", "pad.toolbar.underline.title": "ہیٹھ لکیر (Ctrl+U)", + "pad.toolbar.indent.title": "حاشیہ (ٹیب)", + "pad.toolbar.unindent.title": "حاشیہ ٻاہر دوں (شفٹ + ٹیٻ)", + "pad.toolbar.undo.title": "اݨ کیتا (کنٹرول + زیڈ)", + "pad.toolbar.redo.title": "ولدا کرو (کنٹرول + وائی)", "pad.toolbar.savedRevision.title": "رویژن بچاؤ", "pad.toolbar.settings.title": "ترتیباں", "pad.colorpicker.save": "بچاؤ", @@ -16,6 +35,8 @@ "pad.settings.padSettings": "پیڈ ترتیباں", "pad.settings.fontType": "فونٹ قسم:", "pad.settings.language": "زبان:", + "pad.settings.about": "تعارف", + "pad.settings.poweredBy": "تکڑا کرݨ آلے", "pad.importExport.importSuccessful": "کامیاب!", "pad.importExport.exportetherpad": "ایتھرپیڈ", "pad.importExport.exporthtml": "ایچ ٹی ایم ایل", diff --git a/src/locales/sl.json b/src/locales/sl.json index e8e952d48..7e25a4d7e 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -9,130 +9,165 @@ "Upwinxp" ] }, + "admin.page-title": "Administratorska nadzorna plošča – Etherpad", + "admin_plugins": "Upravitelj vtičnikov", + "admin_plugins.available": "Razpoložljivi vtičniki", + "admin_plugins.available_not-found": "Ni najdenih vtičnikov.", + "admin_plugins.available_fetching": "Pridobivanje ...", + "admin_plugins.available_install.value": "Namesti", + "admin_plugins.available_search.placeholder": "Poiščite vtičnike za namestitev", "admin_plugins.description": "Opis", + "admin_plugins.installed": "Nameščeni vtičniki", + "admin_plugins.installed_fetching": "Pridobivanje nameščenih vtičnikov ...", + "admin_plugins.installed_nothing": "Namestili niste še nobenega vtičnika.", + "admin_plugins.installed_uninstall.value": "Odmesti", "admin_plugins.last-update": "Zadnja posodobitev", "admin_plugins.name": "Ime", + "admin_plugins.page-title": "Upravitelj vtičnikov – Etherpad", "admin_plugins.version": "Različica", + "admin_plugins_info": "Informacije o odpravljanju težav", + "admin_plugins_info.hooks": "Nameščene razširitvene točke", + "admin_plugins_info.hooks_client": "Razširitvene točke na strani odjemalca", + "admin_plugins_info.hooks_server": "Razširitvene točke na strani strežnika", + "admin_plugins_info.parts": "Nameščeni deli", + "admin_plugins_info.plugins": "Nameščeni vtičniki", + "admin_plugins_info.page-title": "Informacije o vtičniku – Etherpad", + "admin_plugins_info.version": "Različica Etherpada", + "admin_plugins_info.version_latest": "Najnovejša razpoložljiva različica", + "admin_plugins_info.version_number": "Številka različice", "admin_settings": "Nastavitve", + "admin_settings.current": "Trenutna konfiguracija", + "admin_settings.current_example-devel": "Zgled predloge za razvojne nastavitve", + "admin_settings.current_example-prod": "Zgled predloge za roizvodne nastavitve", + "admin_settings.current_restart.value": "Znova zaženi Etherpad", "admin_settings.current_save.value": "Shrani nastavitve", - "index.newPad": "Nov dokument", - "index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:", - "pad.toolbar.bold.title": "Krepko (Ctrl-B)", - "pad.toolbar.italic.title": "Ležeče (Ctrl-I)", - "pad.toolbar.underline.title": "Podčrtano (Ctrl-U)", - "pad.toolbar.strikethrough.title": "Prečrtano (Ctrl+5)", - "pad.toolbar.ol.title": "Urejen seznam (Ctrl+Shift+N)", - "pad.toolbar.ul.title": "Neurejen seznam (Ctrl+Shift+L)", + "admin_settings.page-title": "Nastavitve – Etherpad", + "index.newPad": "Nov blokec", + "index.createOpenPad": "ali pa ustvari/odpri blokec z imenom:", + "index.openPad": "odpri obstoječ blokec z imenom:", + "pad.toolbar.bold.title": "Krepko (Ctrl + B)", + "pad.toolbar.italic.title": "Ležeče (Ctrl + I)", + "pad.toolbar.underline.title": "Podčrtano (Ctrl + U)", + "pad.toolbar.strikethrough.title": "Prečrtano (Ctrl + 5)", + "pad.toolbar.ol.title": "Urejen seznam (Ctrl + dvigalka + N)", + "pad.toolbar.ul.title": "Neurejen seznam (Ctrl + dvigalka + L)", "pad.toolbar.indent.title": "Zamik desno (TAB)", - "pad.toolbar.unindent.title": "Zamik levo (Shift+TAB)", - "pad.toolbar.undo.title": "Razveljavi (Ctrl-Z)", - "pad.toolbar.redo.title": "Ponovno uveljavi (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Počisti barve avtorstva (Ctrl+Shift+C)", - "pad.toolbar.import_export.title": "Uvozi/Izvozi različne oblike zapisov", + "pad.toolbar.unindent.title": "Zmanjšanje zamika (Shift+TAB)", + "pad.toolbar.undo.title": "Razveljavi (Ctrl + Z)", + "pad.toolbar.redo.title": "Znova uveljavi (Ctrl + Y)", + "pad.toolbar.clearAuthorship.title": "Počisti barve avtorstva (Ctrl + dvigalka + C)", + "pad.toolbar.import_export.title": "Uvozi/Izvozi iz/v različne datotečne formate", "pad.toolbar.timeslider.title": "Časovni trak", "pad.toolbar.savedRevision.title": "Shrani redakcijo", "pad.toolbar.settings.title": "Nastavitve", - "pad.toolbar.embed.title": "Deli in vključi ta dokument", - "pad.toolbar.showusers.title": "Pokaži uporabnike dokumenta", + "pad.toolbar.embed.title": "Deli in vključi ta blokec", + "pad.toolbar.showusers.title": "Pokaži uporabnike blokca", "pad.colorpicker.save": "Shrani", "pad.colorpicker.cancel": "Prekliči", "pad.loading": "Nalaganje ...", - "pad.noCookie": "Piškotka ni bilo mogoče najti. Prosimo, dovolite piškotke v vašem brskalniku!", - "pad.permissionDenied": "Nimate dovoljenja za dostop do tega dokumenta.", - "pad.settings.padSettings": "Nastavitve dokumenta", + "pad.noCookie": "Piškotka ni bilo mogoče najti. Prosimo, dovolite piškotke v vašem brskalniku! Vaša seja in nastavitve se med obiski ne bodo shranili. Razlog za to je morda, da je Etherpad v nekaterih brskalnikih vključen v iFrame. Zsgotovite, da je Etherpad na isti poddomeni/domeni kot nadrejeni iFrame.", + "pad.permissionDenied": "Nimate dovoljenja za dostop do tega bkokca.", + "pad.settings.padSettings": "Nastavitve blokca.", "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Vsebina klepeta je vedno na zaslonu", "pad.settings.chatandusers": "Prikaži klepet in uporabnike", "pad.settings.colorcheck": "Barve avtorstva", "pad.settings.linenocheck": "Številke vrstic", - "pad.settings.rtlcheck": "Ali naj se vsebina prebira od desne proti levi?", + "pad.settings.rtlcheck": "Ali naj se vsebina bera od desne proti levi?", "pad.settings.fontType": "Vrsta pisave:", "pad.settings.fontType.normal": "Normalno", "pad.settings.language": "Jezik:", + "pad.settings.about": "Kolofon", "pad.settings.poweredBy": "Omogoča", "pad.importExport.import_export": "Uvoz/Izvoz", - "pad.importExport.import": "Naloži katerokoli besedilno datoteko ali dokument.", - "pad.importExport.importSuccessful": "Opravilo je uspešno končano!", - "pad.importExport.export": "Izvozi trenutni dokument kot:", + "pad.importExport.import": "Naložite katero koli besedilno datoteko ali dokument.", + "pad.importExport.importSuccessful": "Uspešno!", + "pad.importExport.export": "Izvozi trenutni blokec kot:", "pad.importExport.exportetherpad": "Etherpad", - "pad.importExport.exporthtml": "HTML (oblikovano besedilo)", - "pad.importExport.exportplain": "TXT (neoblikovano besedilo)", - "pad.importExport.exportword": "DOC (zapis Microsoft Word)", - "pad.importExport.exportpdf": "PDF (zapis Acrobat PDF)", - "pad.importExport.exportopen": "ODF (zapis Open Document)", - "pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le neoblikovano besedilo in zapise HTML. Za naprednejše možnosti uvoza namestite program AbiWord.", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Golo besedilo", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF", + "pad.importExport.abiword.innerHTML": "Uvoziti je mogoče le golo besedilo in formate HTML. Za naprednejše možnosti uvoza namestite program AbiWord.", "pad.modals.connected": "Povezano.", - "pad.modals.reconnecting": "Poteka povezovanje z dokumentom ...", + "pad.modals.reconnecting": "Poteka povezovanje z blokcem ...", "pad.modals.forcereconnect": "Vsili ponovno povezavo", "pad.modals.reconnecttimer": "Poskus ponovne vzpostavitve povezave čez", "pad.modals.cancel": "Prekliči", - "pad.modals.userdup": "Dokument je že odprt v v drugem oknu", - "pad.modals.userdup.explanation": "Videti je, da je ta dokument odprt v več kot enem oknu brskalnika na tem računalniku.", - "pad.modals.userdup.advice": "Ponovno vzpostavite povezavo in uporabljajte to okno.", + "pad.modals.userdup": "Blokec je že odprt v drugem oknu", + "pad.modals.userdup.explanation": "Videti je, da je ta blokec na tem računalniku odprt v več kot enem oknu brskalnika.", + "pad.modals.userdup.advice": "Znova vzpostavite povezavo in uporabljajte to okno.", "pad.modals.unauth": "Nepooblaščen dostop", - "pad.modals.unauth.explanation": "Med ogledovanjem strani so se dovoljenja za ogled spremenila. Poskusite se znova povezati.", - "pad.modals.looping.explanation": "Zaznane so težave pri komunikaciji s strežnikom za usklajevanje.", - "pad.modals.looping.cause": "Morda ste se povezali skozi neustrezno nastavljen požarni zid ali s posredniškim strežnikom.", + "pad.modals.unauth.explanation": "Med ogledovanjem strani so se vaša dovoljenja za ogled spremenila. Poskusite se znova povezati.", + "pad.modals.looping.explanation": "Pri komunikaciji s sinhronizacijskim strežnikom je prišlo do težav.", + "pad.modals.looping.cause": "Morda ste se povezali skozi neustrezno nastavljen požarni zid ali prek posredniškega strežnika.", "pad.modals.initsocketfail": "Strežnik je nedosegljiv.", - "pad.modals.initsocketfail.explanation": "Povezava s strežnikom za usklajevanje ni mogoča.", - "pad.modals.initsocketfail.cause": "Najverjetneje gre za težavo z vašim brskalnikom, ali internetno povezavo.", + "pad.modals.initsocketfail.explanation": "Povezovanje s sinhronizacijskim strežnikom ni uspelo.", + "pad.modals.initsocketfail.cause": "Najverjetneje gre za težavo z vašim brskalnikom ali internetno povezavo.", "pad.modals.slowcommit.explanation": "Strežnik se ne odziva.", "pad.modals.slowcommit.cause": "Možen vzrok so težave z omrežno povezljivostjo.", - "pad.modals.badChangeset.explanation": "Urejanje, ki ste ga naredili, je strežnik za usklajevanje označil kot nedovoljeno.", - "pad.modals.badChangeset.cause": "Razlog za to je morda napačna konfiguracija strežnika ali neko drugo nepričakovano vedenje. Prosimo, stopite v stik z skrbnikom storitve, če menite, da gre za napako. Poskusite se ponovno povezati, da nadaljujete z urejanjem.", - "pad.modals.corruptPad.explanation": "Dokument, do katerega želite dostopati, je poškodovan.", - "pad.modals.corruptPad.cause": "Razlog za to je morda napačna konfiguracija strežnika ali neko drugo nepričakovano vedenje. Prosimo, stopite v stik s skrbnikom storitve.", + "pad.modals.badChangeset.explanation": "Urejanje, ki ste ga naredili, je sinhronizacijski strežnik prepoznal kot nedovoljeno.", + "pad.modals.badChangeset.cause": "Razlog za to je morda napačna konfiguracija strežnika ali neko drugo nepričakovano vedenje. Če menite, da gre za napako, stopite v stik z administratorjem storitve. Za nadaljevanje urejanja se poskusite znova povezati.", + "pad.modals.corruptPad.explanation": "Blokec, do katerega želite dostopati, je poškodovan.", + "pad.modals.corruptPad.cause": "Razlog za to je morda napačna konfiguracija strežnika ali neko drugo nepričakovano vedenje. Prosimo, stopite v stik z administratorjem storitve.", "pad.modals.deleted": "Izbrisano.", - "pad.modals.deleted.explanation": "Dokument je odstranjen.", + "pad.modals.deleted.explanation": "Blokec je odstranjen.", + "pad.modals.rateLimited": "Omejena hitrost.", + "pad.modals.rateLimited.explanation": "Na ta blokec ste poslali preveč sporočil, zato ste bili odklopljeni.", + "pad.modals.rejected.explanation": "Strežnik je zavrnil sporočilo, ki ga je poslal vaš brskalnik.", + "pad.modals.rejected.cause": "Strežnik je bil morda posodobljen, ko ste si ogledovali blokec, ali pa je v Etherpadu napaka. Poskusite znova naložiti stran.", "pad.modals.disconnected": "Vaša povezava je bila prekinjena.", "pad.modals.disconnected.explanation": "Povezava s strežnikom je bila izgubljena.", - "pad.modals.disconnected.cause": "Strežnik morda ni na voljo. Prosimo, obvestite skrbnika storitve, če se to zgodi večkrat.", - "pad.share": "Določi souporabo dokumenta", + "pad.modals.disconnected.cause": "Strežnik morda ni na voljo. Če se to ponavlja, obvestite administratorja storitve.", + "pad.share": "Deljenje blokca", "pad.share.readonly": "Samo za branje", "pad.share.link": "Povezava", "pad.share.emebdcode": "URL za vključitev", "pad.chat": "Klepet", - "pad.chat.title": "Odpri klepetalno okno dokumenta.", + "pad.chat.title": "Odpri klepetalno okno za blokec.", "pad.chat.loadmessages": "Naloži več sporočil", "pad.chat.stick.title": "Prilepi klepet na zaslon", "pad.chat.writeMessage.placeholder": "Napišite sporočilo", + "timeslider.followContents": "Spremljajte posodobitve vsebine blokca", "timeslider.pageTitle": "Časovni trak {{appTitle}}", - "timeslider.toolbar.returnbutton": "Vrni se na dokument", + "timeslider.toolbar.returnbutton": "Nazaj na blokec", "timeslider.toolbar.authors": "Avtorji:", "timeslider.toolbar.authorsList": "Ni določenih avtorjev", "timeslider.toolbar.exportlink.title": "Izvozi", "timeslider.exportCurrent": "Izvozi trenutno različico kot:", "timeslider.version": "Različica {{version}}", "timeslider.saved": "Shranjeno {{day}}.{{month}}.{{year}}", - "timeslider.playPause": "Predvajaj/začasno ustavi vsebino dokumenta", - "timeslider.backRevision": "Pojdi eno redakcijo nazaj v tem dokumentu", - "timeslider.forwardRevision": "Pojdi eno redakcijo naprej v tem dokumentu", + "timeslider.playPause": "Predvajaj/zaustavi vsebino blokca", + "timeslider.backRevision": "Pojdi v tem blokcu eno redakcijo nazaj", + "timeslider.forwardRevision": "Pojdi v tem blokcu eno redakcijo naprej", "timeslider.dateformat": "{{day}}.{{month}}.{{year}} {{hours}}:{{minutes}}:{{seconds}}", - "timeslider.month.january": "Januar", - "timeslider.month.february": "Februar", - "timeslider.month.march": "Marec", - "timeslider.month.april": "April", - "timeslider.month.may": "Maj", - "timeslider.month.june": "Junij", - "timeslider.month.july": "Julij", - "timeslider.month.august": "August", - "timeslider.month.september": "September", - "timeslider.month.october": "Oktober", - "timeslider.month.november": "November", - "timeslider.month.december": "December", + "timeslider.month.january": "januarja", + "timeslider.month.february": "februarja", + "timeslider.month.march": "marca", + "timeslider.month.april": "aprila", + "timeslider.month.may": "maja", + "timeslider.month.june": "junija", + "timeslider.month.july": "julija", + "timeslider.month.august": "avgusta", + "timeslider.month.september": "septembra", + "timeslider.month.october": "oktobra", + "timeslider.month.november": "novembra", + "timeslider.month.december": "decembra", "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovan avtor, plural(num) two: neimenovana avtorja, plural(num) few: neimenovani avtorji, other: neimenovanih avtorjev ]}", "pad.savedrevs.marked": "Ta redakcija je zdaj označena kot shranjena redakcija", - "pad.savedrevs.timeslider": "Shranjene redakcije si lahko ogledate s pomočjo časovnega traku", + "pad.savedrevs.timeslider": "Shranjene redakcije si lahko ogledate z odprtjem časovnega traku", "pad.userlist.entername": "Vnesite svoje ime", "pad.userlist.unnamed": "neimenovana oseba", - "pad.editbar.clearcolors": "Ali naj se počistijo barve avtorstva v celotnem dokumentu?", + "pad.editbar.clearcolors": "Ali naj se počistijo barve avtorstva v vsem dokumentu? Tega ni mogoče razveljaviti.", "pad.impexp.importbutton": "Uvozi takoj", "pad.impexp.importing": "Poteka uvažanje ...", - "pad.impexp.confirmimport": "Uvoz datoteke prepiše obstoječe besedilo dokumenta. Ali ste prepričani, da želite nadaljevati?", - "pad.impexp.convertFailed": "Datoteke ni bilo mogoče uvoziti. Prosimo uporabite drug podprt zapis dokumenta ali pa vsebino prilepite ročno", - "pad.impexp.padHasData": "Nismo mogli uvoziti datoteke, ker dokument že vsebuje spremembe. Prosimo, uvozite datoteko v nov dokument", + "pad.impexp.confirmimport": "Uvoz datoteke bo prepisal obstoječe besedilo blokca. Ali res želite nadaljevati?", + "pad.impexp.convertFailed": "Datoteke ni bilo mogoče uvoziti. Prosimo, uporabite drug format dokumenta ali pa vsebino kopirajte in prilepite ročno.", + "pad.impexp.padHasData": "Datoteke ni bilo mogoče uvoziti, ker blokec že vsebuje spremembe. Prosimo, uvozite datoteko v nov blokec.", "pad.impexp.uploadFailed": "Nalaganje je spodletelo, prosimo poskusite znova", "pad.impexp.importfailed": "Uvoz je spodletel", "pad.impexp.copypaste": "Vsebino kopirajte in prilepite", - "pad.impexp.exportdisabled": "Izvoz v zapis {{type}} je onemogočen. Za več podrobnosti stopite v stik s skrbnikom." + "pad.impexp.exportdisabled": "Izvoz v format {{type}} je onemogočen. Za več podrobnosti stopite v stik z administratorjem.", + "pad.impexp.maxFileSize": "Datoteka je prevelika. Za povečanje dovoljene velikosti datoteke za uvoz se obrnite na administratorja spletnega mesta" } diff --git a/src/locales/sms.json b/src/locales/sms.json new file mode 100644 index 000000000..3ea65a3cb --- /dev/null +++ b/src/locales/sms.json @@ -0,0 +1,60 @@ +{ + "@metadata": { + "authors": [ + "Yupik" + ] + }, + "admin_plugins.description": "Deskriptt", + "admin_plugins.name": "Nõmm", + "admin_plugins.version": "Versio", + "admin_plugins_info.version": "Etherpad-versio", + "admin_plugins_info.version_number": "Versionââmar", + "admin_settings": "Asetõõzz", + "admin_settings.current_save.value": "Ruõkk asetõõzzid", + "admin_settings.page-title": "Asetõõzz - Etherpad", + "index.newPad": "Ođđ mošttʼtõspõʹmmai", + "index.createOpenPad": "leʼbe raaj leʼbe ääʹved mošttʼtõspõʹmmai nõõmin:", + "pad.toolbar.underline.title": "Vuâllacertldâsttmõš (CTRL-U)", + "pad.toolbar.undo.title": "Kååʹmet (Ctrl+Z)", + "pad.toolbar.savedRevision.title": "Ruõkk muttâz", + "pad.toolbar.settings.title": "Asetõõzz", + "pad.toolbar.showusers.title": "Čuäʼjet tän mošttʼtõspõʹmmai õõʹnnʼjid", + "pad.colorpicker.save": "Ruõkk", + "pad.colorpicker.cancel": "Jõõsk", + "pad.settings.padSettings": "Mošttʼtõspõʹmmai asetõõzz", + "pad.settings.chatandusers": "Čuäʹjet čääʹtt da õõʹnnʼjid", + "pad.settings.language": "Ǩiõll:", + "pad.settings.about": "Lââʹssteâđ", + "pad.settings.poweredBy": "Kääzzkõõzz vueiʹtlvâstt", + "pad.importExport.importSuccessful": "Oʹnnsti!", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.modals.cancel": "Jõõsk", + "pad.modals.slowcommit.explanation": "Server ij vaʹstted.", + "pad.modals.deleted": "Jaukkuum.", + "pad.modals.deleted.explanation": "Tät mošttʼtõspõʹmmai lij jaukkuum.", + "pad.share": "Jueʼjj mošttʼtõspõʹmmai", + "pad.share.link": "Liŋkk", + "pad.chat": "Čäʹtt", + "pad.chat.writeMessage.placeholder": "Ǩeeʹrjet jiijjad saaǥǥ täzz", + "timeslider.toolbar.returnbutton": "Määʹcc mošttʼtõspõʹmma", + "timeslider.version": "Versio {{version}}", + "timeslider.saved": "Ruõkkum {{month}} {{day}}. peeiʹv {{year}}", + "timeslider.dateformat": "{{day}}.{{month}}.{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "ođđeeʹjjmannu", + "timeslider.month.february": "täʹlvvmannu", + "timeslider.month.march": "pâʹsslašttâm-mannu", + "timeslider.month.april": "njuhččmannu", + "timeslider.month.may": "vueʹssmannu", + "timeslider.month.june": "ǩieʹssmannu", + "timeslider.month.july": "sueiʹnnmannu", + "timeslider.month.august": "påʹrǧǧmannu", + "timeslider.month.september": "čõhččmannu", + "timeslider.month.october": "kålggmannu", + "timeslider.month.november": "skamm-mannu", + "timeslider.month.december": "rosttovmannu", + "pad.userlist.entername": "Ǩeeʹrjet jiijjad nõõm" +} diff --git a/src/locales/sq.json b/src/locales/sq.json index 0c59f590d..b6c2fa285 100644 --- a/src/locales/sq.json +++ b/src/locales/sq.json @@ -7,7 +7,7 @@ "Liridon" ] }, - "admin.page-title": "Paneli i Administratorit - Etherpad", + "admin.page-title": "Pult Përgjegjësi - Etherpad", "admin_plugins": "Përgjegjës shtojcash", "admin_plugins.available": "Shtojca të gatshme", "admin_plugins.available_not-found": "S’u gjetën shtojca.", @@ -63,7 +63,7 @@ "pad.colorpicker.save": "Ruaje", "pad.colorpicker.cancel": "Anuloje", "pad.loading": "Po ngarkohet…", - "pad.noCookie": "S’u gjet dot cookie. Ju lutemi, lejoni cookie-t te shfletuesi juaj! Sesioni dhe rregullimet tuaja s’do të ruhen nga një sesion në tjetër. Kjo mund të vijë ngaqë Etherpad përfshihet brenda një iFrame në disa shfletues. Ju lutemi, sigurohuni që Etherpad-i të jetë në të njëjtën nënpërkatësi/përkatësi si iFrame-i mëmë.", + "pad.noCookie": "S’u gjet dot cookie. Ju lutemi, lejoni cookie-t te shfletuesi juaj! Sesioni dhe rregullimet tuaja s’do të ruhen nga një sesion në tjetër. Kjo mund të vijë ngaqë, në disa shfletues, Etherpad përfshihet brenda një iFrame. Ju lutemi, sigurohuni që Etherpad-i të jetë në të njëjtën nënpërkatësi/përkatësi si iFrame-i mëmë.", "pad.permissionDenied": "S’keni leje të hyni në këtë bllok", "pad.settings.padSettings": "Rregullime Blloku", "pad.settings.myView": "Pamja Ime", @@ -78,7 +78,7 @@ "pad.settings.about": "Mbi", "pad.settings.poweredBy": "Bazuar në", "pad.importExport.import_export": "Import/Eksport", - "pad.importExport.import": "Ngarkoni cilëndo kartelë teksti ose dokument", + "pad.importExport.import": "Ngarkoni cilëndo kartelë tekst ose dokument", "pad.importExport.importSuccessful": "Me sukses!", "pad.importExport.export": "Eksportojeni bllokun e tanishëm si:", "pad.importExport.exportetherpad": "Etherpad", diff --git a/src/locales/sr-ec.json b/src/locales/sr-ec.json index e2917dab9..156715af0 100644 --- a/src/locales/sr-ec.json +++ b/src/locales/sr-ec.json @@ -9,6 +9,7 @@ "Obsuser", "Srdjan m", "Srđan", + "Zenfiric", "Милан Јелисавчић" ] }, @@ -34,7 +35,6 @@ "pad.colorpicker.save": "Сачувај", "pad.colorpicker.cancel": "Откажи", "pad.loading": "Учитавам…", - "pad.noCookie": "Колачић није пронађен. Молимо да укључите колачиће у вашем прегледавачу!", "pad.permissionDenied": "Немате дозволу да приступите овом паду", "pad.settings.padSettings": "Подешавања пада", "pad.settings.myView": "Мој приказ", diff --git a/src/locales/sro.json b/src/locales/sro.json new file mode 100644 index 000000000..03578c299 --- /dev/null +++ b/src/locales/sro.json @@ -0,0 +1,162 @@ +{ + "@metadata": { + "authors": [ + "Adr mm", + "F Samaritani" + ] + }, + "admin.page-title": "Pannellu amministrativu - Etherpad", + "admin_plugins": "Gestore de connetores", + "admin_plugins.available": "Connetores a disponimentu", + "admin_plugins.available_not-found": "Nissunu connetore a disponimentu", + "admin_plugins.available_fetching": "Recuperende...", + "admin_plugins.available_install.value": "Installa", + "admin_plugins.available_search.placeholder": "Chirca connetores de installare", + "admin_plugins.description": "Descritzione", + "admin_plugins.installed": "Connetores installados", + "admin_plugins.installed_fetching": "Recuperende connetores installados...", + "admin_plugins.installed_nothing": "No as installadu ancora nissunu connetore.", + "admin_plugins.installed_uninstall.value": "Disinstalla", + "admin_plugins.last-update": "Ùrtima atualizatzione", + "admin_plugins.name": "Nòmine", + "admin_plugins.page-title": "Gestore de connetores - Etherpad", + "admin_plugins.version": "Versione", + "admin_plugins_info": "Informatzione pro sa risolutzione de problemas", + "admin_plugins_info.parts": "Partes installadas", + "admin_plugins_info.plugins": "Connetores installados", + "admin_plugins_info.page-title": "Informatzione de su connetore - Etherpad", + "admin_plugins_info.version": "Versione de Etherpad", + "admin_plugins_info.version_latest": "Ùrtima versione a disponimentu", + "admin_plugins_info.version_number": "Nùmeru de versione", + "admin_settings": "Cunfiguratzione", + "admin_settings.current": "Cunfiguratzione atuale", + "admin_settings.current_restart.value": "Torra a aviare Etherpad", + "admin_settings.current_save.value": "Sarva sa cunfiguratzione", + "admin_settings.page-title": "Cunfiguratzione - Etherpad", + "index.newPad": "Pad nou", + "index.createOpenPad": "o crea/aberi unu pad cun su nòmine:", + "index.openPad": "aberi unu pad esistente cun su nòmine:", + "pad.toolbar.bold.title": "Grassetu (Ctrl+B)", + "pad.toolbar.italic.title": "Cursivu (Ctrl+I)", + "pad.toolbar.underline.title": "Sutaliniadu (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Istangadu (Ctrl+5)", + "pad.toolbar.ol.title": "Lista numerada (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Lista cun puntos (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Indentatzione a dereta (Tab)", + "pad.toolbar.unindent.title": "Indentatzione a manca (Shift+Tab)", + "pad.toolbar.undo.title": "Iscontza (Ctrl+Z)", + "pad.toolbar.redo.title": "Torra a fàghere (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Lìmpia is colores de autoria (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Importa/esporta dae/a formados de archìviu diferentes", + "pad.toolbar.timeslider.title": "Presentatzione cronologia", + "pad.toolbar.savedRevision.title": "Sarva sa versione", + "pad.toolbar.settings.title": "Cunfiguratzione", + "pad.toolbar.embed.title": "Cumpartzi e incòrpora custu Pad", + "pad.toolbar.showusers.title": "Ammustra is utentes in custu Pad", + "pad.colorpicker.save": "Sarva", + "pad.colorpicker.cancel": "Annulla", + "pad.loading": "Carrighende...", + "pad.noCookie": "Su testimòngiu no est istètiu agatadu. Permite is testimòngios in su navigadore tuo. Sa sessione e sa cunfiguratzione tuas no ant a èssere sarvadas intre bìsitas. Podet èssere pro more de s'inclusione de Etherpad comente iFrame in tzertos navigadores. Assegura·ti chi Etherpad s'agatat in su pròpiu sutadomìniu/domìniu chi s'iFrame printzipale", + "pad.permissionDenied": "Non tenes permissu pro atzèdere a custu pad", + "pad.settings.padSettings": "Cunfiguratzione de su pad", + "pad.settings.myView": "Sa visualizatzione mia", + "pad.settings.stickychat": "Ammustra semper sa tzarrada", + "pad.settings.chatandusers": "Ammustra sa tzarrada e is utentes", + "pad.settings.colorcheck": "Colores de autoria", + "pad.settings.linenocheck": "Nùmeros de lìnia", + "pad.settings.rtlcheck": "Cuntenutu dae manca a dereta", + "pad.settings.fontType": "Tipu de caràtere:", + "pad.settings.fontType.normal": "Normale", + "pad.settings.language": "Lìngua:", + "pad.settings.about": "Informatziones", + "pad.settings.poweredBy": "Realizadu cun", + "pad.importExport.import_export": "Importatzione/esportatzione", + "pad.importExport.import": "Càrriga un'archìviu de testu o unu documentu", + "pad.importExport.importSuccessful": "Carrigadu.", + "pad.importExport.export": "Esporta su pad atuale comente:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Testu sèmplitze", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.importExport.abiword.innerHTML": "Isceti is formados de testu sèmplitze o HTML podent èssere importados. Pro mètodos avantzados de importatzione, installa AbiWord o LibreOffice.", + "pad.modals.connected": "Connètidu.", + "pad.modals.reconnecting": "Connetende a su pad tuo...", + "pad.modals.forcereconnect": "Fortza sa connesione", + "pad.modals.reconnecttimer": "Torrende a connètere in", + "pad.modals.cancel": "Annulla", + "pad.modals.userdup": "Abertu in un'àtera ventana", + "pad.modals.userdup.explanation": "Podet èssere chi custu pad siat abertu in un'àtera ischeda de custu navigadore in custu ordinadore.", + "pad.modals.userdup.advice": "Torra a connètere pro impreare custa ventana.", + "pad.modals.unauth": "Chena autorizatzione", + "pad.modals.unauth.explanation": "Is permissos tuos funt istètios cambiados in su mentras chi fias bidende custa pàgina. Prova de ti torrare a connètere.", + "pad.modals.looping.explanation": "Nci funt problemas de comunicatzione cun su serbidore de sincronizatzione.", + "pad.modals.looping.cause": "Forsis sa connessione at impreadu unu serbidore intermediàriu (proxy) o unu firewall chi no est cumpatìbile.", + "pad.modals.initsocketfail": "Su serbidore no est atzessìbile.", + "pad.modals.initsocketfail.explanation": "Impossìbile connètere cun su serbidore de sincronizatzione.", + "pad.modals.initsocketfail.cause": "Podet èssere a càusa de unu problema cun su navigadore tuo o cun sa connessione de internet.", + "pad.modals.slowcommit.explanation": "Su serbidore non rispondet.", + "pad.modals.slowcommit.cause": "Podet èssere a càusa de problemas cun sa connessione de internet.", + "pad.modals.badChangeset.explanation": "Una modìfica tua est istètia cunsiderada illegale dae su serbidore de sincronizatzione.", + "pad.modals.badChangeset.cause": "Podet èssere a càusa de una cunfiguratzione de serbidore isballiada o calicunu àteru cumportamentu imprevistu. Iscrie a s'amministratzione de su servìtziu si pensas chi siat un'errore. Prova a connètere torra pro sighire a modificare.", + "pad.modals.corruptPad.explanation": "Su pad a su chi ses chirchende de atzèdere est dannadu.", + "pad.modals.corruptPad.cause": "Podet èssere a càusa de una cunfiguratzione de serbidore non curreta o pro unu cumportamentu imprevistu. Iscrie a s'amministratzione de su servìtziu.", + "pad.modals.deleted": "Cantzelladu.", + "pad.modals.deleted.explanation": "Pad cantzelladu.", + "pad.modals.rateLimited.explanation": "As imbiadu tropu messàgios a custu pad e t'at disconnètidu.", + "pad.modals.rejected.explanation": "Su serbidore at rifiutadu unu messàgiu imbiadu dae su navigadore tuo.", + "pad.modals.rejected.cause": "Podet èssere chi su serbidore siat istètiu atualizadu in su mentras chi fias bidende su pad, o podet èssere chi nci siat un'errore in Etherpad. Prova a atualizare sa pàgina.", + "pad.modals.disconnected": "Disconnètidu.", + "pad.modals.disconnected.explanation": "Connessione cun su serbidore pèrdida.", + "pad.modals.disconnected.cause": "Su serbidore no est a disponimentu. Iscrie a s'amministratzione de su servìtziu si su problema persistet.", + "pad.share": "Cumpartzi custu pad", + "pad.share.link": "Ligòngiu", + "pad.share.emebdcode": "Incòrpora URL", + "pad.chat": "Tzarrada", + "pad.chat.title": "Aberi sa tzarrada pro custu pad.", + "pad.chat.loadmessages": "Càrriga àteros messàgios", + "pad.chat.stick.title": "Apica sa tzarrada in s'ischermu", + "pad.chat.writeMessage.placeholder": "Iscrie su messàgiu tuo inoghe", + "timeslider.followContents": "Sighi is atualizatziones de cuntenutu de su pad", + "timeslider.pageTitle": "Cronologia {{appTitle}}", + "timeslider.toolbar.returnbutton": "Torra a su pad", + "timeslider.toolbar.authors": "Autores:", + "timeslider.toolbar.authorsList": "Nissunu autore", + "timeslider.toolbar.exportlink.title": "Esporta", + "timeslider.exportCurrent": "Esporta sa versione atuale comente:", + "timeslider.version": "Versione {{version}}", + "timeslider.saved": "Sarvadu su {{day}} de {{month}} de su {{year}}", + "timeslider.playPause": "Riprodutzione/pàusa de is cuntenutos de su pad", + "timeslider.backRevision": "Bae a una versione pretzedente de custu pad", + "timeslider.forwardRevision": "Bae a una versione imbeniente de custu pad", + "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.month.january": "Ghennàrgiu", + "timeslider.month.february": "Freàrgiu", + "timeslider.month.march": "Martzu", + "timeslider.month.april": "Abrile", + "timeslider.month.may": "Maju", + "timeslider.month.june": "Làmpadas", + "timeslider.month.july": "Mese de argiolas", + "timeslider.month.august": "Austu", + "timeslider.month.september": "Cabudanni", + "timeslider.month.october": "Ledàmine", + "timeslider.month.november": "Onniasantu", + "timeslider.month.december": "Mese de idas", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: autore, other: autores ]} chena nòmine", + "pad.savedrevs.marked": "Custa revisione est istètia marcada comente revisione sarvada", + "pad.savedrevs.timeslider": "Podes bìdere is versiones sarvadas bisitende sa cronologia", + "pad.userlist.entername": "Inserta su nòmine tuo", + "pad.userlist.unnamed": "Chena nòmine", + "pad.editbar.clearcolors": "Seguru chi boles limpiare is colores de autoria de totu su documentu? Custa atzione no dda podes annullare", + "pad.impexp.importbutton": "Importa immoe", + "pad.impexp.importing": "Importende...", + "pad.impexp.confirmimport": "S'importatzione de un'archìviu at a subraiscrìere su testu atuale de su pad. Seguru chi boles sighire?", + "pad.impexp.convertFailed": "Impossìbile importare custu archìviu. Imprea unu formadu de documentu diferente o còpia e incolla a manu", + "pad.impexp.padHasData": "Impossìbile importare custu archìviu pro ite custu pad est istètiu giai modificadu. Importa·ddu in unu pad nou", + "pad.impexp.uploadFailed": "Errore in sa càrriga. Torra a provare", + "pad.impexp.importfailed": "Errore de importatzione", + "pad.impexp.copypaste": "Còpia e incolla", + "pad.impexp.exportdisabled": "S'esportatzione comente {{type}} est disativada. Iscrie a s'amministratzione de su sistema pro àteras informatziones.", + "pad.impexp.maxFileSize": "S'archìviu est tropu manu. Iscrie a s'amministratzione pro ismanniare sa dimensione permìtida pro s'importatzione" +} diff --git a/src/locales/th.json b/src/locales/th.json index 573c7a761..7de1e4fdc 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -3,7 +3,8 @@ "authors": [ "Aefgh39622", "Andibecker", - "Patsagorn Y." + "Patsagorn Y.", + "Trisorn Triboon" ] }, "admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad", @@ -16,12 +17,12 @@ "admin_plugins.description": "คำอธิบาย", "admin_plugins.installed": "ปลั๊กอินที่ติดตั้ง", "admin_plugins.installed_fetching": "กำลังเรียกปลั๊กอินที่ติดตั้ง…", - "admin_plugins.installed_nothing": "คุณยังไม่ได้ติดตั้งปลั๊กอินใดๆ", + "admin_plugins.installed_nothing": "คุณยังไม่ได้ติดตั้งปลั๊กอินใด ๆ", "admin_plugins.installed_uninstall.value": "ถอนการติดตั้ง", "admin_plugins.last-update": "การปรับปรุงครั้งล่าสุด", "admin_plugins.name": "ชื่อ", "admin_plugins.page-title": "ตัวจัดการปลั๊กอิน - Etherpad", - "admin_plugins.version": "เวอร์ชั่น", + "admin_plugins.version": "เวอร์ชัน", "admin_plugins_info": "ข้อมูลการแก้ไขปัญหา", "admin_plugins_info.hooks": "ติดตั้งตะขอ", "admin_plugins_info.hooks_client": "ตะขอฝั่งไคลเอ็นต์", @@ -35,7 +36,7 @@ "admin_settings": "การตั้งค่า", "admin_settings.current": "การกำหนดค่าปัจจุบัน", "admin_settings.current_example-devel": "ตัวอย่างเทมเพลตการตั้งค่าการพัฒนา", - "admin_settings.current_example-prod": "ตัวอย่างเทมเพลตการตั้งค่าการผลิต", + "admin_settings.current_example-prod": "ตัวอย่างแม่แบบการตั้งค่าการผลิต", "admin_settings.current_restart.value": "รีสตาร์ท Etherpad", "admin_settings.current_save.value": "บันทึกการตั้งค่า", "admin_settings.page-title": "การตั้งค่า - Etherpad", @@ -74,9 +75,9 @@ "pad.settings.fontType": "ชนิดแบบอักษร:", "pad.settings.language": "ภาษา:", "pad.settings.about": "เกี่ยวกับ", - "pad.settings.poweredBy": "ขับเคลื่อนโดย $1", + "pad.settings.poweredBy": "ขับเคลื่อนโดย", "pad.importExport.import_export": "นำเข้า/ส่งออก", - "pad.importExport.import": "อัปโหลดไฟล์ข้อความหรือเอกสารใดๆ", + "pad.importExport.import": "อัปโหลดไฟล์ข้อความหรือเอกสารใด ๆ", "pad.importExport.importSuccessful": "สำเร็จ!", "pad.importExport.export": "ส่งออกแผ่นจดบันทึกปัจจุบันเป็น:", "pad.importExport.exportetherpad": "Etherpad", @@ -124,7 +125,7 @@ "pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้", "pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม", "pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ", - "pad.chat.writeMessage.placeholder": "เขียนข้อความ...", + "pad.chat.writeMessage.placeholder": "เขียนข้อความของคุณที่นี่", "timeslider.followContents": "ติดตามการอัพเดตเนื้อหาแพด", "timeslider.pageTitle": "ตัวเลื่อนเวลา {{appTitle}}", "timeslider.toolbar.returnbutton": "กลับไปแผ่นจดบันทึก", @@ -132,7 +133,7 @@ "timeslider.toolbar.authorsList": "ไม่มีผู้เขียน", "timeslider.toolbar.exportlink.title": "ส่งออก", "timeslider.exportCurrent": "ส่งออกรุ่นปัจจุบันเป็น:", - "timeslider.version": "รุ่น {{version}}", + "timeslider.version": "เวอร์ชัน {{version}}", "timeslider.saved": "บันทึกแล้วเมื่อ {{day}} {{month}} {{year}}", "timeslider.playPause": "เล่น / พักเนื้อหาแผ่นจดบันทึก", "timeslider.backRevision": "กลับไปรุ่นแก้ไขเก่าของแผ่นจดบันทึกนี้", @@ -153,7 +154,7 @@ "timeslider.unnamedauthors": "{{num}} ผู้เขียนที่ไม่มีชื่อ", "pad.savedrevs.marked": "รุ่นแก้ไขนี้ถูกทำเครื่องหมายเป็นรุ่นแก้ไขที่บันทึกแล้ว", "pad.savedrevs.timeslider": "คุณสามารถดูรุ่นแก้ไขที่บันทึกแล้วโดยเยี่ยมชมตัวเลื่อนเวลา", - "pad.userlist.entername": "กรอกชื่อของคุณ", + "pad.userlist.entername": "ป้อนชื่อของคุณ", "pad.userlist.unnamed": "ไม่มีชื่อ", "pad.editbar.clearcolors": "ลบการเน้นความเป็นเจ้าของข้อความหรือไม่? การกระทำนี่ไม่สามารถย้อนได้", "pad.impexp.importbutton": "นำเข้าตอนนี้", diff --git a/src/locales/uk.json b/src/locales/uk.json index 994c32ba3..314e3219d 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -5,6 +5,7 @@ "Base", "Bunyk", "DDPAT", + "Ice bulldog", "Lxlalexlxl", "Movses", "Olvin", diff --git a/src/locales/vec.json b/src/locales/vec.json index b925baf73..97892523e 100644 --- a/src/locales/vec.json +++ b/src/locales/vec.json @@ -27,7 +27,7 @@ "pad.colorpicker.cancel": "Descançełare", "pad.loading": "Drio cargar...", "pad.noCookie": "El cookie no el xé sta catà. Cosenti i cookie n'tel to navegadore web.", - "timeslider.month.january": "Xenaro", + "timeslider.month.january": "Zenaro", "timeslider.month.march": "Marso", "timeslider.month.april": "Apriłe", "timeslider.month.may": "Majo", diff --git a/src/locales/vi.json b/src/locales/vi.json index c5a420171..d618725e7 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -4,6 +4,7 @@ "Baonguyen21022003", "Max20091", "Minh Nguyen", + "NguoiDungKhongDinhDanh", "Tuankiet65" ] }, @@ -32,7 +33,7 @@ "pad.permissionDenied": "Bạn không có quyền truy cập pad này.", "pad.settings.padSettings": "Tùy chọn Pad", "pad.settings.myView": "Chỉ có tôi", - "pad.settings.stickychat": "Luân hiện cửa sổ trò chuyện trên màn hình", + "pad.settings.stickychat": "Luôn hiện cửa sổ trò chuyện trên màn hình", "pad.settings.colorcheck": "Màu chỉ tác giả", "pad.settings.linenocheck": "Số dòng", "pad.settings.rtlcheck": "Đọc nội dung từ phải sang trái?", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index e2dca09a8..69a908ee4 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -2,15 +2,18 @@ "@metadata": { "authors": [ "94rain", + "Cosing", "Dimension", "GuoPC", "Hydra", "Hzy980512", "JuneAugust", + "Lakejason0", "Liuxinyu970226", "Qiyue2001", "Shangkuanlc", "Shizhao", + "Stang", "VulpesVulpes825", "Yfdyh000", "乌拉跨氪", @@ -19,6 +22,39 @@ "燃玉" ] }, + "admin.page-title": "管理员面板 - Etherpad", + "admin_plugins": "插件管理器", + "admin_plugins.available": "可用插件", + "admin_plugins.available_not-found": "找不到插件。", + "admin_plugins.available_fetching": "获取中…", + "admin_plugins.available_install.value": "安装", + "admin_plugins.available_search.placeholder": "搜索插件以安装", + "admin_plugins.description": "描述", + "admin_plugins.installed": "已装插件", + "admin_plugins.installed_fetching": "正在获取已安装的插件…", + "admin_plugins.installed_nothing": "您尚未安装任何插件。", + "admin_plugins.installed_uninstall.value": "卸载", + "admin_plugins.last-update": "最后更新", + "admin_plugins.name": "名称", + "admin_plugins.page-title": "插件管理器 - Etherpad", + "admin_plugins.version": "版本", + "admin_plugins_info": "故障排除信息", + "admin_plugins_info.hooks": "已安装的钩子", + "admin_plugins_info.hooks_client": "客户端钩子", + "admin_plugins_info.hooks_server": "服务器端钩子", + "admin_plugins_info.parts": "已安装部分", + "admin_plugins_info.plugins": "已安装插件", + "admin_plugins_info.page-title": "插件信息 - Etherpad", + "admin_plugins_info.version": "Etherpad版本", + "admin_plugins_info.version_latest": "最新可用版本", + "admin_plugins_info.version_number": "版本号", + "admin_settings": "设置", + "admin_settings.current": "当前配置", + "admin_settings.current_example-devel": "开发设置模板示例", + "admin_settings.current_example-prod": "生产设置模板示例", + "admin_settings.current_restart.value": "重启Etherpad", + "admin_settings.current_save.value": "保存设置", + "admin_settings.page-title": "设置 - Etherpad", "index.newPad": "新记事本", "index.createOpenPad": "或者创建/打开带名字的记事本:", "index.openPad": "打开一个现有的记事本,名称为:", @@ -41,7 +77,7 @@ "pad.toolbar.showusers.title": "显示此记事本上的用户", "pad.colorpicker.save": "保存", "pad.colorpicker.cancel": "取消", - "pad.loading": "载入中……", + "pad.loading": "加载中...", "pad.noCookie": "无法找到Cookie。请在您的浏览器中允许Cookie!您的会话和设置不会在访问时保存。这可能是由于Etherpad包含在某些浏览器中的iFrame中。请确保Etherpad与父iFrame位于同一子域/域上", "pad.permissionDenied": "您没有访问这个记事本的权限", "pad.settings.padSettings": "记事本设置", @@ -92,6 +128,8 @@ "pad.modals.deleted.explanation": "此记事本已被移除。", "pad.modals.rateLimited": "速率限制", "pad.modals.rateLimited.explanation": "您向此记事本发送了太多消息,因此中断了与您的连接。", + "pad.modals.rejected.explanation": "服务器拒绝了您的浏览器发送的信息。", + "pad.modals.rejected.cause": "服务器可能在你查看页面时更新了,也可能是Etherpad出现了错误。请尝试重新加载页面。", "pad.modals.disconnected": "您已断开连接。", "pad.modals.disconnected.explanation": "到服务器的连接已丢失", "pad.modals.disconnected.cause": "服务器可能无法使用。若此情况持续发生,请通知服务器管理员。", @@ -131,7 +169,7 @@ "timeslider.month.december": "12月", "timeslider.unnamedauthors": "{{num}}个匿名作者", "pad.savedrevs.marked": "这一修订现在被标记为已保存的修订版本", - "pad.savedrevs.timeslider": "您可以使用时间滑块查阅已保存的版本", + "pad.savedrevs.timeslider": "您可以使用时间轴查阅已保存的版本", "pad.userlist.entername": "输入您的姓名", "pad.userlist.unnamed": "匿名", "pad.editbar.clearcolors": "清除整个文档的作者颜色吗?此操作无法撤消", diff --git a/src/node/db/API.js b/src/node/db/API.js index d48867558..9b2ecadc7 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -139,11 +139,11 @@ exports.getRevisionChangeset = async (padID, rev) => { } // get the changeset for this revision - return pad.getRevisionChangeset(rev); + return await pad.getRevisionChangeset(rev); } // the client wants the latest changeset, lets return it to him - return pad.getRevisionChangeset(head); + return await pad.getRevisionChangeset(head); }; /** @@ -172,7 +172,9 @@ exports.getText = async (padID, rev) => { } // get the text of this revision - const text = await pad.getInternalRevisionAText(rev); + // getInternalRevisionAText() returns an atext object but we only want the .text inside it. + // Details at https://github.com/ether/etherpad-lite/issues/5073 + const {text} = await pad.getInternalRevisionAText(rev); return {text}; } @@ -182,7 +184,7 @@ exports.getText = async (padID, rev) => { }; /** -setText(padID, text) sets the text of a pad +setText(padID, text, [authorId]) sets the text of a pad Example returns: @@ -190,7 +192,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.setText = async (padID, text) => { +exports.setText = async (padID, text, authorId = '') => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -199,14 +201,12 @@ exports.setText = async (padID, text) => { // get the pad const pad = await getPadSafe(padID, true); - await Promise.all([ - pad.setText(text), - padMessageHandler.updatePadClients(pad), - ]); + await pad.setText(text, authorId); + await padMessageHandler.updatePadClients(pad); }; /** -appendText(padID, text) appends text to a pad +appendText(padID, text, [authorId]) appends text to a pad Example returns: @@ -214,17 +214,15 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.appendText = async (padID, text) => { +exports.appendText = async (padID, text, authorId = '') => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); } const pad = await getPadSafe(padID, true); - await Promise.all([ - pad.appendText(text), - padMessageHandler.updatePadClients(pad), - ]); + await pad.appendText(text, authorId); + await padMessageHandler.updatePadClients(pad); }; /** @@ -260,14 +258,14 @@ exports.getHTML = async (padID, rev) => { }; /** -setHTML(padID, html) sets the text of a pad based on HTML +setHTML(padID, html, [authorId]) sets the text of a pad based on HTML Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setHTML = async (padID, html) => { +exports.setHTML = async (padID, html, authorId = '') => { // html string is required if (typeof html !== 'string') { throw new CustomError('html is not a string', 'apierror'); @@ -278,7 +276,7 @@ exports.setHTML = async (padID, html) => { // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html)); + await importHtml.setPadHTML(pad, cleanText(html), authorId); } catch (e) { throw new CustomError('HTML is malformed', 'apierror'); } @@ -461,14 +459,14 @@ exports.getLastEdited = async (padID) => { }; /** -createPad(padName [, text]) creates a new pad in this group +createPad(padName, [text], [authorId]) creates a new pad in this group Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} */ -exports.createPad = async (padID, text) => { +exports.createPad = async (padID, text, authorId = '') => { if (padID) { // ensure there is no $ in the padID if (padID.indexOf('$') !== -1) { @@ -482,7 +480,7 @@ exports.createPad = async (padID, text) => { } // create pad - await getPadSafe(padID, false, text); + await getPadSafe(padID, false, text, authorId); }; /** @@ -499,14 +497,14 @@ exports.deletePad = async (padID) => { }; /** - restoreRevision(padID, [rev]) Restores revision from past as new changeset + restoreRevision(padID, rev, [authorId]) Restores revision from past as new changeset Example returns: {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = async (padID, rev) => { +exports.restoreRevision = async (padID, rev, authorId = '') => { // check if rev is a number if (rev === undefined) { throw new CustomError('rev is not defined', 'apierror'); @@ -527,12 +525,10 @@ exports.restoreRevision = async (padID, rev) => { atext.text += '\n'; const eachAttribRun = (attribs, func) => { - const attribsIter = Changeset.opIterator(attribs); let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); + for (const op of Changeset.deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); @@ -559,10 +555,8 @@ exports.restoreRevision = async (padID, rev) => { const changeset = builder.toString(); - await Promise.all([ - pad.appendRevision(changeset), - padMessageHandler.updatePadClients(pad), - ]); + await pad.appendRevision(changeset, authorId); + await padMessageHandler.updatePadClients(pad); }; /** @@ -580,17 +574,17 @@ exports.copyPad = async (sourceID, destinationID, force) => { }; /** -copyPadWithoutHistory(sourceID, destinationID[, force=false]) copies a pad. If force is true, - the destination will be overwritten if it exists. +copyPadWithoutHistory(sourceID, destinationID[, force=false], [authorId]) copies a pad. If force is +true, the destination will be overwritten if it exists. Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPadWithoutHistory = async (sourceID, destinationID, force) => { +exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => { const pad = await getPadSafe(sourceID, true); - await pad.copyPadWithoutHistory(destinationID, force); + await pad.copyPadWithoutHistory(destinationID, force, authorId); }; /** @@ -832,7 +826,7 @@ exports.getStats = async () => { const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); // gets a pad safe -const getPadSafe = async (padID, shouldExist, text) => { +const getPadSafe = async (padID, shouldExist, text, authorId = '') => { // check if padID is a string if (typeof padID !== 'string') { throw new CustomError('padID is not a string', 'apierror'); @@ -857,7 +851,7 @@ const getPadSafe = async (padID, shouldExist, text) => { } // pad exists, let's get it - return padManager.getPad(padID, text); + return padManager.getPad(padID, text, authorId); }; // checks if a rev is a legal number diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 2a354d425..7049be5db 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -21,7 +21,8 @@ const db = require('./DB'); const CustomError = require('../utils/customError'); -const randomString = require('../../static/js/pad_utils').randomString; +const hooks = require('../../static/js/pluginfw/hooks.js'); +const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); exports.getColorPalette = () => [ '#ffc7c7', @@ -102,17 +103,32 @@ exports.doesAuthorExist = async (authorID) => { /* exported for backwards compatibility */ exports.doesAuthorExists = exports.doesAuthorExist; -/** - * Returns the AuthorID for a token. - * @param {String} token The token - */ -exports.getAuthor4Token = async (token) => { +const getAuthor4Token = async (token) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID return author ? author.authorID : author; }; +exports.getAuthorId = async (token, user) => { + const context = {dbKey: token, token, user}; + let [authorId] = await hooks.aCallFirst('getAuthorId', context); + if (!authorId) authorId = await getAuthor4Token(context.dbKey); + return authorId; +}; + +/** + * Returns the AuthorID for a token. + * + * @deprecated Use `getAuthorId` instead. + * @param {String} token The token + */ +exports.getAuthor4Token = async (token) => { + warnDeprecated( + 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); + return await getAuthor4Token(token); +}; + /** * Returns the AuthorID for a mapper. * @param {String} token The mapper diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 203e21a35..4302048c4 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -43,41 +43,29 @@ exports.deleteGroup = async (groupID) => { } // iterate through all pads of this group and delete them (in parallel) - await Promise.all(Object.keys(group.pads) - .map((padID) => padManager.getPad(padID) - .then((pad) => pad.remove()) - )); + await Promise.all(Object.keys(group.pads).map(async (padId) => { + const pad = await padManager.getPad(padId); + await pad.remove(); + })); - // iterate through group2sessions and delete all sessions - const group2sessions = await db.get(`group2sessions:${groupID}`); - const sessions = group2sessions ? group2sessions.sessionIDs : {}; + // Delete associated sessions in parallel. This should be done before deleting the group2sessions + // record because deleting a session updates the group2sessions record. + const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {}; + await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => { + await sessionManager.deleteSession(sessionId); + })); - // loop through all sessions and delete them (in parallel) - await Promise.all(Object.keys(sessions).map((session) => sessionManager.deleteSession(session))); + await Promise.all([ + db.remove(`group2sessions:${groupID}`), + // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and + // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify() + // ignores such properties). + db.setSub('groups', [groupID], undefined), + ...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)), + ]); - // remove group and group2sessions entry - await db.remove(`group2sessions:${groupID}`); + // Remove the group record after updating the `groups` record so that the state is consistent. await db.remove(`group:${groupID}`); - - // unlist the group - let groups = await exports.listAllGroups(); - groups = groups ? groups.groupIDs : []; - - const index = groups.indexOf(groupID); - - if (index === -1) { - // it's not listed - - return; - } - - // remove from the list - groups.splice(index, 1); - - // regenerate group list - const newGroups = {}; - groups.forEach((group) => newGroups[group] = 1); - await db.set('groups', newGroups); }; exports.doesGroupExist = async (groupID) => { @@ -88,51 +76,34 @@ exports.doesGroupExist = async (groupID) => { }; exports.createGroup = async () => { - // search for non existing groupID const groupID = `g.${randomString(16)}`; - - // create the group - await db.set(`group:${groupID}`, {pads: {}}); - - // list the group - let groups = await exports.listAllGroups(); - groups = groups ? groups.groupIDs : []; - groups.push(groupID); - - // regenerate group list - const newGroups = {}; - groups.forEach((group) => newGroups[group] = 1); - await db.set('groups', newGroups); - + await db.set(`group:${groupID}`, {pads: {}, mappings: {}}); + // Add the group to the `groups` record after the group's individual record is created so that + // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates + // the appropriate property, and writes the result. + await db.setSub('groups', [groupID], 1); return {groupID}; }; exports.createGroupIfNotExistsFor = async (groupMapper) => { - // ensure mapper is optional if (typeof groupMapper !== 'string') { throw new CustomError('groupMapper is not a string', 'apierror'); } - - // try to get a group for this mapper const groupID = await db.get(`mapper2group:${groupMapper}`); - - if (groupID) { - // there is a group for this mapper - const exists = await exports.doesGroupExist(groupID); - - if (exists) return {groupID}; - } - - // hah, the returned group doesn't exist, let's create one + if (groupID && await exports.doesGroupExist(groupID)) return {groupID}; const result = await exports.createGroup(); - - // create the mapper entry for this group - await db.set(`mapper2group:${groupMapper}`, result.groupID); - + await Promise.all([ + db.set(`mapper2group:${groupMapper}`, result.groupID), + // Remember the mapping in the group record so that it can be cleaned up when the group is + // deleted. Although the core Etherpad API does not support multiple mappings for the same + // group, the database record does support multiple mappings in case a plugin decides to extend + // the core Etherpad functionality. (It's also easy to implement it this way.) + db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1), + ]); return result; }; -exports.createGroupPad = async (groupID, padName, text) => { +exports.createGroupPad = async (groupID, padName, text, authorId = '') => { // create the padID const padID = `${groupID}$${padName}`; @@ -152,7 +123,7 @@ exports.createGroupPad = async (groupID, padName, text) => { } // create the pad - await padManager.getPad(padID, text); + await padManager.getPad(padID, text, authorId); // create an entry in the group for this pad await db.setSub(`group:${groupID}`, ['pads', padID], 1); diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 9e1ef0399..b692962f1 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -3,10 +3,11 @@ * The pad object, defined with joose */ - +const AttributeMap = require('../../static/js/AttributeMap'); const Changeset = require('../../static/js/Changeset'); const ChatMessage = require('../../static/js/ChatMessage'); const AttributePool = require('../../static/js/AttributePool'); +const Stream = require('../utils/Stream'); const assert = require('assert').strict; const db = require('./DB'); const settings = require('../utils/Settings'); @@ -18,12 +19,9 @@ const CustomError = require('../utils/customError'); const readOnlyManager = require('./ReadOnlyManager'); const randomString = require('../utils/randomstring'); const hooks = require('../../static/js/pluginfw/hooks'); +const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); const promises = require('../utils/promises'); -// serialization/deserialization attributes -const attributeBlackList = ['_db', 'id']; -const jsonableList = ['pool']; - /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix * line breaks and convert Tabs to spaces @@ -34,700 +32,695 @@ exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n') .replace(/\t/g, ' ') .replace(/\xa0/g, ' '); -/** - * @param [database] - Database object to access this pad's records (and only this pad's records -- - * the shared global Etherpad database object is still used for all other pad accesses, such as - * copying the pad). Defaults to the shared global Etherpad database object. This parameter can - * be used to shard pad storage across multiple database backends, to put each pad in its own - * database table, or to validate imported pad data before it is written to the database. - */ -const Pad = function (id, database = db) { - this._db = database; - this.atext = Changeset.makeAText('\n'); - this.pool = new AttributePool(); - this.head = -1; - this.chatHead = -1; - this.publicStatus = false; - this.id = id; - this.savedRevisions = []; -}; +class Pad { + /** + * @param [database] - Database object to access this pad's records (and only this pad's records; + * the shared global Etherpad database object is still used for all other pad accesses, such + * as copying the pad). Defaults to the shared global Etherpad database object. This parameter + * can be used to shard pad storage across multiple database backends, to put each pad in its + * own database table, or to validate imported pad data before it is written to the database. + */ + constructor(id, database = db) { + this.db = database; + this.atext = Changeset.makeAText('\n'); + this.pool = new AttributePool(); + this.head = -1; + this.chatHead = -1; + this.publicStatus = false; + this.id = id; + this.savedRevisions = []; + } + apool() { + return this.pool; + } + + getHeadRevisionNumber() { + return this.head; + } + + getSavedRevisionsNumber() { + return this.savedRevisions.length; + } + + getSavedRevisionsList() { + const savedRev = this.savedRevisions.map((rev) => rev.revNum); + savedRev.sort((a, b) => a - b); + return savedRev; + } + + getPublicStatus() { + return this.publicStatus; + } + + async appendRevision(aChangeset, authorId = '') { + const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); + if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs) { + return this.head; + } + Changeset.copyAText(newAText, this.atext); + + const newRev = ++this.head; + + // ex. getNumForAuthor + if (authorId !== '') this.pool.putAttrib(['author', authorId]); + + const hook = this.head === 0 ? 'padCreate' : 'padUpdate'; + await Promise.all([ + this.db.set(`pad:${this.id}:revs:${newRev}`, { + changeset: aChangeset, + meta: { + author: authorId, + timestamp: Date.now(), + ...newRev === this.getKeyRevisionNumber(newRev) ? { + pool: this.pool, + atext: this.atext, + } : {}, + }, + }), + this.saveToDatabase(), + authorId && authorManager.addPad(authorId, this.id), + hooks.aCallAll(hook, { + pad: this, + authorId, + get author() { + warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + return this.authorId; + }, + set author(authorId) { + warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + this.authorId = authorId; + }, + ...this.head === 0 ? {} : { + revs: newRev, + changeset: aChangeset, + }, + }), + ]); + return newRev; + } + + toJSON() { + const o = {...this, pool: this.pool.toJsonable()}; + delete o.db; + delete o.id; + return o; + } + + // save all attributes to the database + async saveToDatabase() { + await this.db.set(`pad:${this.id}`, this); + } + + // get time of last edit (changeset application) + async getLastEdit() { + const revNum = this.getHeadRevisionNumber(); + return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); + } + + async getRevisionChangeset(revNum) { + return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); + } + + async getRevisionAuthor(revNum) { + return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); + } + + async getRevisionDate(revNum) { + return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); + } + + /** + * @param {number} revNum - Must be a key revision number (see `getKeyRevisionNumber`). + * @returns The attribute text stored at `revNum`. + */ + async _getKeyRevisionAText(revNum) { + return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']); + } + + getAllAuthors() { + const authorIds = []; + + for (const key in this.pool.numToAttrib) { + if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') { + authorIds.push(this.pool.numToAttrib[key][1]); + } + } + + return authorIds; + } + + async getInternalRevisionAText(targetRev) { + const keyRev = this.getKeyRevisionNumber(targetRev); + const [keyAText, changesets] = await Promise.all([ + this._getKeyRevisionAText(keyRev), + Promise.all( + Stream.range(keyRev + 1, targetRev + 1).map(this.getRevisionChangeset.bind(this))), + ]); + const apool = this.apool(); + let atext = keyAText; + for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool); + return atext; + } + + async getRevision(revNum) { + return await this.db.get(`pad:${this.id}:revs:${revNum}`); + } + + async getAllAuthorColors() { + const authorIds = this.getAllAuthors(); + const returnTable = {}; + const colorPalette = authorManager.getColorPalette(); + + await Promise.all( + authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId) => { + // colorId might be a hex color or an number out of the palette + returnTable[authorId] = colorPalette[colorId] || colorId; + }))); + + return returnTable; + } + + getValidRevisionRange(startRev, endRev) { + startRev = parseInt(startRev, 10); + const head = this.getHeadRevisionNumber(); + endRev = endRev ? parseInt(endRev, 10) : head; + + if (isNaN(startRev) || startRev < 0 || startRev > head) { + startRev = null; + } + + if (isNaN(endRev) || endRev < startRev) { + endRev = null; + } else if (endRev > head) { + endRev = head; + } + + if (startRev != null && endRev != null) { + return {startRev, endRev}; + } + return null; + } + + getKeyRevisionNumber(revNum) { + return Math.floor(revNum / 100) * 100; + } + + /** + * @returns {string} The pad's text. + */ + text() { + return this.atext.text; + } + + /** + * Splices text into the pad. If the result of the splice does not end with a newline, one will be + * automatically appended. + * + * @param {number} start - Location in pad text to start removing and inserting characters. Must + * be a non-negative integer less than or equal to `this.text().length`. + * @param {number} ndel - Number of characters to remove starting at `start`. Must be a + * non-negative integer less than or equal to `this.text().length - start`. + * @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted). + * @param {string} [authorId] - Author ID of the user making the change (if applicable). + */ + async spliceText(start, ndel, ins, authorId = '') { + if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); + if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); + const orig = this.text(); + assert(orig.endsWith('\n')); + if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text'); + ins = exports.cleanText(ins); + const willEndWithNewline = + start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline). + ins.endsWith('\n') || + (!ins && start > 0 && orig[start - 1] === '\n'); + if (!willEndWithNewline) ins += '\n'; + if (ndel === 0 && ins.length === 0) return; + const changeset = Changeset.makeSplice(orig, start, ndel, ins); + await this.appendRevision(changeset, authorId); + } + + /** + * Replaces the pad's text with new text. + * + * @param {string} newText - The pad's new text. If this string does not end with a newline, one + * will be automatically appended. + * @param {string} [authorId] - The author ID of the user that initiated the change, if + * applicable. + */ + async setText(newText, authorId = '') { + await this.spliceText(0, this.text().length, newText, authorId); + } + + /** + * Appends text to the pad. + * + * @param {string} newText - Text to insert just BEFORE the pad's existing terminating newline. + * @param {string} [authorId] - The author ID of the user that initiated the change, if + * applicable. + */ + async appendText(newText, authorId = '') { + await this.spliceText(this.text().length - 1, 0, newText, authorId); + } + + /** + * Adds a chat message to the pad, including saving it to the database. + * + * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a + * string containing the raw text of the user's chat message (deprecated). + * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId` + * instead. + * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use + * `msgOrText.time` instead. + */ + async appendChatMessage(msgOrText, authorId = null, time = null) { + const msg = + msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); + this.chatHead++; + await Promise.all([ + // Don't save the display name in the database because the user can change it at any time. The + // `displayName` property will be populated with the current value when the message is read + // from the database. + this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), + this.saveToDatabase(), + ]); + } + + /** + * @param {number} entryNum - ID of the desired chat message. + * @returns {?ChatMessage} + */ + async getChatMessage(entryNum) { + const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); + if (entry == null) return null; + const message = ChatMessage.fromObject(entry); + message.displayName = await authorManager.getAuthorName(message.authorId); + return message; + } + + /** + * @param {number} start - ID of the first desired chat message. + * @param {number} end - ID of the last desired chat message. + * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end` + * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open + * interval as is typical in code. + */ + async getChatMessages(start, end) { + const entries = + await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this))); + + // sort out broken chat entries + // it looks like in happened in the past that the chat head was + // incremented, but the chat message wasn't added + return entries.filter((entry) => { + const pass = (entry != null); + if (!pass) { + console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); + } + return pass; + }); + } + + async init(text, authorId = '') { + // try to load the pad + const value = await this.db.get(`pad:${this.id}`); + + // if this pad exists, load it + if (value != null) { + Object.assign(this, value); + if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool); + } else { + if (text == null) { + const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText}; + await hooks.aCallAll('padDefaultContent', context); + if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); + text = exports.cleanText(context.content); + } + const firstChangeset = Changeset.makeSplice('\n', 0, 0, text); + await this.appendRevision(firstChangeset, authorId); + } + await hooks.aCallAll('padLoad', {pad: this}); + } + + async copy(destinationID, force) { + // Kick everyone from this pad. + // This was commented due to https://github.com/ether/etherpad-lite/issues/3183. + // Do we really need to kick everyone out? + // padMessageHandler.kickSessionsFromPad(sourceID); + + // flush the source pad: + await this.saveToDatabase(); + + // if it's a group pad, let's make sure the group exists. + const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); + + // if force is true and already exists a Pad with the same id, remove that Pad + await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); + + const copyRecord = async (keySuffix) => { + const val = await this.db.get(`pad:${this.id}${keySuffix}`); + await db.set(`pad:${destinationID}${keySuffix}`, val); + }; + + const promises = (function* () { + yield copyRecord(''); + yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`)); + yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`)); + yield this.copyAuthorInfoToDestinationPad(destinationID); + if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); + }).call(this); + for (const p of new Stream(promises).batch(100).buffer(99)) await p; + + // Initialize the new pad (will update the listAllPads cache) + const dstPad = await padManager.getPad(destinationID, null); + + // let the plugins know the pad was copied + await hooks.aCallAll('padCopy', { + get originalPad() { + warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + return this.srcPad; + }, + get destinationID() { + warnDeprecated( + 'padCopy destinationID context property is deprecated; use dstPad.id instead'); + return this.dstPad.id; + }, + srcPad: this, + dstPad, + }); + + return {padID: destinationID}; + } + + async checkIfGroupExistAndReturnIt(destinationID) { + let destGroupID = false; + + if (destinationID.indexOf('$') >= 0) { + destGroupID = destinationID.split('$')[0]; + const groupExists = await groupManager.doesGroupExist(destGroupID); + + // group does not exist + if (!groupExists) { + throw new CustomError('groupID does not exist for destinationID', 'apierror'); + } + } + return destGroupID; + } + + async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { + // if the pad exists, we should abort, unless forced. + const exists = await padManager.doesPadExist(destinationID); + + // allow force to be a string + if (typeof force === 'string') { + force = (force.toLowerCase() === 'true'); + } else { + force = !!force; + } + + if (exists) { + if (!force) { + console.error('erroring out without force'); + throw new CustomError('destinationID already exists', 'apierror'); + } + + // exists and forcing + const pad = await padManager.getPad(destinationID); + await pad.remove(); + } + } + + async copyAuthorInfoToDestinationPad(destinationID) { + // add the new sourcePad to all authors who contributed to the old one + await Promise.all(this.getAllAuthors().map( + (authorID) => authorManager.addPad(authorID, destinationID))); + } + + async copyPadWithoutHistory(destinationID, force, authorId = '') { + // flush the source pad + this.saveToDatabase(); + + // if it's a group pad, let's make sure the group exists. + const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); + + // if force is true and already exists a Pad with the same id, remove that Pad + await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); + + await this.copyAuthorInfoToDestinationPad(destinationID); + + // Group pad? Add it to the group's list + if (destGroupID) { + await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); + } + + // initialize the pad with a new line to avoid getting the defaultText + const dstPad = await padManager.getPad(destinationID, '\n', authorId); + dstPad.pool = this.pool.clone(); + + const oldAText = this.atext; + + // based on Changeset.makeSplice + const assem = Changeset.smartOpAssembler(); + for (const op of Changeset.opsFromAText(oldAText)) assem.append(op); + assem.endDocument(); + + // although we have instantiated the dstPad with '\n', an additional '\n' is + // added internally, so the pad text on the revision 0 is "\n\n" + const oldLength = 2; + + const newLength = assem.getLengthChange(); + const newText = oldAText.text; + + // create a changeset that removes the previous text and add the newText with + // all atributes present on the source pad + const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); + dstPad.appendRevision(changeset, authorId); + + await hooks.aCallAll('padCopy', { + get originalPad() { + warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + return this.srcPad; + }, + get destinationID() { + warnDeprecated( + 'padCopy destinationID context property is deprecated; use dstPad.id instead'); + return this.dstPad.id; + }, + srcPad: this, + dstPad, + }); + + return {padID: destinationID}; + } + + async remove() { + const padID = this.id; + const p = []; + + // kick everyone from this pad + padMessageHandler.kickSessionsFromPad(padID); + + // delete all relations - the original code used async.parallel but + // none of the operations except getting the group depended on callbacks + // so the database operations here are just started and then left to + // run to completion + + // is it a group pad? -> delete the entry of this pad in the group + if (padID.indexOf('$') >= 0) { + // it is a group pad + const groupID = padID.substring(0, padID.indexOf('$')); + const group = await db.get(`group:${groupID}`); + + // remove the pad entry + delete group.pads[padID]; + + // set the new value + p.push(db.set(`group:${groupID}`, group)); + } + + // remove the readonly entries + p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { + await db.remove(`readonly2pad:${readonlyID}`); + })); + p.push(db.remove(`pad2readonly:${padID}`)); + + // delete all chat messages + p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { + await this.db.remove(`pad:${this.id}:chat:${i}`, null); + })); + + // delete all revisions + p.push(promises.timesLimit(this.head + 1, 500, async (i) => { + await this.db.remove(`pad:${this.id}:revs:${i}`, null); + })); + + // remove pad from all authors who contributed + this.getAllAuthors().forEach((authorId) => { + p.push(authorManager.removePad(authorId, padID)); + }); + + // delete the pad entry and delete pad from padManager + p.push(padManager.removePad(padID)); + p.push(hooks.aCallAll('padRemove', { + get padID() { + warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); + return this.pad.id; + }, + pad: this, + })); + await Promise.all(p); + } + + // set in db + async setPublicStatus(publicStatus) { + this.publicStatus = publicStatus; + await this.saveToDatabase(); + } + + async addSavedRevision(revNum, savedById, label) { + // if this revision is already saved, return silently + for (const i in this.savedRevisions) { + if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { + return; + } + } + + // build the saved revision object + const savedRevision = {}; + savedRevision.revNum = revNum; + savedRevision.savedById = savedById; + savedRevision.label = label || `Revision ${revNum}`; + savedRevision.timestamp = Date.now(); + savedRevision.id = randomString(10); + + // save this new saved revision + this.savedRevisions.push(savedRevision); + await this.saveToDatabase(); + } + + getSavedRevisions() { + return this.savedRevisions; + } + + /** + * Asserts that all pad data is consistent. Throws if inconsistent. + */ + async check() { + assert(this.id != null); + assert.equal(typeof this.id, 'string'); + + const head = this.getHeadRevisionNumber(); + assert(head != null); + assert(Number.isInteger(head)); + assert(head >= -1); + + const savedRevisionsList = this.getSavedRevisionsList(); + assert(Array.isArray(savedRevisionsList)); + assert.equal(this.getSavedRevisionsNumber(), savedRevisionsList.length); + let prevSavedRev = null; + for (const rev of savedRevisionsList) { + assert(rev != null); + assert(Number.isInteger(rev)); + assert(rev >= 0); + assert(rev <= head); + assert(prevSavedRev == null || rev > prevSavedRev); + prevSavedRev = rev; + } + const savedRevisions = this.getSavedRevisions(); + assert(Array.isArray(savedRevisions)); + assert.equal(savedRevisions.length, savedRevisionsList.length); + const savedRevisionsIds = new Set(); + for (const savedRev of savedRevisions) { + assert(savedRev != null); + assert.equal(typeof savedRev, 'object'); + assert(savedRevisionsList.includes(savedRev.revNum)); + assert(savedRev.id != null); + assert.equal(typeof savedRev.id, 'string'); + assert(!savedRevisionsIds.has(savedRev.id)); + savedRevisionsIds.add(savedRev.id); + } + + const pool = this.apool(); + assert(pool instanceof AttributePool); + await pool.check(); + + const authorIds = new Set(); + pool.eachAttrib((k, v) => { + if (k === 'author' && v) authorIds.add(v); + }); + const revs = Stream.range(0, head + 1) + .map(async (r) => { + const isKeyRev = r === this.getKeyRevisionNumber(r); + try { + return await Promise.all([ + r, + this.getRevisionChangeset(r), + this.getRevisionAuthor(r), + this.getRevisionDate(r), + isKeyRev, + isKeyRev ? this._getKeyRevisionAText(r) : null, + ]); + } catch (err) { + err.message = `(pad ${this.id} revision ${r}) ${err.message}`; + throw err; + } + }) + .batch(100).buffer(99); + let atext = Changeset.makeAText('\n'); + for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) { + try { + assert(authorId != null); + assert.equal(typeof authorId, 'string'); + if (authorId) authorIds.add(authorId); + assert(timestamp != null); + assert.equal(typeof timestamp, 'number'); + assert(timestamp > 0); + assert(changeset != null); + assert.equal(typeof changeset, 'string'); + Changeset.checkRep(changeset); + const unpacked = Changeset.unpack(changeset); + let text = atext.text; + for (const op of Changeset.deserializeOps(unpacked.ops)) { + if (['=', '-'].includes(op.opcode)) { + assert(text.length >= op.chars); + const consumed = text.slice(0, op.chars); + const nlines = (consumed.match(/\n/g) || []).length; + assert.equal(op.lines, nlines); + if (op.lines > 0) assert(consumed.endsWith('\n')); + text = text.slice(op.chars); + } + assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString()); + } + atext = Changeset.applyToAText(changeset, atext, pool); + if (isKeyRev) assert.deepEqual(keyAText, atext); + } catch (err) { + err.message = `(pad ${this.id} revision ${r}) ${err.message}`; + throw err; + } + } + assert.equal(this.text(), atext.text); + assert.deepEqual(this.atext, atext); + assert.deepEqual(this.getAllAuthors().sort(), [...authorIds].sort()); + + assert(this.chatHead != null); + assert(Number.isInteger(this.chatHead)); + assert(this.chatHead >= -1); + const chats = Stream.range(0, this.chatHead + 1) + .map(async (c) => { + try { + const msg = await this.getChatMessage(c); + assert(msg != null); + assert(msg instanceof ChatMessage); + } catch (err) { + err.message = `(pad ${this.id} chat message ${c}) ${err.message}`; + throw err; + } + }) + .batch(100).buffer(99); + for (const p of chats) await p; + + await hooks.aCallAll('padCheck', {pad: this}); + } +} exports.Pad = Pad; - -Pad.prototype.apool = function () { - return this.pool; -}; - -Pad.prototype.getHeadRevisionNumber = function () { - return this.head; -}; - -Pad.prototype.getSavedRevisionsNumber = function () { - return this.savedRevisions.length; -}; - -Pad.prototype.getSavedRevisionsList = function () { - const savedRev = this.savedRevisions.map((rev) => rev.revNum); - savedRev.sort((a, b) => a - b); - return savedRev; -}; - -Pad.prototype.getPublicStatus = function () { - return this.publicStatus; -}; - -Pad.prototype.appendRevision = async function (aChangeset, author) { - if (!author) { - author = ''; - } - - const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); - Changeset.copyAText(newAText, this.atext); - - const newRev = ++this.head; - - const newRevData = {}; - newRevData.changeset = aChangeset; - newRevData.meta = {}; - newRevData.meta.author = author; - newRevData.meta.timestamp = Date.now(); - - // ex. getNumForAuthor - if (author !== '') { - this.pool.putAttrib(['author', author]); - } - - if (newRev % 100 === 0) { - newRevData.meta.pool = this.pool; - newRevData.meta.atext = this.atext; - } - - const p = [ - this._db.set(`pad:${this.id}:revs:${newRev}`, newRevData), - this.saveToDatabase(), - ]; - - // set the author to pad - if (author) { - p.push(authorManager.addPad(author, this.id)); - } - - if (this.head === 0) { - hooks.callAll('padCreate', {pad: this, author}); - } else { - hooks.callAll('padUpdate', {pad: this, author, revs: newRev, changeset: aChangeset}); - } - - await Promise.all(p); -}; - -// save all attributes to the database -Pad.prototype.saveToDatabase = async function () { - const dbObject = {}; - - for (const attr in this) { - if (typeof this[attr] === 'function') continue; - if (attributeBlackList.indexOf(attr) !== -1) continue; - - dbObject[attr] = this[attr]; - - if (jsonableList.indexOf(attr) !== -1) { - dbObject[attr] = dbObject[attr].toJsonable(); - } - } - - await this._db.set(`pad:${this.id}`, dbObject); -}; - -// get time of last edit (changeset application) -Pad.prototype.getLastEdit = async function () { - const revNum = this.getHeadRevisionNumber(); - return await this._db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); -}; - -Pad.prototype.getRevisionChangeset = async function (revNum) { - return await this._db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); -}; - -Pad.prototype.getRevisionAuthor = async function (revNum) { - return await this._db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); -}; - -Pad.prototype.getRevisionDate = async function (revNum) { - return await this._db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); -}; - -Pad.prototype.getAllAuthors = function () { - const authors = []; - - for (const key in this.pool.numToAttrib) { - if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') { - authors.push(this.pool.numToAttrib[key][1]); - } - } - - return authors; -}; - -Pad.prototype.getInternalRevisionAText = async function (targetRev) { - const keyRev = this.getKeyRevisionNumber(targetRev); - - // find out which changesets are needed - const neededChangesets = []; - for (let curRev = keyRev; curRev < targetRev;) { - neededChangesets.push(++curRev); - } - - // get all needed data out of the database - - // start to get the atext of the key revision - const p_atext = this._db.getSub(`pad:${this.id}:revs:${keyRev}`, ['meta', 'atext']); - - // get all needed changesets - const changesets = []; - await Promise.all( - neededChangesets.map((item) => this.getRevisionChangeset(item).then((changeset) => { - changesets[item] = changeset; - }))); - - // we should have the atext by now - let atext = await p_atext; - atext = Changeset.cloneAText(atext); - - // apply all changesets to the key changeset - const apool = this.apool(); - for (let curRev = keyRev; curRev < targetRev;) { - const cs = changesets[++curRev]; - atext = Changeset.applyToAText(cs, atext, apool); - } - - return atext; -}; - -Pad.prototype.getRevision = async function (revNum) { - return await this._db.get(`pad:${this.id}:revs:${revNum}`); -}; - -Pad.prototype.getAllAuthorColors = async function () { - const authors = this.getAllAuthors(); - const returnTable = {}; - const colorPalette = authorManager.getColorPalette(); - - await Promise.all( - authors.map((author) => authorManager.getAuthorColorId(author).then((colorId) => { - // colorId might be a hex color or an number out of the palette - returnTable[author] = colorPalette[colorId] || colorId; - }))); - - return returnTable; -}; - -Pad.prototype.getValidRevisionRange = function (startRev, endRev) { - startRev = parseInt(startRev, 10); - const head = this.getHeadRevisionNumber(); - endRev = endRev ? parseInt(endRev, 10) : head; - - if (isNaN(startRev) || startRev < 0 || startRev > head) { - startRev = null; - } - - if (isNaN(endRev) || endRev < startRev) { - endRev = null; - } else if (endRev > head) { - endRev = head; - } - - if (startRev != null && endRev != null) { - return {startRev, endRev}; - } - return null; -}; - -Pad.prototype.getKeyRevisionNumber = function (revNum) { - return Math.floor(revNum / 100) * 100; -}; - -Pad.prototype.text = function () { - return this.atext.text; -}; - -Pad.prototype.setText = async function (newText) { - // clean the new text - newText = exports.cleanText(newText); - - const oldText = this.text(); - - // create the changeset - // We want to ensure the pad still ends with a \n, but otherwise keep - // getText() and setText() consistent. - let changeset; - if (newText[newText.length - 1] === '\n') { - changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText); - } else { - changeset = Changeset.makeSplice(oldText, 0, oldText.length - 1, newText); - } - - // append the changeset - if (newText !== oldText) await this.appendRevision(changeset); -}; - -Pad.prototype.appendText = async function (newText) { - // clean the new text - newText = exports.cleanText(newText); - - const oldText = this.text(); - - // create the changeset - const changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); - - // append the changeset - await this.appendRevision(changeset); -}; - -/** - * Adds a chat message to the pad, including saving it to the database. - * - * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a string - * containing the raw text of the user's chat message (deprecated). - * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId` instead. - * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use - * `msgOrText.time` instead. - */ -Pad.prototype.appendChatMessage = async function (msgOrText, authorId = null, time = null) { - const msg = - msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); - this.chatHead++; - await Promise.all([ - // Don't save the display name in the database because the user can change it at any time. The - // `displayName` property will be populated with the current value when the message is read from - // the database. - this._db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), - this.saveToDatabase(), - ]); -}; - -/** - * @param {number} entryNum - ID of the desired chat message. - * @returns {?ChatMessage} - */ -Pad.prototype.getChatMessage = async function (entryNum) { - const entry = await this._db.get(`pad:${this.id}:chat:${entryNum}`); - if (entry == null) return null; - const message = ChatMessage.fromObject(entry); - message.displayName = await authorManager.getAuthorName(message.authorId); - return message; -}; - -/** - * @param {number} start - ID of the first desired chat message. - * @param {number} end - ID of the last desired chat message. - * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end` - * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open - * interval as is typical in code. - */ -Pad.prototype.getChatMessages = async function (start, end) { - const entries = await Promise.all( - [...Array(end + 1 - start).keys()].map((i) => this.getChatMessage(start + i))); - - // sort out broken chat entries - // it looks like in happened in the past that the chat head was - // incremented, but the chat message wasn't added - return entries.filter((entry) => { - const pass = (entry != null); - if (!pass) { - console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); - } - return pass; - }); -}; - -Pad.prototype.init = async function (text) { - // replace text with default text if text isn't set - if (text == null) { - text = settings.defaultPadText; - } - - // try to load the pad - const value = await this._db.get(`pad:${this.id}`); - - // if this pad exists, load it - if (value != null) { - // copy all attr. To a transfrom via fromJsonable if necassary - for (const attr in value) { - if (jsonableList.indexOf(attr) !== -1) { - this[attr] = this[attr].fromJsonable(value[attr]); - } else { - this[attr] = value[attr]; - } - } - } else { - // this pad doesn't exist, so create it - const firstChangeset = Changeset.makeSplice('\n', 0, 0, exports.cleanText(text)); - - await this.appendRevision(firstChangeset, ''); - } -}; - -Pad.prototype.copy = async function (destinationID, force) { - // Kick everyone from this pad. - // This was commented due to https://github.com/ether/etherpad-lite/issues/3183. - // Do we really need to kick everyone out? - // padMessageHandler.kickSessionsFromPad(sourceID); - - // flush the source pad: - await this.saveToDatabase(); - - // if it's a group pad, let's make sure the group exists. - const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); - - // if force is true and already exists a Pad with the same id, remove that Pad - await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - - // copy the 'pad' entry - const pad = await this._db.get(`pad:${this.id}`); - db.set(`pad:${destinationID}`, pad); - - // copy all relations in parallel - const promises = []; - - // copy all chat messages - const chatHead = this.chatHead; - for (let i = 0; i <= chatHead; ++i) { - const p = this._db.get(`pad:${this.id}:chat:${i}`) - .then((chat) => db.set(`pad:${destinationID}:chat:${i}`, chat)); - promises.push(p); - } - - // copy all revisions - const revHead = this.head; - for (let i = 0; i <= revHead; ++i) { - const p = this._db.get(`pad:${this.id}:revs:${i}`) - .then((rev) => db.set(`pad:${destinationID}:revs:${i}`, rev)); - promises.push(p); - } - - this.copyAuthorInfoToDestinationPad(destinationID); - - // wait for the above to complete - await Promise.all(promises); - - // Group pad? Add it to the group's list - if (destGroupID) { - await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); - } - - // delay still necessary? - await new Promise((resolve) => setTimeout(resolve, 10)); - - // Initialize the new pad (will update the listAllPads cache) - await padManager.getPad(destinationID, null); // this runs too early. - - // let the plugins know the pad was copied - await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); - - return {padID: destinationID}; -}; - -Pad.prototype.checkIfGroupExistAndReturnIt = async function (destinationID) { - let destGroupID = false; - - if (destinationID.indexOf('$') >= 0) { - destGroupID = destinationID.split('$')[0]; - const groupExists = await groupManager.doesGroupExist(destGroupID); - - // group does not exist - if (!groupExists) { - throw new CustomError('groupID does not exist for destinationID', 'apierror'); - } - } - return destGroupID; -}; - -Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function (destinationID, force) { - // if the pad exists, we should abort, unless forced. - const exists = await padManager.doesPadExist(destinationID); - - // allow force to be a string - if (typeof force === 'string') { - force = (force.toLowerCase() === 'true'); - } else { - force = !!force; - } - - if (exists) { - if (!force) { - console.error('erroring out without force'); - throw new CustomError('destinationID already exists', 'apierror'); - } - - // exists and forcing - const pad = await padManager.getPad(destinationID); - await pad.remove(); - } -}; - -Pad.prototype.copyAuthorInfoToDestinationPad = function (destinationID) { - // add the new sourcePad to all authors who contributed to the old one - this.getAllAuthors().forEach((authorID) => { - authorManager.addPad(authorID, destinationID); - }); -}; - -Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) { - const sourceID = this.id; - - // flush the source pad - this.saveToDatabase(); - - // if it's a group pad, let's make sure the group exists. - const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID); - - // if force is true and already exists a Pad with the same id, remove that Pad - await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - - const sourcePad = await padManager.getPad(sourceID); - - // add the new sourcePad to all authors who contributed to the old one - this.copyAuthorInfoToDestinationPad(destinationID); - - // Group pad? Add it to the group's list - if (destGroupID) { - await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); - } - - // initialize the pad with a new line to avoid getting the defaultText - const newPad = await padManager.getPad(destinationID, '\n'); - - const oldAText = this.atext; - const newPool = newPad.pool; - newPool.fromJsonable(sourcePad.pool.toJsonable()); // copy that sourceId pool to the new pad - - // based on Changeset.makeSplice - const assem = Changeset.smartOpAssembler(); - Changeset.appendATextToAssembler(oldAText, assem); - assem.endDocument(); - - // although we have instantiated the newPad with '\n', an additional '\n' is - // added internally, so the pad text on the revision 0 is "\n\n" - const oldLength = 2; - - const newLength = assem.getLengthChange(); - const newText = oldAText.text; - - // create a changeset that removes the previous text and add the newText with - // all atributes present on the source pad - const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); - newPad.appendRevision(changeset); - - await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); - - return {padID: destinationID}; -}; - - -Pad.prototype.remove = async function () { - const padID = this.id; - const p = []; - - // kick everyone from this pad - padMessageHandler.kickSessionsFromPad(padID); - - // delete all relations - the original code used async.parallel but - // none of the operations except getting the group depended on callbacks - // so the database operations here are just started and then left to - // run to completion - - // is it a group pad? -> delete the entry of this pad in the group - if (padID.indexOf('$') >= 0) { - // it is a group pad - const groupID = padID.substring(0, padID.indexOf('$')); - const group = await db.get(`group:${groupID}`); - - // remove the pad entry - delete group.pads[padID]; - - // set the new value - p.push(db.set(`group:${groupID}`, group)); - } - - // remove the readonly entries - p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { - await db.remove(`readonly2pad:${readonlyID}`); - })); - p.push(db.remove(`pad2readonly:${padID}`)); - - // delete all chat messages - p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { - await this._db.remove(`pad:${this.id}:chat:${i}`, null); - })); - - // delete all revisions - p.push(promises.timesLimit(this.head + 1, 500, async (i) => { - await this._db.remove(`pad:${this.id}:revs:${i}`, null); - })); - - // remove pad from all authors who contributed - this.getAllAuthors().forEach((authorID) => { - p.push(authorManager.removePad(authorID, padID)); - }); - - // delete the pad entry and delete pad from padManager - p.push(padManager.removePad(padID)); - p.push(hooks.aCallAll('padRemove', {padID})); - await Promise.all(p); -}; - -// set in db -Pad.prototype.setPublicStatus = async function (publicStatus) { - this.publicStatus = publicStatus; - await this.saveToDatabase(); -}; - -Pad.prototype.addSavedRevision = async function (revNum, savedById, label) { - // if this revision is already saved, return silently - for (const i in this.savedRevisions) { - if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { - return; - } - } - - // build the saved revision object - const savedRevision = {}; - savedRevision.revNum = revNum; - savedRevision.savedById = savedById; - savedRevision.label = label || `Revision ${revNum}`; - savedRevision.timestamp = Date.now(); - savedRevision.id = randomString(10); - - // save this new saved revision - this.savedRevisions.push(savedRevision); - await this.saveToDatabase(); -}; - -Pad.prototype.getSavedRevisions = function () { - return this.savedRevisions; -}; - -/** - * Asserts that all pad data is consistent. Throws if inconsistent. - */ -Pad.prototype.check = async function () { - assert(this.id != null); - assert.equal(typeof this.id, 'string'); - - const head = this.getHeadRevisionNumber(); - assert(Number.isInteger(head)); - assert(head >= -1); - - const savedRevisionsList = this.getSavedRevisionsList(); - assert(Array.isArray(savedRevisionsList)); - assert.equal(this.getSavedRevisionsNumber(), savedRevisionsList.length); - let prevSavedRev = null; - for (const rev of savedRevisionsList) { - assert(Number.isInteger(rev)); - assert(rev >= 0); - assert(rev <= head); - assert(prevSavedRev == null || rev > prevSavedRev); - prevSavedRev = rev; - } - const savedRevisions = this.getSavedRevisions(); - assert(Array.isArray(savedRevisions)); - assert.equal(savedRevisions.length, savedRevisionsList.length); - const savedRevisionsIds = new Set(); - for (const savedRev of savedRevisions) { - assert(savedRev != null); - assert.equal(typeof savedRev, 'object'); - assert(savedRevisionsList.includes(savedRev.revNum)); - assert(savedRev.id != null); - assert.equal(typeof savedRev.id, 'string'); - assert(!savedRevisionsIds.has(savedRev.id)); - savedRevisionsIds.add(savedRev.id); - } - - const pool = this.apool(); - assert(pool instanceof AttributePool); - await pool.check(); - - const decodeAttribString = function* (str) { - const re = /\*([0-9a-z]+)|./gy; - let match; - while ((match = re.exec(str)) != null) { - const [m, n] = match; - if (n == null) throw new Error(`invalid character in attribute string: ${m}`); - yield Number.parseInt(n, 36); - } - }; - - const authors = new Set(); - pool.eachAttrib((k, v) => { - if (k === 'author' && v) authors.add(v); - }); - let atext = Changeset.makeAText('\n'); - let r; - try { - for (r = 0; r <= head; ++r) { - const [changeset, author, timestamp] = await Promise.all([ - this.getRevisionChangeset(r), - this.getRevisionAuthor(r), - this.getRevisionDate(r), - ]); - assert(author != null); - assert.equal(typeof author, 'string'); - if (author) authors.add(author); - assert(timestamp != null); - assert.equal(typeof timestamp, 'number'); - assert(timestamp > 0); - assert(changeset != null); - assert.equal(typeof changeset, 'string'); - Changeset.checkRep(changeset); - const unpacked = Changeset.unpack(changeset); - let text = atext.text; - const iter = Changeset.opIterator(unpacked.ops); - while (iter.hasNext()) { - const op = iter.next(); - if (['=', '-'].includes(op.opcode)) { - assert(text.length >= op.chars); - const consumed = text.slice(0, op.chars); - const nlines = (consumed.match(/\n/g) || []).length; - assert.equal(op.lines, nlines); - if (op.lines > 0) assert(consumed.endsWith('\n')); - text = text.slice(op.chars); - } - let prevK = null; - for (const n of decodeAttribString(op.attribs)) { - const attrib = pool.getAttrib(n); - assert(attrib != null); - const [k] = attrib; - assert(prevK == null || prevK < k); - prevK = k; - } - } - atext = Changeset.applyToAText(changeset, atext, pool); - assert.deepEqual(await this.getInternalRevisionAText(r), atext); - } - } catch (err) { - const pfx = `(pad ${this.id} revision ${r}) `; - if (err.stack) err.stack = pfx + err.stack; - err.message = pfx + err.message; - throw err; - } - assert.equal(this.text(), atext.text); - assert.deepEqual(this.atext, atext); - assert.deepEqual(this.getAllAuthors().sort(), [...authors].sort()); - - assert(Number.isInteger(this.chatHead)); - assert(this.chatHead >= -1); - let c; - try { - for (c = 0; c <= this.chatHead; ++c) { - const msg = await this.getChatMessage(c); - assert(msg != null); - assert(msg instanceof ChatMessage); - } - } catch (err) { - const pfx = `(pad ${this.id} chat message ${c}) `; - if (err.stack) err.stack = pfx + err.stack; - err.message = pfx + err.message; - throw err; - } -}; diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 4aebc1a86..e419e8392 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -22,7 +22,6 @@ const CustomError = require('../utils/customError'); const Pad = require('../db/Pad'); const db = require('./DB'); -const hooks = require('../../static/js/pluginfw/hooks'); /** * A cache of all loaded Pads. @@ -50,68 +49,52 @@ const globalPads = { * * Updated without db access as new pads are created/old ones removed. */ -const padList = { - list: new Set(), - cachedList: undefined, - initiated: false, - async init() { - const dbData = await db.findKeys('pad:*', '*:*:*'); +const padList = new class { + constructor() { + this._cachedList = null; + this._list = new Set(); + this._loaded = null; + } - if (dbData != null) { - this.initiated = true; - - for (const val of dbData) { - this.addPad(val.replace(/^pad:/, ''), false); - } - } - - return this; - }, - async load() { - if (!this.initiated) { - return this.init(); - } - - return this; - }, /** * Returns all pads in alphabetical order as array. */ async getPads() { - await this.load(); - - if (!this.cachedList) { - this.cachedList = Array.from(this.list).sort(); + if (!this._loaded) { + this._loaded = (async () => { + const dbData = await db.findKeys('pad:*', '*:*:*'); + if (dbData == null) return; + for (const val of dbData) this.addPad(val.replace(/^pad:/, '')); + })(); } + await this._loaded; + if (!this._cachedList) this._cachedList = [...this._list].sort(); + return this._cachedList; + } - return this.cachedList; - }, addPad(name) { - if (!this.initiated) return; + if (this._list.has(name)) return; + this._list.add(name); + this._cachedList = null; + } - if (!this.list.has(name)) { - this.list.add(name); - this.cachedList = undefined; - } - }, removePad(name) { - if (!this.initiated) return; - - if (this.list.has(name)) { - this.list.delete(name); - this.cachedList = undefined; - } - }, -}; + if (!this._list.has(name)) return; + this._list.delete(name); + this._cachedList = null; + } +}(); // initialises the all-knowing data structure /** * Returns a Pad Object with the callback * @param id A String with the id of the pad - * @param {Function} callback + * @param {string} [text] - Optional initial pad text if creating a new pad. + * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if + * applicable). */ -exports.getPad = async (id, text) => { +exports.getPad = async (id, text, authorId = '') => { // check if this is a valid padId if (!exports.isValidPadId(id)) { throw new CustomError(`${id} is not a valid padId`, 'apierror'); @@ -141,8 +124,7 @@ exports.getPad = async (id, text) => { pad = new Pad.Pad(id); // initialize the pad - await pad.init(text); - hooks.callAll('padLoad', {pad}); + await pad.init(text, authorId); globalPads.set(id, pad); padList.addPad(id); diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 0b8d29171..33ce2930a 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -41,8 +41,10 @@ exports.getReadOnlyId = async (padId) => { // there is no readOnly Entry in the database, let's create one if (readOnlyId == null) { readOnlyId = `r.${randomString(16)}`; - db.set(`pad2readonly:${padId}`, readOnlyId); - db.set(`readonly2pad:${readOnlyId}`, padId); + await Promise.all([ + db.set(`pad2readonly:${padId}`, readOnlyId), + db.set(`readonly2pad:${readOnlyId}`, padId), + ]); } return readOnlyId; @@ -52,7 +54,7 @@ exports.getReadOnlyId = async (padId) => { * returns the padId for a read only id * @param {String} readOnlyId read only id */ -exports.getPadId = (readOnlyId) => db.get(`readonly2pad:${readOnlyId}`); +exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`); /** * returns the padId and readonlyPadId in an object for any id diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 4851866d5..280c753bb 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -28,6 +28,7 @@ const settings = require('../utils/Settings'); const webaccess = require('../hooks/express/webaccess'); const log4js = require('log4js'); const authLogger = log4js.getLogger('auth'); +const {padutils} = require('../../static/js/pad_utils'); const DENY = Object.freeze({accessStatus: 'deny'}); @@ -95,26 +96,26 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { return DENY; } - // start fetching the info we may need - const p_sessionAuthorID = sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); - const p_tokenAuthorID = authorManager.getAuthor4Token(token); - const p_padExists = padManager.doesPadExist(padID); - - const padExists = await p_padExists; + const padExists = await padManager.doesPadExist(padID); if (!padExists && !canCreate) { authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); return DENY; } - const sessionAuthorID = await p_sessionAuthorID; + const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); if (settings.requireSession && !sessionAuthorID) { authLogger.debug('access denied: HTTP API session is required'); return DENY; } + if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) { + // The author token should be kept secret, so do not log it. + authLogger.debug('access denied: invalid author token'); + return DENY; + } const grant = { accessStatus: 'grant', - authorID: (sessionAuthorID != null) ? sessionAuthorID : await p_tokenAuthorID, + authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), }; if (!padID.includes('$')) { diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index b5f93094d..5ef75acfa 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -134,41 +134,14 @@ exports.createSession = async (groupID, authorID, validUntil) => { // set the session into the database await db.set(`session:${sessionID}`, {groupID, authorID, validUntil}); - // get the entry - let group2sessions = await db.get(`group2sessions:${groupID}`); - - /* - * In some cases, the db layer could return "undefined" as well as "null". - * Thus, it is not possible to perform strict null checks on group2sessions. - * In a previous version of this code, a strict check broke session - * management. - * - * See: https://github.com/ether/etherpad-lite/issues/3567#issuecomment-468613960 - */ - if (!group2sessions || !group2sessions.sessionIDs) { - // the entry doesn't exist so far, let's create it - group2sessions = {sessionIDs: {}}; - } - - // add the entry for this session - group2sessions.sessionIDs[sessionID] = 1; - - // save the new element back - await db.set(`group2sessions:${groupID}`, group2sessions); - - // get the author2sessions entry - let author2sessions = await db.get(`author2sessions:${authorID}`); - - if (author2sessions == null || author2sessions.sessionIDs == null) { - // the entry doesn't exist so far, let's create it - author2sessions = {sessionIDs: {}}; - } - - // add the entry for this session - author2sessions.sessionIDs[sessionID] = 1; - - // save the new element back - await db.set(`author2sessions:${authorID}`, author2sessions); + // Add the session ID to the group2sessions and author2sessions records after creating the session + // so that the state is consistent. + await Promise.all([ + // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object + // property, and writes the result. + db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1), + db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1), + ]); return {sessionID}; }; @@ -200,24 +173,17 @@ exports.deleteSession = async (sessionID) => { const groupID = session.groupID; const authorID = session.authorID; - // get the group2sessions and author2sessions entries - const group2sessions = await db.get(`group2sessions:${groupID}`); - const author2sessions = await db.get(`author2sessions:${authorID}`); + await Promise.all([ + // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object + // property, and writes the result. Setting a property to `undefined` deletes that property + // (JSON.stringify() ignores such properties). + db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined), + db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined), + ]); - // remove the session + // Delete the session record after updating group2sessions and author2sessions so that the state + // is consistent. await db.remove(`session:${sessionID}`); - - // remove session from group2sessions - if (group2sessions != null) { // Maybe the group was already deleted - delete group2sessions.sessionIDs[sessionID]; - await db.set(`group2sessions:${groupID}`, group2sessions); - } - - // remove session from author2sessions - if (author2sessions != null) { // Maybe the author was already deleted - delete author2sessions.sessionIDs[sessionID]; - await db.set(`author2sessions:${authorID}`, author2sessions); - } }; exports.listSessionsOfGroup = async (groupID) => { diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 2c5d1ca25..40e5e90d0 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -1,44 +1,113 @@ 'use strict'; -/* - * Stores session data in the database - * Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js - * This is not used for authors that are created via the API at current - * - * RPB: this module was not migrated to Promises, because it is only used via - * express-session, which can't actually use promises anyway. - */ const DB = require('./DB'); const Store = require('express-session').Store; const log4js = require('log4js'); +const util = require('util'); const logger = log4js.getLogger('SessionStore'); -module.exports = class SessionStore extends Store { - get(sid, fn) { +class SessionStore extends Store { + /** + * @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's + * database record with the cookie's latest expiration time. If the difference between the + * value saved in the database and the actual value is greater than this amount, the database + * record will be updated to reflect the actual value. Use this to avoid continual database + * writes caused by express-session's rolling=true feature (see + * https://github.com/expressjs/session#rolling). A good value is high enough to keep query + * rate low but low enough to avoid annoying premature logouts (session invalidation) if + * Etherpad is restarted. Use `null` to prevent `touch()` from ever updating the record. + * Ignored if the cookie does not expire. + */ + constructor(refresh = null) { + super(); + this._refresh = refresh; + // Maps session ID to an object with the following properties: + // - `db`: Session expiration as recorded in the database (ms since epoch, not a Date). + // - `real`: Actual session expiration (ms since epoch, not a Date). Always greater than or + // equal to `db`. + // - `timeout`: Timeout ID for a timeout that will clean up the database record. + this._expirations = new Map(); + } + + shutdown() { + for (const {timeout} of this._expirations.values()) clearTimeout(timeout); + } + + async _updateExpirations(sid, sess, updateDbExp = true) { + const exp = this._expirations.get(sid) || {}; + clearTimeout(exp.timeout); + const {cookie: {expires} = {}} = sess || {}; + if (expires) { + const sessExp = new Date(expires).getTime(); + if (updateDbExp) exp.db = sessExp; + exp.real = Math.max(exp.real || 0, exp.db || 0, sessExp); + const now = Date.now(); + if (exp.real <= now) return await this._destroy(sid); + // If reading from the database, update the expiration with the latest value from touch() so + // that touch() appears to write to the database every time even though it doesn't. + if (typeof expires === 'string') sess.cookie.expires = new Date(exp.real).toJSON(); + // Use this._get(), not this._destroy(), to destroy the DB record for the expired session. + // This is done in case multiple Etherpad instances are sharing the same database and users + // are bouncing between the instances. By using this._get(), this instance will query the DB + // for the latest expiration time written by any of the instances, ensuring that the record + // isn't prematurely deleted if the expiration time was updated by a different Etherpad + // instance. (Important caveat: Client-side database caching, which ueberdb does by default, + // could still cause the record to be prematurely deleted because this instance might get a + // stale expiration time from cache.) + exp.timeout = setTimeout(() => this._get(sid), exp.real - now); + this._expirations.set(sid, exp); + } else { + this._expirations.delete(sid); + } + return sess; + } + + async _write(sid, sess) { + await DB.set(`sessionstorage:${sid}`, sess); + } + + async _get(sid) { logger.debug(`GET ${sid}`); - DB.db.get(`sessionstorage:${sid}`, (err, sess) => { - if (sess) { - sess.cookie.expires = ('string' === typeof sess.cookie.expires - ? new Date(sess.cookie.expires) : sess.cookie.expires); - if (!sess.cookie.expires || new Date() < sess.cookie.expires) { - fn(null, sess); - } else { - this.destroy(sid, fn); - } - } else { - fn(); - } - }); + const s = await DB.get(`sessionstorage:${sid}`); + return await this._updateExpirations(sid, s); } - set(sid, sess, fn) { + async _set(sid, sess) { logger.debug(`SET ${sid}`); - DB.db.set(`sessionstorage:${sid}`, sess, fn); + sess = await this._updateExpirations(sid, sess); + if (sess != null) await this._write(sid, sess); } - destroy(sid, fn) { + async _destroy(sid) { logger.debug(`DESTROY ${sid}`); - DB.db.remove(`sessionstorage:${sid}`, fn); + clearTimeout((this._expirations.get(sid) || {}).timeout); + this._expirations.delete(sid); + await DB.remove(`sessionstorage:${sid}`); } -}; + + // Note: express-session might call touch() before it calls set() for the first time. Ideally this + // would behave like set() in that case but it's OK if it doesn't -- express-session will call + // set() soon enough. + async _touch(sid, sess) { + logger.debug(`TOUCH ${sid}`); + sess = await this._updateExpirations(sid, sess, false); + if (sess == null) return; // Already expired. + const exp = this._expirations.get(sid); + // If the session doesn't expire, don't do anything. Ideally we would write the session to the + // database if it didn't already exist, but we have no way of knowing that without querying the + // database. The query overhead is not worth it because set() should be called soon anyway. + if (exp == null) return; + if (exp.db != null && (this._refresh == null || exp.real < exp.db + this._refresh)) return; + await this._write(sid, sess); + exp.db = new Date(sess.cookie.expires).getTime(); + } +} + +// express-session doesn't support Promise-based methods. This is where the callbackified versions +// used by express-session are defined. +for (const m of ['get', 'set', 'destroy', 'touch']) { + SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); +} + +module.exports = SessionStore; diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 6bc6e5378..2c060c239 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -47,95 +47,118 @@ try { // a list of all functions const version = {}; -version['1'] = Object.assign({}, - {createGroup: [], - createGroupIfNotExistsFor: ['groupMapper'], - deleteGroup: ['groupID'], - listPads: ['groupID'], - createPad: ['padID', 'text'], - createGroupPad: ['groupID', 'padName', 'text'], - createAuthor: ['name'], - createAuthorIfNotExistsFor: ['authorMapper', 'name'], - listPadsOfAuthor: ['authorID'], - createSession: ['groupID', 'authorID', 'validUntil'], - deleteSession: ['sessionID'], - getSessionInfo: ['sessionID'], - listSessionsOfGroup: ['groupID'], - listSessionsOfAuthor: ['authorID'], - getText: ['padID', 'rev'], - setText: ['padID', 'text'], - getHTML: ['padID', 'rev'], - setHTML: ['padID', 'html'], - getRevisionsCount: ['padID'], - getLastEdited: ['padID'], - deletePad: ['padID'], - getReadOnlyID: ['padID'], - setPublicStatus: ['padID', 'publicStatus'], - getPublicStatus: ['padID'], - listAuthorsOfPad: ['padID'], - padUsersCount: ['padID']} -); +version['1'] = { + createGroup: [], + createGroupIfNotExistsFor: ['groupMapper'], + deleteGroup: ['groupID'], + listPads: ['groupID'], + createPad: ['padID', 'text'], + createGroupPad: ['groupID', 'padName', 'text'], + createAuthor: ['name'], + createAuthorIfNotExistsFor: ['authorMapper', 'name'], + listPadsOfAuthor: ['authorID'], + createSession: ['groupID', 'authorID', 'validUntil'], + deleteSession: ['sessionID'], + getSessionInfo: ['sessionID'], + listSessionsOfGroup: ['groupID'], + listSessionsOfAuthor: ['authorID'], + getText: ['padID', 'rev'], + setText: ['padID', 'text'], + getHTML: ['padID', 'rev'], + setHTML: ['padID', 'html'], + getRevisionsCount: ['padID'], + getLastEdited: ['padID'], + deletePad: ['padID'], + getReadOnlyID: ['padID'], + setPublicStatus: ['padID', 'publicStatus'], + getPublicStatus: ['padID'], + listAuthorsOfPad: ['padID'], + padUsersCount: ['padID'], +}; -version['1.1'] = Object.assign({}, version['1'], - {getAuthorName: ['authorID'], - padUsers: ['padID'], - sendClientsMessage: ['padID', 'msg'], - listAllGroups: []} -); +version['1.1'] = { + ...version['1'], + getAuthorName: ['authorID'], + padUsers: ['padID'], + sendClientsMessage: ['padID', 'msg'], + listAllGroups: [], +}; -version['1.2'] = Object.assign({}, version['1.1'], - {checkToken: []} -); +version['1.2'] = { + ...version['1.1'], + checkToken: [], +}; -version['1.2.1'] = Object.assign({}, version['1.2'], - {listAllPads: []} -); +version['1.2.1'] = { + ...version['1.2'], + listAllPads: [], +}; -version['1.2.7'] = Object.assign({}, version['1.2.1'], - {createDiffHTML: ['padID', 'startRev', 'endRev'], - getChatHistory: ['padID', 'start', 'end'], - getChatHead: ['padID']} -); +version['1.2.7'] = { + ...version['1.2.1'], + createDiffHTML: ['padID', 'startRev', 'endRev'], + getChatHistory: ['padID', 'start', 'end'], + getChatHead: ['padID'], +}; -version['1.2.8'] = Object.assign({}, version['1.2.7'], - {getAttributePool: ['padID'], - getRevisionChangeset: ['padID', 'rev']} -); +version['1.2.8'] = { + ...version['1.2.7'], + getAttributePool: ['padID'], + getRevisionChangeset: ['padID', 'rev'], +}; -version['1.2.9'] = Object.assign({}, version['1.2.8'], - {copyPad: ['sourceID', 'destinationID', 'force'], - movePad: ['sourceID', 'destinationID', 'force']} -); +version['1.2.9'] = { + ...version['1.2.8'], + copyPad: ['sourceID', 'destinationID', 'force'], + movePad: ['sourceID', 'destinationID', 'force'], +}; -version['1.2.10'] = Object.assign({}, version['1.2.9'], - {getPadID: ['roID']} -); +version['1.2.10'] = { + ...version['1.2.9'], + getPadID: ['roID'], +}; -version['1.2.11'] = Object.assign({}, version['1.2.10'], - {getSavedRevisionsCount: ['padID'], - listSavedRevisions: ['padID'], - saveRevision: ['padID', 'rev'], - restoreRevision: ['padID', 'rev']} -); +version['1.2.11'] = { + ...version['1.2.10'], + getSavedRevisionsCount: ['padID'], + listSavedRevisions: ['padID'], + saveRevision: ['padID', 'rev'], + restoreRevision: ['padID', 'rev'], +}; -version['1.2.12'] = Object.assign({}, version['1.2.11'], - {appendChatMessage: ['padID', 'text', 'authorID', 'time']} -); +version['1.2.12'] = { + ...version['1.2.11'], + appendChatMessage: ['padID', 'text', 'authorID', 'time'], +}; -version['1.2.13'] = Object.assign({}, version['1.2.12'], - {appendText: ['padID', 'text']} -); +version['1.2.13'] = { + ...version['1.2.12'], + appendText: ['padID', 'text'], +}; -version['1.2.14'] = Object.assign({}, version['1.2.13'], - {getStats: []} -); +version['1.2.14'] = { + ...version['1.2.13'], + getStats: [], +}; -version['1.2.15'] = Object.assign({}, version['1.2.14'], - {copyPadWithoutHistory: ['sourceID', 'destinationID', 'force']} -); +version['1.2.15'] = { + ...version['1.2.14'], + copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'], +}; + +version['1.3.0'] = { + ...version['1.2.15'], + appendText: ['padID', 'text', 'authorId'], + copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'], + createGroupPad: ['groupID', 'padName', 'text', 'authorId'], + createPad: ['padID', 'text', 'authorId'], + restoreRevision: ['padID', 'rev', 'authorId'], + setHTML: ['padID', 'html', 'authorId'], + setText: ['padID', 'text', 'authorId'], +}; // set the latest available API version here -exports.latestApiVersion = '1.2.15'; +exports.latestApiVersion = '1.3.0'; // exports the versions so it can be used by the new Swagger endpoint exports.version = version; diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index c865dcf98..c1fbc94d0 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -26,7 +26,7 @@ const padMessageHandler = require('./PadMessageHandler'); const fs = require('fs').promises; const path = require('path'); const settings = require('../utils/Settings'); -const formidable = require('formidable'); +const {Formidable} = require('formidable'); const os = require('os'); const importHtml = require('../utils/ImportHtml'); const importEtherpad = require('../utils/ImportEtherpad'); @@ -74,7 +74,7 @@ const tmpDirectory = os.tmpdir(); /** * do a requested import */ -const doImport = async (req, res, padId) => { +const doImport = async (req, res, padId, authorId) => { // pipe to a file // convert file to html via abiword or soffice // set html in the pad @@ -83,22 +83,11 @@ const doImport = async (req, res, padId) => { // setting flag for whether to use converter or not let useConverter = (converter != null); - const form = new formidable.IncomingForm(); - form.keepExtensions = true; - form.uploadDir = tmpDirectory; - form.maxFileSize = settings.importMaxFileSize; - - // Ref: https://github.com/node-formidable/formidable/issues/469 - // Crash in Etherpad was Uploading Error: Error: Request aborted - // [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed - form.onPart = (part) => { - form.handlePart(part); - if (part.filename !== undefined) { - form.openedFiles[form.openedFiles.length - 1]._writeStream.on('error', (err) => { - form.emit('error', err); - }); - } - }; + const form = new Formidable({ + keepExtensions: true, + uploadDir: tmpDirectory, + maxFileSize: settings.importMaxFileSize, + }); // locally wrapped Promise, since form.parse requires a callback let srcFile = await new Promise((resolve, reject) => { @@ -115,7 +104,7 @@ const doImport = async (req, res, padId) => { logger.warn('Import failed because form had no file'); return reject(new ImportError('uploadFailed')); } - resolve(files.file.path); + resolve(files.file.filepath); }); }); @@ -142,26 +131,24 @@ const doImport = async (req, res, padId) => { } const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); - const importHandledByPlugin = - (await hooks.aCallAll('import', {srcFile, destFile, fileEnding, padId})).some((x) => x); + const context = {srcFile, destFile, fileEnding, padId, ImportError}; + const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); let directDatabaseAccess = false; if (fileIsEtherpad) { - // we do this here so we can see if the pad has quite a few edits - const _pad = await padManager.getPad(padId); - const headCount = _pad.head; - + // Use '\n' to avoid the default pad text if the pad doesn't yet exist. + const pad = await padManager.getPad(padId, '\n', authorId); + const headCount = pad.head; if (headCount >= 10) { logger.warn('Aborting direct database import attempt of a pad that already has content'); throw new ImportError('padHasData'); } - - const _text = await fs.readFile(srcFile, 'utf8'); + const text = await fs.readFile(srcFile, 'utf8'); directDatabaseAccess = true; - await importEtherpad.setPadRaw(padId, _text); + await importEtherpad.setPadRaw(padId, text, authorId); } // convert file to html if necessary @@ -198,8 +185,8 @@ const doImport = async (req, res, padId) => { } } - // get the pad object - let pad = await padManager.getPad(padId); + // Use '\n' to avoid the default pad text if the pad doesn't yet exist. + let pad = await padManager.getPad(padId, '\n', authorId); // read the text let text; @@ -218,18 +205,18 @@ const doImport = async (req, res, padId) => { if (!directDatabaseAccess) { if (importHandledByPlugin || useConverter || fileIsHTML) { try { - await importHtml.setPadHTML(pad, text); + await importHtml.setPadHTML(pad, text, authorId); } catch (err) { logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); } } else { - await pad.setText(text); + await pad.setText(text, authorId); } } // Load the Pad into memory then broadcast updates to all clients padManager.unloadPad(padId); - pad = await padManager.getPad(padId); + pad = await padManager.getPad(padId, '\n', authorId); padManager.unloadPad(padId); // Direct database access means a pad user should reload the pad and not attempt to receive @@ -246,13 +233,13 @@ const doImport = async (req, res, padId) => { return false; }; -exports.doImport = async (req, res, padId) => { +exports.doImport = async (req, res, padId, authorId = '') => { let httpStatus = 200; let code = 0; let message = 'ok'; let directDatabaseAccess; try { - directDatabaseAccess = await doImport(req, res, padId); + directDatabaseAccess = await doImport(req, res, padId, authorId); } catch (err) { const known = err instanceof ImportError && err.status; if (!known) logger.error(`Internal error during import: ${err.stack || err}`); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c048c2fd2..9a1885b73 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -19,12 +19,14 @@ * limitations under the License. */ +const AttributeMap = require('../../static/js/AttributeMap'); const padManager = require('../db/PadManager'); const Changeset = require('../../static/js/Changeset'); const ChatMessage = require('../../static/js/ChatMessage'); const AttributePool = require('../../static/js/AttributePool'); const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); +const {padutils} = require('../../static/js/pad_utils'); const readOnlyManager = require('../db/ReadOnlyManager'); const settings = require('../utils/Settings'); const securityManager = require('../db/SecurityManager'); @@ -32,7 +34,6 @@ const plugins = require('../../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('../../static/js/pluginfw/hooks.js'); const stats = require('../stats'); const assert = require('assert').strict; @@ -44,6 +45,15 @@ let socketio = null; hooks.deprecationNotices.clientReady = 'use the userJoin hook instead'; +const addContextToError = (err, pfx) => { + const newErr = new Error(`${pfx}${err.message}`, {cause: err}); + if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError); + // Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10. + if (newErr.cause === err) return newErr; + err.message = `${pfx}${err.message}`; + return err; +}; + exports.socketio = () => { // The rate limiter is created in this hook so that restarting the server resets the limiter. The // settings.commitRateLimiting object is passed directly to the rate limiter so that the limits @@ -201,29 +211,20 @@ exports.handleMessage = async (socket, message) => { if (env === 'production') { try { await rateLimiter.consume(socket.request.ip); // consume 1 point per event from IP - } catch (e) { + } catch (err) { messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` + 'limiting that happens edit the rateLimit values in settings.json'); stats.meter('rateLimited').mark(); socket.json.send({disconnect: 'rateLimited'}); - return; + throw err; } } - if (message == null) { - return; - } - - if (!message.type) { - return; - } + if (message == null) throw new Error('message is null'); + if (!message.type) throw new Error('message type missing'); const thisSession = sessioninfos[socket.id]; - - if (!thisSession) { - messageLogger.warn('Dropped message from an unknown connection.'); - return; - } + if (!thisSession) throw new Error('message from an unknown connection'); if (message.type === 'CLIENT_READY') { // Remember this information since we won't have the cookie in further socket.io messages. This @@ -234,6 +235,11 @@ exports.handleMessage = async (socket, message) => { padID: message.padId, token: message.token, }; + const padIds = await readOnlyManager.getIds(thisSession.auth.padID); + thisSession.padId = padIds.padId; + thisSession.readOnlyPadId = padIds.readOnlyPadId; + thisSession.readonly = + padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request); } // Outside of the checks done by this function, message.padId must not be accessed because it is // too easy to introduce a security vulnerability that allows malicious users to read or modify @@ -248,39 +254,62 @@ exports.handleMessage = async (socket, message) => { if (!auth) { const ip = settings.disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || ''); const msg = JSON.stringify(message, null, 2); - messageLogger.error(`Dropping pre-CLIENT_READY message from IP ${ip}: ${msg}`); - messageLogger.debug( - 'If you are using the stress-test tool then restart Etherpad and the Stress test tool.'); - return; + throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`); } const {session: {user} = {}} = socket.client.request; const {accessStatus, authorID} = await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { - // Access denied. Send the reason to the user. socket.json.send({accessStatus}); - return; + throw new Error('access denied'); } 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; + throw new Error([ + '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}`, + ].join(' ')); } thisSession.author = authorID; // 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; + let readOnly = thisSession.readonly; + const context = { + message, + sessionInfo: { + authorId: thisSession.author, + padId: thisSession.padId, + readOnly: thisSession.readonly, + }, + socket, + get client() { + padutils.warnDeprecated( + 'the `client` context property for the handleMessageSecurity and handleMessage hooks ' + + 'is deprecated; use the `socket` property instead'); + return this.socket; + }, + }; + for (const res of await hooks.aCallAll('handleMessageSecurity', context)) { + switch (res) { + case true: + padutils.warnDeprecated( + 'returning `true` from a `handleMessageSecurity` hook function is deprecated; ' + + 'return "permitOnce" instead'); + thisSession.readonly = false; + // Fall through: + case 'permitOnce': + readOnly = false; + break; + default: + messageLogger.warn( + 'Ignoring unsupported return value from handleMessageSecurity hook function:', res); + } } // Call handleMessage hook. If a plugin returns null, the message will be dropped. @@ -289,39 +318,49 @@ exports.handleMessage = async (socket, message) => { } // 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; - } + if (sessioninfos[socket.id] !== thisSession) throw new Error('client disconnected'); - // Check what type of message we get and delegate to the other methods - if (message.type === 'CLIENT_READY') { - await handleClientReady(socket, message); - } 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(); - await padChannels.enqueue(thisSession.padId, {socket, message}); - } 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(socket, message); - } else { - messageLogger.warn(`Dropped message, unknown COLLABROOM Data Type ${message.data.type}`); + const {type} = message; + try { + switch (type) { + case 'CLIENT_READY': await handleClientReady(socket, message); break; + case 'CHANGESET_REQ': await handleChangesetRequest(socket, message); break; + case 'COLLABROOM': { + if (readOnly) throw new Error('write attempt on read-only pad'); + const {type} = message.data; + try { + switch (type) { + case 'USER_CHANGES': + stats.counter('pendingEdits').inc(); + await padChannels.enqueue(thisSession.padId, {socket, message}); + break; + case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break; + case 'CHAT_MESSAGE': await handleChatMessage(socket, message); break; + case 'GET_CHAT_MESSAGES': await handleGetChatMessages(socket, message); break; + case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message); break; + case 'CLIENT_MESSAGE': { + const {type} = message.data.payload; + try { + switch (type) { + case 'suggestUserName': handleSuggestUserName(socket, message); break; + default: throw new Error('unknown message type'); + } + } catch (err) { + throw addContextToError(err, `${type}: `); + } + break; + } + default: throw new Error('unknown message type'); + } + } catch (err) { + throw addContextToError(err, `${type}: `); + } + break; + } + default: throw new Error('unknown message type'); } - } else { - messageLogger.warn(`Dropped message, unknown Message Type ${message.type}`); + } catch (err) { + throw addContextToError(err, `${type}: `); } }; @@ -333,7 +372,7 @@ exports.handleMessage = async (socket, message) => { */ const handleSaveRevisionMessage = async (socket, message) => { const {padId, author: authorId} = sessioninfos[socket.id]; - const pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId, null, authorId); await pad.addSavedRevision(pad.head, authorId); }; @@ -402,12 +441,12 @@ const handleChatMessage = async (socket, message) => { exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => { const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); padId = mt instanceof ChatMessage ? puId : padId; - const pad = await padManager.getPad(padId); + const pad = await padManager.getPad(padId, null, message.authorId); await hooks.aCallAll('chatNewMessage', {message, pad, padId}); // pad.appendChatMessage() ignores the displayName property so we don't need to wait for // authorManager.getAuthorName() to resolve before saving the message to the database. const promise = pad.appendChatMessage(message); - message.displayName = await authorManager.getAuthorName(message.userId); + message.displayName = await authorManager.getAuthorName(message.authorId); socketio.sockets.in(padId).json.send({ type: 'COLLABROOM', data: {type: 'CHAT_MESSAGE', message}, @@ -420,29 +459,13 @@ exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleGetChatMessages = async (socket, message) => { - if (message.data.start == null) { - messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); - return; - } - - if (message.data.end == null) { - messageLogger.warn('Dropped message, GetChatMessages Message has no start!'); - return; - } - - const start = message.data.start; - const end = message.data.end; +const handleGetChatMessages = async (socket, {data: {start, end}}) => { + if (!Number.isInteger(start)) throw new Error(`missing or invalid start: ${start}`); + if (!Number.isInteger(end)) throw new Error(`missing or invalid end: ${end}`); const count = end - start; - - if (count < 0 || count > 100) { - messageLogger.warn( - 'Dropped message, GetChatMessages Message, client requested invalid amount of messages!'); - return; - } - - const padId = sessioninfos[socket.id].padId; - const pad = await padManager.getPad(padId); + if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`); + const {padId, author: authorId} = sessioninfos[socket.id]; + const pad = await padManager.getPad(padId, null, authorId); const chatMessages = await pad.getChatMessages(start, end); const infoMsg = { @@ -463,23 +486,14 @@ const handleGetChatMessages = async (socket, message) => { * @param message the message from the client */ const handleSuggestUserName = (socket, message) => { - // check if all ok - if (message.data.payload.newName == null) { - 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!'); - return; - } - + const {newName, unnamedId} = message.data.payload; + if (newName == null) throw new Error('missing newName'); + if (unnamedId == null) throw new Error('missing unnamedId'); const padId = sessioninfos[socket.id].padId; - // search the author and send him this message _getRoomSockets(padId).forEach((socket) => { const session = sessioninfos[socket.id]; - if (session && session.author === message.data.payload.unnamedId) { + if (session && session.author === unnamedId) { socket.json.send(message); } }); @@ -491,40 +505,20 @@ const handleSuggestUserName = (socket, message) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleUserInfoUpdate = async (socket, message) => { - // check if all ok - if (message.data.userInfo == null) { - 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!'); - return; - } - - // Check that we have a valid session and author to update. +const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}}) => { + if (colorId == null) throw new Error('missing colorId'); + if (!name) name = null; const session = sessioninfos[socket.id]; - if (!session || !session.author || !session.padId) { - messageLogger.warn(`Dropped message, USERINFO_UPDATE Session not ready.${message.data}`); - return; - } - - // Find out the author name of this session + if (!session || !session.author || !session.padId) throw new Error('session not ready'); const author = session.author; - - // Check colorId is a Hex color - // for #f00 (Thanks Smamatti) - const isColor = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(message.data.userInfo.colorId); - if (!isColor) { - messageLogger.warn(`Dropped message, USERINFO_UPDATE Color is malformed.${message.data}`); - return; + if (!/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(colorId)) { + throw new Error(`malformed color: ${colorId}`); } // Tell the authorManager about the new attributes const p = Promise.all([ - authorManager.setAuthorColorId(author, message.data.userInfo.colorId), - authorManager.setAuthorName(author, message.data.userInfo.name), + authorManager.setAuthorColorId(author, colorId), + authorManager.setAuthorName(author, name), ]); const padId = session.padId; @@ -534,12 +528,7 @@ const handleUserInfoUpdate = async (socket, message) => { data: { // The Client doesn't know about USERINFO_UPDATE, use 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, - }, + userInfo: {userId: author, name, colorId}, }, }; @@ -575,10 +564,7 @@ const handleUserChanges = async (socket, message) => { // 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 (!thisSession) { - messageLogger.warn('Ignoring USER_CHANGES from disconnected user'); - return; - } + if (!thisSession) throw new Error('client disconnected'); // Measure time to process edit const stopWatch = stats.timer('edits').start(); @@ -588,44 +574,26 @@ const handleUserChanges = async (socket, message) => { if (apool == null) throw new Error('missing apool'); if (changeset == null) throw new Error('missing changeset'); const wireApool = (new AttributePool()).fromJsonable(apool); - const pad = await padManager.getPad(thisSession.padId); + const pad = await padManager.getPad(thisSession.padId, null, thisSession.author); // Verify that the changeset has valid syntax and is in canonical form Changeset.checkRep(changeset); - // Verify that the attribute indexes used in the changeset are all - // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, (n) => { - if (!wireApool.getAttrib(n)) { - throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); - } - }); - // Validate all added 'author' attribs to be the same value as the current user - const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); - let op; - - while (iterator.hasNext()) { - op = iterator.next(); - + for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) { // + can add text with attribs // = can change or add attribs // - can have attribs, but they are discarded and don't show up in the attribs - // but do show up in the pool - op.attribs.split('*').forEach((attr) => { - if (!attr) return; - - attr = wireApool.getAttrib(Changeset.parseNum(attr)); - if (!attr) return; - - // the empty author is used in the clearAuthorship functionality so this - // should be the only exception - if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { - throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + - `${attr[1]} in changeset ${changeset}`); - } - }); + // Besides verifying the author attribute, this serves a second purpose: + // AttributeMap.fromString() ensures that all attribute numbers are valid (it will throw if + // an attribute number isn't in the pool). + const opAuthorId = AttributeMap.fromString(op.attribs, wireApool).get('author'); + if (opAuthorId && opAuthorId !== thisSession.author) { + throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + + `${opAuthorId} in changeset ${changeset}`); + } } // ex. adoptChangesetAttribs @@ -641,19 +609,15 @@ const handleUserChanges = async (socket, message) => { // Update the changeset so that it can be applied to the latest revision. while (r < pad.getHeadRevisionNumber()) { r++; - - const c = await pad.getRevisionChangeset(r); - + const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r); + if (changeset === c && thisSession.author === authorId) { + // Assume this is a retransmission of an already applied changeset. + rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen); + } // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - - // a changeset can be based on an old revision with the same changes in it - // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES - // of that revision - if (baseRev + 1 === r && c === changeset) throw new Error('Changeset already accepted'); - rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool); } @@ -665,19 +629,28 @@ const handleUserChanges = async (socket, message) => { `${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`); } - await pad.appendRevision(rebasedChangeset, thisSession.author); + const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author); + // The head revision will either stay the same or increase by 1 depending on whether the + // changeset has a net effect. + assert([r, r + 1].includes(newRev)); const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { - await pad.appendRevision(correctionChangeset); + await pad.appendRevision(correctionChangeset, thisSession.author); } // Make sure the pad always ends with an empty line. if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); - await pad.appendRevision(nlChangeset); + await pad.appendRevision(nlChangeset, thisSession.author); } + // The client assumes that ACCEPT_COMMIT and NEW_CHANGES messages arrive in order. Make sure we + // have already sent any previous ACCEPT_COMMIT and NEW_CHANGES messages. + assert.equal(thisSession.rev, r); + socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}}); + thisSession.rev = newRev; + if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); await exports.updatePadClients(pad); } catch (err) { socket.json.send({disconnect: 'badChangeset'}); @@ -723,24 +696,19 @@ exports.updatePadClients = async (pad) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - let msg; - if (author === sessioninfo.author) { - msg = {type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev: r}}; - } else { - const forWire = Changeset.prepareForWire(revChangeset, pad.pool); - msg = { - type: 'COLLABROOM', - data: { - type: 'NEW_CHANGES', - newRev: r, - changeset: forWire.translated, - apool: forWire.pool, - author, - currentTime, - timeDelta: currentTime - sessioninfo.time, - }, - }; - } + const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const msg = { + type: 'COLLABROOM', + data: { + type: 'NEW_CHANGES', + newRev: r, + changeset: forWire.translated, + apool: forWire.pool, + author, + currentTime, + timeDelta: currentTime - sessioninfo.time, + }, + }; try { socket.json.send(msg); } catch (err) { @@ -762,15 +730,10 @@ const _correctMarkersInPad = (atext, apool) => { // collect char positions of line markers (e.g. bullets) in new atext // that aren't at the start of a line const badMarkers = []; - const iter = Changeset.opIterator(atext.attribs); let offset = 0; - while (iter.hasNext()) { - const op = iter.next(); - - const hasMarker = _.find( - AttributeManager.lineAttributes, - (attribute) => Changeset.opAttributeValue(op, attribute, apool)) !== undefined; - + for (const op of Changeset.deserializeOps(atext.attribs)) { + const attribs = AttributeMap.fromString(op.attribs, apool); + const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); if (hasMarker) { for (let i = 0; i < op.chars; i++) { if (offset > 0 && text.charAt(offset - 1) !== '\n') { @@ -810,16 +773,9 @@ const _correctMarkersInPad = (atext, apool) => { */ const handleClientReady = async (socket, message) => { const sessionInfo = sessioninfos[socket.id]; - // Check if the user has already disconnected. - if (sessionInfo == null) return; + if (sessionInfo == null) throw new Error('client disconnected'); assert(sessionInfo.author); - const padIds = await readOnlyManager.getIds(sessionInfo.auth.padID); - sessionInfo.padId = padIds.padId; - sessionInfo.readOnlyPadId = padIds.readOnlyPadId; - sessionInfo.readonly = - padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); - await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context. let {colorId: authorColorId, name: authorName} = message.userInfo || {}; @@ -834,7 +790,7 @@ const handleClientReady = async (socket, message) => { ({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author)); // load the pad-object from the database - const pad = await padManager.getPad(sessionInfo.padId); + const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author); // these db requests all need the pad object (timestamp of latest revision, author data) const authors = pad.getAllAuthors(); @@ -858,7 +814,7 @@ const handleClientReady = async (socket, message) => { // glue the clientVars together, send them and tell the other clients that a new one is there // Check if the user has disconnected during any of the above awaits. - if (sessionInfo !== sessioninfos[socket.id]) return; + if (sessionInfo !== sessioninfos[socket.id]) throw new Error('client disconnected'); // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); @@ -960,8 +916,7 @@ const handleClientReady = async (socket, message) => { } catch (e) { messageLogger.error(e.stack || e); socket.json.send({disconnect: 'corruptPad'}); // pull the brakes - - return; + throw new Error('corrupt pad'); } // Warning: never ever send sessionInfo.padId to the client. If the client is read only you @@ -1001,6 +956,7 @@ const handleClientReady = async (socket, message) => { readOnlyId: sessionInfo.readOnlyPadId, readonly: sessionInfo.readonly, serverTimestamp: Date.now(), + sessionRefreshInterval: settings.cookie.sessionRefreshInterval, userId: sessionInfo.author, abiwordAvailable: settings.abiwordAvailable(), sofficeAvailable: settings.sofficeAvailable(), @@ -1116,57 +1072,24 @@ const handleClientReady = async (socket, message) => { /** * Handles a request for a rough changeset, the timeslider client needs it */ -const handleChangesetRequest = async (socket, message) => { - // check if all ok - if (message.data == null) { - messageLogger.warn('Dropped message, changeset request has no data!'); - return; - } - - if (message.data.granularity == null) { - 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!'); - return; - } - - if (message.data.start == null) { - messageLogger.warn('Dropped message, changeset request has no start!'); - return; - } - - if (message.data.requestID == null) { - messageLogger.warn('Dropped message, changeset request has no requestID!'); - return; - } - - const granularity = message.data.granularity; - const start = message.data.start; +const handleChangesetRequest = async (socket, {data: {granularity, start, requestID}}) => { + if (granularity == null) throw new Error('missing granularity'); + if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer'); + if (start == null) throw new Error('missing start'); + if (requestID == null) throw new Error('mising requestID'); const end = start + (100 * granularity); - - const {padId} = sessioninfos[socket.id]; - - // build the requested rough changesets and send them back - try { - const data = await getChangesetInfo(padId, start, end, granularity); - data.requestID = message.data.requestID; - socket.json.send({type: 'CHANGESET_REQ', data}); - } catch (err) { - messageLogger.error(`Error while handling a changeset request ${message.data} ` + - `for ${padId}: ${err.stack || err}`); - } + const {padId, author: authorId} = sessioninfos[socket.id]; + const pad = await padManager.getPad(padId, null, authorId); + const data = await getChangesetInfo(pad, start, end, granularity); + data.requestID = requestID; + socket.json.send({type: 'CHANGESET_REQ', data}); }; /** * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -const getChangesetInfo = async (padId, startNum, endNum, granularity) => { - const pad = await padManager.getPad(padId); +const getChangesetInfo = async (pad, startNum, endNum, granularity) => { const headRevision = pad.getHeadRevisionNumber(); // calculate the last full endnum @@ -1194,10 +1117,10 @@ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { const composedChangesets = {}; const revisionDate = []; const [lines] = await Promise.all([ - getPadLines(padId, startNum - 1), + getPadLines(pad, startNum - 1), // Get all needed composite Changesets. ...compositesChangesetNeeded.map(async (item) => { - const changeset = await composePadChangesets(padId, item.start, item.end); + const changeset = await composePadChangesets(pad, item.start, item.end); composedChangesets[`${item.start}/${item.end}`] = changeset; }), // Get all needed revision Dates. @@ -1243,9 +1166,7 @@ const getChangesetInfo = async (padId, startNum, endNum, 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 */ -const getPadLines = async (padId, revNum) => { - const pad = await padManager.getPad(padId); - +const getPadLines = async (pad, revNum) => { // get the atext let atext; @@ -1265,9 +1186,7 @@ const getPadLines = async (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 */ -const composePadChangesets = async (padId, startNum, endNum) => { - const pad = await padManager.getPad(padId); - +const composePadChangesets = async (pad, startNum, endNum) => { // fetch all changesets we need const headNum = pad.getHeadRevisionNumber(); endNum = Math.min(endNum, headNum + 1); @@ -1284,8 +1203,7 @@ const composePadChangesets = async (padId, startNum, endNum) => { const changesets = {}; await Promise.all(changesetsNeeded.map( (revNum) => pad.getRevisionChangeset(revNum) - .then((changeset) => changesets[revNum] = changeset) - )); + .then((changeset) => changesets[revNum] = changeset))); // compose Changesets let r; @@ -1301,7 +1219,7 @@ const composePadChangesets = async (padId, startNum, endNum) => { } catch (e) { // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 messageLogger.warn( - `failed to compose cs in pad: ${padId} startrev: ${startNum} current rev: ${r}`); + `failed to compose cs in pad: ${pad.id} startrev: ${startNum} current rev: ${r}`); throw e; } }; diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 53bb6d241..863401ace 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -68,19 +68,19 @@ exports.setSocketIO = (_io) => { components[i].handleConnect(socket); } - socket.on('message', (message, ack = () => {}) => { + socket.on('message', (message, ack = () => {}) => (async () => { if (!message.component || !components[message.component]) { - logger.error(`Can't route the message: ${JSON.stringify(message)}`); - return; + throw new Error(`unknown message component: ${message.component}`); } - logger.debug(`from ${socket.id}: ${JSON.stringify(message)}`); - (async () => await components[message.component].handleMessage(socket, message))().then( - (val) => ack(null, val), - (err) => { - logger.error(`Error while handling message from ${socket.id}: ${err.stack || err}`); - ack({name: err.name, message: err.message}); - }); - }); + logger.debug(`from ${socket.id}:`, message); + return await components[message.component].handleMessage(socket, message); + })().then( + (val) => ack(null, val), + (err) => { + logger.error( + `Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`); + ack({name: err.name, message: err.message}); // socket.io can't handle Error objects. + })); socket.on('disconnect', (reason) => { logger.debug(`${socket.id} disconnected: ${reason}`); diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 351ab5bf2..9c42fd6d8 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -12,9 +12,11 @@ const SessionStore = require('../db/SessionStore'); const settings = require('../utils/Settings'); const stats = require('../stats'); const util = require('util'); +const webaccess = require('./express/webaccess'); const logger = log4js.getLogger('http'); let serverName; +let sessionStore; const sockets = new Set(); const socketsEvents = new events.EventEmitter(); const startTime = stats.settableGauge('httpStartTime'); @@ -22,32 +24,35 @@ const startTime = stats.settableGauge('httpStartTime'); exports.server = null; const closeServer = async () => { - if (exports.server == null) return; - logger.info('Closing HTTP server...'); - // Call exports.server.close() to reject new connections but don't await just yet because the - // Promise won't resolve until all preexisting connections are closed. - const p = util.promisify(exports.server.close.bind(exports.server))(); - await hooks.aCallAll('expressCloseServer'); - // Give existing connections some time to close on their own before forcibly terminating. The time - // should be long enough to avoid interrupting most preexisting transmissions but short enough to - // avoid a noticeable outage. - const timeout = setTimeout(async () => { - logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); - for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); - }, 5000); - let lastLogged = 0; - while (sockets.size > 0) { - if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. - logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); - lastLogged = Date.now(); + if (exports.server != null) { + logger.info('Closing HTTP server...'); + // Call exports.server.close() to reject new connections but don't await just yet because the + // Promise won't resolve until all preexisting connections are closed. + const p = util.promisify(exports.server.close.bind(exports.server))(); + await hooks.aCallAll('expressCloseServer'); + // Give existing connections some time to close on their own before forcibly terminating. The + // time should be long enough to avoid interrupting most preexisting transmissions but short + // enough to avoid a noticeable outage. + const timeout = setTimeout(async () => { + logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); + for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); + }, 5000); + let lastLogged = 0; + while (sockets.size > 0) { + if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. + logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); + lastLogged = Date.now(); + } + await events.once(socketsEvents, 'updated'); } - await events.once(socketsEvents, 'updated'); + await p; + clearTimeout(timeout); + exports.server = null; + startTime.setValue(0); + logger.info('HTTP server closed'); } - await p; - clearTimeout(timeout); - exports.server = null; - startTime.setValue(0); - logger.info('HTTP server closed'); + if (sessionStore) sessionStore.shutdown(); + sessionStore = null; }; exports.createServer = async () => { @@ -169,16 +174,21 @@ exports.restartServer = async () => { })); } + app.use(cookieParser(settings.sessionKey, {})); + + sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); exports.sessionMiddleware = expressSession({ + propagateTouch: true, + rolling: true, secret: settings.sessionKey, - store: new SessionStore(), + store: sessionStore, resave: false, - saveUninitialized: true, + saveUninitialized: false, // Set the cookie name to a javascript identifier compatible string. Makes code handling it // cleaner :) name: 'express_sid', - proxy: true, cookie: { + maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null. sameSite: settings.cookie.sameSite, // The automatic express-session mechanism for determining if the application is being served @@ -200,9 +210,14 @@ exports.restartServer = async () => { secure: 'auto', }, }); + + // Give plugins an opportunity to install handlers/middleware before the express-session + // middleware. This allows plugins to avoid creating an express-session record in the database + // when it is not needed (e.g., public static content). + await hooks.aCallAll('expressPreSession', {app}); app.use(exports.sessionMiddleware); - app.use(cookieParser(settings.sessionKey, {})); + app.use(webaccess.checkAccess); await Promise.all([ hooks.aCallAll('expressConfigure', {app}), diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index a0fbbc638..010ab14e5 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -2,28 +2,29 @@ const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); -const formidable = require('formidable'); +const {Formidable} = require('formidable'); const apiHandler = require('../../handler/APIHandler'); const util = require('util'); -exports.expressCreateServer = (hookName, args, cb) => { +exports.expressPreSession = async (hookName, {app}) => { // 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) => { + app.post('/ep/pad/connection-diagnostic-info', (req, res) => { + new Formidable().parse(req, (err, fields, files) => { clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); res.end('OK'); }); }); const parseJserrorForm = async (req) => await new Promise((resolve, reject) => { - const form = new formidable.IncomingForm(); - form.maxFileSize = 1; // Files are not expected. Not sure if 0 means unlimited, so 1 is used. + const form = new Formidable({ + maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used. + }); form.on('error', (err) => reject(err)); form.parse(req, (err, fields) => err != null ? reject(err) : resolve(fields.errorInfo)); }); // The Etherpad client side sends information about client side javscript errors - args.app.post('/jserror', (req, res, next) => { + app.post('/jserror', (req, res, next) => { (async () => { const data = JSON.parse(await parseJserrorForm(req)); clientLogger.warn(`${data.msg} --`, { @@ -38,9 +39,7 @@ exports.expressCreateServer = (hookName, args, cb) => { }); // Provide a possibility to query the latest available API version - args.app.get('/api', (req, res) => { + app.get('/api', (req, res) => { res.json({currentVersion: apiHandler.latestApiVersion}); }); - - return cb(); }; diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index ab8d60376..96f0efab4 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -70,12 +70,12 @@ exports.expressCreateServer = (hookName, args, cb) => { args.app.post('/p/:pad/import', (req, res, next) => { (async () => { const {session: {user} = {}} = req; - const {accessStatus} = await securityManager.checkAccess( + const {accessStatus, authorID: authorId} = 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'); } - await importHandler.doImport(req, res, req.params.pad); + await importHandler.doImport(req, res, req.params.pad, authorId); })().catch((err) => next(err || new Error(err))); }); diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index c4c1ccf5c..0531854aa 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -540,9 +540,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { return definition; }; -exports.expressCreateServer = (hookName, args, cb) => { - const {app} = args; - +exports.expressPreSession = async (hookName, {app}) => { // create openapi-backend handlers for each api version under /api/{version}/* for (const version of Object.keys(apiHandler.version)) { // we support two different styles of api: flat + rest @@ -610,7 +608,10 @@ exports.expressCreateServer = (hookName, args, cb) => { } // pass to api handler - const data = await apiHandler.handle(version, funcName, fields, req, res).catch((err) => { + let data; + try { + data = await apiHandler.handle(version, funcName, fields, req, res); + } catch (err) { // convert all errors to http errors if (createHTTPError.isHttpError(err)) { // pass http errors thrown by handler forward @@ -625,7 +626,7 @@ exports.expressCreateServer = (hookName, args, cb) => { logger.error(err.stack || err.toString()); throw new createHTTPError.InternalError('internal error'); } - }); + } // return in common format const response = {code: 0, message: 'ok', data: data || null}; @@ -690,7 +691,6 @@ exports.expressCreateServer = (hookName, args, cb) => { }); } } - return cb(); }; // helper to get api root diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index b805fc4ba..ff1afa477 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -4,24 +4,27 @@ const padManager = require('../../db/PadManager'); exports.expressCreateServer = (hookName, args, cb) => { // redirects browser to the pad's sanitized url if needed. otherwise, renders the html - 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; - } + args.app.param('pad', (req, res, next, padId) => { + (async () => { + // ensure the padname is valid and the url doesn't end with a / + if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { + res.status(404).send('Such a padname is forbidden'); + return; + } - const 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 - const realURL = encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search; - res.header('Location', realURL); - res.status(302).send(`You should be redirected to ${realURL}`); - } + 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 + const realURL = + encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search; + res.header('Location', realURL); + res.status(302).send(`You should be redirected to ${realURL}`); + } + })().catch((err) => next(err || new Error(err))); }); return cb(); }; diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 47a657747..edb679940 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -105,6 +105,19 @@ exports.expressCreateServer = (hookName, args, cb) => { express.sessionMiddleware(req, {}, next); }); + io.use((socket, next) => { + socket.conn.on('packet', (packet) => { + // Tell express-session that the session is still active. The session store can use these + // touch events to defer automatic session cleanup, and if express-session is configured with + // rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not + // have a standard mechanism for periodically updating the browser's cookies, so the browser + // will not see the new cookie expiration time unless it makes a new HTTP request or the new + // cookie value is sent to the client in a custom socket.io message.) + if (socket.request.session != null) socket.request.session.touch(); + }); + next(); + }); + // var socketIOLogger = log4js.getLogger("socket.io"); // Debug logging now has to be set at an environment level, this is stupid. // https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0 diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 66ee0221e..4f41d8cd0 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -10,33 +10,28 @@ const settings = require('../../utils/Settings'); const util = require('util'); const webaccess = require('./webaccess'); -exports.expressCreateServer = (hookName, args, cb) => { - // expose current stats - args.app.get('/stats', (req, res) => { +exports.expressPreSession = async (hookName, {app}) => { + // This endpoint is intended to conform to: + // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html + app.get('/health', (req, res) => { + res.set('Content-Type', 'application/health+json'); + res.json({ + status: 'pass', + releaseId: settings.getEpVersion(), + }); + }); + + app.get('/stats', (req, res) => { res.json(require('../../stats').toJSON()); }); - // 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', (req, res) => { + app.get('/javascript', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); }); - - // serve robots.txt - args.app.get('/robots.txt', (req, res) => { - let filePath = path.join( - settings.root, - 'src', - 'static', - 'skins', - settings.skinName, - 'robots.txt' - ); + 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) { @@ -46,6 +41,34 @@ exports.expressCreateServer = (hookName, args, cb) => { }); }); + app.get('/favicon.ico', (req, res, next) => { + (async () => { + const fns = [ + ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), + path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + path.join(settings.root, 'src', 'static', 'favicon.ico'), + ]; + for (const fn of fns) { + try { + await fsp.access(fn, fs.constants.R_OK); + } catch (err) { + continue; + } + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + await util.promisify(res.sendFile.bind(res))(fn); + return; + } + next(); + })().catch((err) => next(err || new Error(err))); + }); +}; + +exports.expressCreateServer = (hookName, args, cb) => { + // serve index.html under / + args.app.get('/', (req, res) => { + res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); + }); + // serve pad.html under /p args.app.get('/p/:pad', (req, res, next) => { // The below might break for pads being rewritten @@ -77,25 +100,11 @@ exports.expressCreateServer = (hookName, args, cb) => { })); }); - args.app.get('/favicon.ico', (req, res, next) => { - (async () => { - const fns = [ - ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), - path.join(settings.root, 'src', 'static', 'favicon.ico'), - ]; - for (const fn of fns) { - try { - await fsp.access(fn, fs.constants.R_OK); - } catch (err) { - continue; - } - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); - await util.promisify(res.sendFile.bind(res))(fn); - return; - } - next(); - })().catch((err) => next(err || new Error(err))); + // The client occasionally polls this endpoint to get an updated expiration for the express_sid + // cookie. This handler must be installed after the express-session middleware. + args.app.put('/_extendExpressSessionLifetime', (req, res) => { + // express-session automatically calls req.session.touch() so we don't need to do it here. + res.json({status: 'ok'}); }); return cb(); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index 2b01f84cf..26c18995a 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -28,14 +28,14 @@ const getTar = async () => { return tar; }; -exports.expressCreateServer = async (hookName, args) => { +exports.expressPreSession = async (hookName, {app}) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); - args.app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); + app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. - args.app.all('/static/:filename(*)', minify.minify); + app.all('/static/:filename(*)', minify.minify); // Setup middleware that will package JavaScript files served by minify for // CommonJS loader on the client-side. @@ -53,12 +53,12 @@ exports.expressCreateServer = async (hookName, args) => { const associator = new StaticAssociator(associations); jsServer.setAssociator(associator); - args.app.use(jsServer.handle.bind(jsServer)); + 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', (req, res, next) => { + app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { const clientParts = plugins.parts.filter((part) => part.client_hooks != null); const clientPlugins = {}; for (const name of new Set(clientParts.map((part) => part.plugin))) { diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 1b1fe8f55..66b47d2af 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -29,8 +29,8 @@ const findSpecs = async (specDir) => { return specs; }; -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { +exports.expressPreSession = async (hookName, {app}) => { + app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { (async () => { const modules = []; await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { @@ -59,14 +59,14 @@ exports.expressCreateServer = (hookName, args, cb) => { const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); - args.app.get('/tests/frontend/index.html', (req, res) => { + app.get('/tests/frontend/index.html', (req, res) => { res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); }); // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // version used with Express v4.x) interprets '.' and '*' differently than regexp. - args.app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { + app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { (async () => { let file = sanitizePathname(req.params.file); if (['', '.', './'].includes(file)) file = 'index.html'; @@ -74,9 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => { })().catch((err) => next(err || new Error(err))); }); - args.app.get('/tests/frontend', (req, res) => { + app.get('/tests/frontend', (req, res) => { res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?')); }); - - return cb(); }; diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 8a183681c..81ed69b07 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -9,22 +9,13 @@ const readOnlyManager = require('../../db/ReadOnlyManager'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; -const staticPathsRE = new RegExp(`^/(?:${[ - 'api(?:/.*)?', - 'favicon\\.ico', - 'ep/pad/connection-diagnostic-info', - 'javascript', - 'javascripts/.*', - 'jserror/?', - 'locales\\.json', - 'locales/.*', - 'rest/.*', - 'pluginfw/.*', - 'robots.txt', - 'static/.*', - 'stats/?', - 'tests/frontend(?:/.*)?', -].join('|')})$`); +// 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]; exports.normalizeAuthzLevel = (level) => { if (!level) return false; @@ -45,27 +36,53 @@ 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; + if (!user || 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'; + return level && 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 requireAdmin = req.path.toLowerCase().startsWith('/admin'); - const aCallFirst0 = - async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; + // /////////////////////////////////////////////////////////////////////////////////////////////// + // 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. + // /////////////////////////////////////////////////////////////////////////////////////////////// - const requireAdmin = req.path.toLowerCase().indexOf('/admin') === 0; + let results; + let skip = false; + const preAuthorizeNext = (...args) => { skip = true; next(...args); }; + try { + results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext}, + // 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) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))); + } catch (err) { + httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`); + if (!skip) res.status(500).send('Internal Server Error'); + return; + } + if (skip) return; + 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'); + } // 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). @@ -99,39 +116,6 @@ const checkAccess = async (req, res, next) => { return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); }; - // /////////////////////////////////////////////////////////////////////////////////////////////// - // 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. @@ -151,18 +135,21 @@ const checkAccess = async (req, res, next) => { 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; + const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); if (httpBasicAuth) { const userpass = Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); ctx.username = userpass.shift(); + // Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype + // pollution warning below (when setting settings.users[ctx.username]) that isn't actually a + // problem unless the attacker can also set Object.prototype.password. + if (ctx.username === '__proto__') ctx.username = null; 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) { + if (!httpBasicAuth || !ctx.username || 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; @@ -199,7 +186,10 @@ const checkAccess = async (req, res, next) => { res.status(403).send('Forbidden'); }; -exports.expressConfigure = (hookName, args, cb) => { - args.app.use((req, res, next) => { checkAccess(req, res, next).catch(next); }); - return cb(); +/** + * Express middleware to authenticate the user and check authorization. Must be installed after the + * express-session middleware. + */ +exports.checkAccess = (req, res, next) => { + checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js index 1cd663c4d..c54348867 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.js @@ -41,6 +41,8 @@ const getAllLocales = () => { // add plugins languages (if any) for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { + // plugin locales should overwrite etherpad's core locales + if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue; extractLangs(path.join(pluginPath, 'locales')); } @@ -100,13 +102,13 @@ const generateLocaleIndex = (locales) => { }; -exports.expressCreateServer = (n, args, cb) => { +exports.expressPreSession = async (hookName, {app}) => { // regenerate locales on server restart const locales = getAllLocales(); const localeIndex = generateLocaleIndex(locales); exports.availableLangs = getAvailableLangs(locales); - args.app.get('/locales/:locale', (req, res) => { + app.get('/locales/:locale', (req, res) => { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { @@ -118,11 +120,9 @@ exports.expressCreateServer = (n, args, cb) => { } }); - args.app.get('/locales.json', (req, res) => { + app.get('/locales.json', (req, res) => { res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); }); - - return cb(); }; diff --git a/src/node/server.js b/src/node/server.js index 5fef8f330..6a494fe17 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -41,8 +41,8 @@ if (settings.dumpOnUncleanExit) { * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('12.13.0'); -NodeVersion.checkDeprecationStatus('12.13.0', '1.8.14'); +NodeVersion.enforceMinNodeVersion('12.17.0'); +NodeVersion.checkDeprecationStatus('12.17.0', '1.9.0'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); @@ -50,6 +50,7 @@ const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); +const {Gate} = require('./utils/promises'); const stats = require('./stats'); const logger = log4js.getLogger('server'); @@ -67,14 +68,6 @@ const State = { let state = State.INITIAL; -class Gate extends Promise { - constructor(executor = null) { - let res; - super((resolve, reject) => { res = resolve; if (executor != null) executor(resolve, reject); }); - this.resolve = res; - } -} - const removeSignalListener = (signal, listener) => { logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` + `Function code:\n${listener.toString()}\n` + @@ -114,7 +107,14 @@ exports.start = async () => { process.on('uncaughtException', (err) => { logger.debug(`uncaught exception: ${err.stack || err}`); - exports.exit(err); + + // eslint-disable-next-line promise/no-promise-in-callback + exports.exit(err) + .catch((err) => { + logger.error('Error in process exit', err); + // eslint-disable-next-line n/no-process-exit + process.exit(1); + }); }); // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an // unhandled rejection into an uncaught exception, which does cause Node.js to exit. @@ -165,7 +165,7 @@ exports.start = async () => { return express.server; }; -let stopDoneGate; +const stopDoneGate = new Gate(); exports.stop = async () => { switch (state) { case State.STARTING: @@ -187,7 +187,6 @@ exports.stop = async () => { throw new Error(`unknown State: ${state.toString()}`); } logger.info('Stopping Etherpad...'); - const stopDoneGate = new Gate(); state = State.STOPPING; try { let timeout = null; diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 45683bc65..e20739ad3 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -15,52 +15,50 @@ * limitations under the License. */ - -const db = require('../db/DB'); +const Stream = require('./Stream'); +const assert = require('assert').strict; +const authorManager = require('../db/AuthorManager'); const hooks = require('../../static/js/pluginfw/hooks'); +const padManager = require('../db/PadManager'); exports.getPadRaw = async (padId, readOnlyId) => { - const keyPrefixRead = `pad:${padId}`; - const keyPrefixWrite = readOnlyId ? `pad:${readOnlyId}` : keyPrefixRead; - const padcontent = await db.get(keyPrefixRead); - - const keySuffixes = ['']; - for (let i = 0; i <= padcontent.head; i++) keySuffixes.push(`:revs:${i}`); - for (let i = 0; i <= padcontent.chatHead; i++) keySuffixes.push(`:chat:${i}`); - - const data = {}; - for (const keySuffix of keySuffixes) { - const entry = data[keyPrefixWrite + keySuffix] = await db.get(keyPrefixRead + keySuffix); - - // Get the Pad Authors - if (entry.pool && entry.pool.numToAttrib) { - const authors = entry.pool.numToAttrib; - - for (const k of Object.keys(authors)) { - if (authors[k][0] === 'author') { - const authorId = authors[k][1]; - - // Get the author info - const authorEntry = await db.get(`globalAuthor:${authorId}`); - if (authorEntry) { - data[`globalAuthor:${authorId}`] = authorEntry; - if (authorEntry.padIDs) { - authorEntry.padIDs = readOnlyId || padId; - } - } - } + const dstPfx = `pad:${readOnlyId || padId}`; + const [pad, customPrefixes] = await Promise.all([ + padManager.getPad(padId), + hooks.aCallAll('exportEtherpadAdditionalContent'), + ]); + const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => { + const srcPfx = `${customPrefix}:${padId}`; + const dstPfx = `${customPrefix}:${readOnlyId || padId}`; + assert(!srcPfx.includes('*')); + const srcKeys = await pad.db.findKeys(`${srcPfx}:*`, null); + return (function* () { + yield [dstPfx, pad.db.get(srcPfx)]; + for (const k of srcKeys) { + assert(k.startsWith(`${srcPfx}:`)); + yield [`${dstPfx}${k.slice(srcPfx.length)}`, pad.db.get(k)]; } - } - } - - // 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}`; - const writeKey = readOnlyId ? `${prefix}:${readOnlyId}` : key; - data[writeKey] = await db.get(key); + })(); })); - + const records = (function* () { + for (const authorId of pad.getAllAuthors()) { + yield [`globalAuthor:${authorId}`, (async () => { + const authorEntry = await authorManager.getAuthor(authorId); + if (!authorEntry) return undefined; // Becomes unset when converted to JSON. + if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId; + return authorEntry; + })()]; + } + for (let i = 0; i <= pad.head; ++i) yield [`${dstPfx}:revs:${i}`, pad.getRevision(i)]; + for (let i = 0; i <= pad.chatHead; ++i) yield [`${dstPfx}:chat:${i}`, pad.getChatMessage(i)]; + for (const gen of pluginRecords) yield* gen; + })(); + const data = {[dstPfx]: pad}; + for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p; + await hooks.aCallAll('exportEtherpad', { + pad, + data, + dstPadId: readOnlyId || padId, + }); return data; }; diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index ba71269d1..7962476e8 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -19,6 +19,7 @@ * limitations under the License. */ +const AttributeMap = require('../../static/js/AttributeMap'); const Changeset = require('../../static/js/Changeset'); exports.getPadPlainText = (pad, revNum) => { @@ -51,10 +52,10 @@ exports._analyzeLine = (text, aline, apool) => { let lineMarker = 0; line.listLevel = 0; if (aline) { - const opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) { - const op = opIter.next(); - let listType = Changeset.opAttributeValue(op, 'list', apool); + const [op] = Changeset.deserializeOps(aline); + if (op != null) { + const attribs = AttributeMap.fromString(op.attribs, apool); + let listType = attribs.get('list'); if (listType) { lineMarker = 1; listType = /([a-z]+)([0-9]+)/.exec(listType); @@ -63,7 +64,7 @@ exports._analyzeLine = (text, aline, apool) => { line.listLevel = Number(listType[2]); } } - const start = Changeset.opAttributeValue(op, 'start', apool); + const start = attribs.get('start'); if (start) { line.start = start; } diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index bc50da77b..d14f40e6e 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -16,6 +16,7 @@ */ const Changeset = require('../../static/js/Changeset'); +const attributes = require('../../static/js/attributes'); const padManager = require('../db/PadManager'); const _ = require('underscore'); const Security = require('../../static/js/security'); @@ -196,21 +197,20 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return; } - const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; // this iterates over every op string and decides which tags to open or to close // based on the attribs used - while (iter.hasNext()) { - const o = iter.next(); + for (const o of ops) { const usedAttribs = []; // mark all attribs as used - Changeset.eachAttribNumber(o.attribs, (a) => { + for (const a of attributes.decodeAttribString(o.attribs)) { if (a in anumMap) { usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. } - }); + } let outermostTag = -1; // find the outer most open tag that is no longer used for (let i = openTags.length - 1; i >= 0; i--) { @@ -316,8 +316,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { if ((!prevLine || prevLine.listLevel !== line.listLevel) || (line.listTypeName !== prevLine.listTypeName)) { const exists = _.find(openLists, (item) => ( - item.level === line.listLevel && item.type === line.listTypeName) - ); + item.level === line.listLevel && item.type === line.listTypeName)); if (!exists) { let prevLevel = 0; if (prevLine && prevLine.listLevel) { diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index 0ff7ded83..9511dd0e7 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -20,6 +20,7 @@ */ const Changeset = require('../../static/js/Changeset'); +const attributes = require('../../static/js/attributes'); const padManager = require('../db/PadManager'); const _analyzeLine = require('./ExportHelper')._analyzeLine; @@ -75,14 +76,13 @@ const getTXTFromAtext = (pad, atext, authorColors) => { return; } - const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; - while (iter.hasNext()) { - const o = iter.next(); + for (const o of ops) { let propChanged = false; - Changeset.eachAttribNumber(o.attribs, (a) => { + for (const a of attributes.decodeAttribString(o.attribs)) { if (a in anumMap) { const i = anumMap[a]; // i = 0 => bold, etc. @@ -93,7 +93,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { propVals[i] = STAY; } } - }); + } for (let i = 0; i < propVals.length; i++) { if (propVals[i] === true) { diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index 60a3b8492..da7e750ff 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -18,16 +18,17 @@ const AttributePool = require('../../static/js/AttributePool'); const {Pad} = require('../db/Pad'); -const async = require('async'); +const Stream = require('./Stream'); const authorManager = require('../db/AuthorManager'); const db = require('../db/DB'); const hooks = require('../../static/js/pluginfw/hooks'); const log4js = require('log4js'); const supportedElems = require('../../static/js/contentcollector').supportedElems; +const ueberdb = require('ueberdb2'); const logger = log4js.getLogger('ImportEtherpad'); -exports.setPadRaw = async (padId, r) => { +exports.setPadRaw = async (padId, r, authorId = '') => { const records = JSON.parse(r); // get supported block Elements from plugins, we will use this later. @@ -48,73 +49,73 @@ exports.setPadRaw = async (padId, r) => { if (originalPadId !== padId) throw new Error('unexpected pad ID in record'); }; - // Limit the number of in-flight database queries so that the queries do not time out when - // importing really large files. - const q = async.queue(async (task) => await task(), 100); - // First validate and transform values. Do not commit any records to the database yet in case // there is a problem with the data. - const dbRecords = new Map(); + const data = new Map(); const existingAuthors = new Set(); - await Promise.all(Object.entries(records).map(([key, value]) => q.pushAsync(async () => { - if (!value) { - return; - } - const keyParts = key.split(':'); - const [prefix, id] = keyParts; - if (prefix === 'globalAuthor' && keyParts.length === 2) { - // In the database, the padIDs subkey is an object (which is used as a set) that records every - // pad the author has worked on. When exported, that object becomes a single string containing - // the exported pad's ID. - if (typeof value.padIDs !== 'string') { - throw new TypeError('globalAuthor padIDs subkey is not a string'); - } - checkOriginalPadId(value.padIDs); - if (await authorManager.doesAuthorExist(id)) { - existingAuthors.add(id); + const padDb = new ueberdb.Database('memory', {data}); + await padDb.init(); + try { + const processRecord = async (key, value) => { + if (!value) return; + const keyParts = key.split(':'); + const [prefix, id] = keyParts; + if (prefix === 'globalAuthor' && keyParts.length === 2) { + // In the database, the padIDs subkey is an object (which is used as a set) that records + // every pad the author has worked on. When exported, that object becomes a single string + // containing the exported pad's ID. + if (typeof value.padIDs !== 'string') { + throw new TypeError('globalAuthor padIDs subkey is not a string'); + } + checkOriginalPadId(value.padIDs); + if (await authorManager.doesAuthorExist(id)) { + existingAuthors.add(id); + return; + } + value.padIDs = {[padId]: 1}; + } else if (padKeyPrefixes.includes(prefix)) { + checkOriginalPadId(id); + if (prefix === 'pad' && keyParts.length === 2) { + const pool = new AttributePool().fromJsonable(value.pool); + const unsupportedElements = new Set(); + pool.eachAttrib((k, v) => { + if (!supportedElems.has(k)) unsupportedElements.add(k); + }); + if (unsupportedElements.size) { + logger.warn(`(pad ${padId}) unsupported attributes (try installing a plugin): ` + + `${[...unsupportedElements].join(', ')}`); + } + } + keyParts[1] = padId; + key = keyParts.join(':'); + } else { + logger.debug(`(pad ${padId}) The record with the following key will be ignored unless an ` + + `importEtherpad hook function processes it: ${key}`); return; } - value.padIDs = {[padId]: 1}; - } else if (padKeyPrefixes.includes(prefix)) { - checkOriginalPadId(id); - if (prefix === 'pad' && keyParts.length === 2) { - const pool = new AttributePool().fromJsonable(value.pool); - const unsupportedElements = new Set(); - pool.eachAttrib((k, v) => { - if (!supportedElems.has(k)) unsupportedElements.add(k); - }); - if (unsupportedElements.size) { - logger.warn(`(pad ${padId}) unsupported attributes (try installing a plugin): ` + - `${[...unsupportedElements].join(', ')}`); - } - } - keyParts[1] = padId; - key = keyParts.join(':'); - } else { - logger.warn(`(pad ${padId}) Ignoring record with unsupported key: ${key}`); - return; - } - dbRecords.set(key, value); - }))); + await padDb.set(key, value); + }; + const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v)); + for (const op of readOps.batch(100).buffer(99)) await op; - const pad = new Pad(padId, { - // Only fetchers are needed to check the pad's integrity. - get: async (k) => dbRecords.get(k), - getSub: async (k, sub) => { - let v = dbRecords.get(k); - for (const sk of sub) { - if (v == null) return null; - v = v[sk]; - } - return v; - }, - }); - await pad.init(); - await pad.check(); + const pad = new Pad(padId, padDb); + await pad.init(null, authorId); + await hooks.aCallAll('importEtherpad', { + pad, + // Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but + // it's not worth the added complexity. + data: Object.freeze(records), + srcPadId: originalPadId, + }); + await pad.check(); + } finally { + await padDb.close(); + } - await Promise.all([ - ...[...dbRecords].map(([k, v]) => q.pushAsync(() => db.set(k, v))), - ...[...existingAuthors].map((a) => q.pushAsync(() => authorManager.addPad(a, padId))), - ]); + const writeOps = (function* () { + for (const [k, v] of data) yield db.set(k, v); + for (const a of existingAuthors) yield authorManager.addPad(a, padId); + })(); + for (const op of new Stream(writeOps).batch(100).buffer(99)) await op; }; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 58b79f3a1..d7b2172b0 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -19,13 +19,17 @@ const log4js = require('log4js'); const Changeset = require('../../static/js/Changeset'); const contentcollector = require('../../static/js/contentcollector'); const jsdom = require('jsdom'); -const rehype = require('rehype'); -const minifyWhitespace = require('rehype-minify-whitespace'); const apiLogger = log4js.getLogger('ImportHtml'); -const processor = rehype().use(minifyWhitespace, {newlines: false}); +let processor; + +exports.setPadHTML = async (pad, html, authorId = '') => { + if (processor == null) { + const [{rehype}, {default: minifyWhitespace}] = + await Promise.all([import('rehype'), import('rehype-minify-whitespace')]); + processor = rehype().use(minifyWhitespace, {newlines: false}); + } -exports.setPadHTML = async (pad, html) => { html = String(await processor.process(html)); const {window: {document}} = new jsdom.JSDOM(html); @@ -67,12 +71,10 @@ exports.setPadHTML = async (pad, html) => { const builder = Changeset.builder(1); // assemble each line into the builder - const attribsIter = Changeset.opIterator(newAttribs); let textIndex = 0; const newTextStart = 0; const newTextEnd = newText.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); + for (const op of Changeset.deserializeOps(newAttribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { const start = Math.max(newTextStart, textIndex); @@ -86,6 +88,6 @@ exports.setPadHTML = async (pad, html) => { const theChangeset = builder.toString(); apiLogger.debug(`The changeset: ${theChangeset}`); - await pad.setText('\n'); - if (!Changeset.isIdentity(theChangeset)) await pad.appendRevision(theChangeset); + await pad.setText('\n', authorId); + await pad.appendRevision(theChangeset, authorId); }; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 58dba2097..2e8a2d960 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -166,8 +166,8 @@ const minify = async (req, res) => { filename = path.join('../node_modules/', library, libraryPath); } } - const [, spec] = /^plugins\/ep_etherpad-lite\/(tests\/frontend\/specs\/.*)/.exec(filename) || []; - if (spec != null) filename = `../${spec}`; + const [, testf] = /^plugins\/ep_etherpad-lite\/(tests\/frontend\/.*)/.exec(filename) || []; + if (testf != null) filename = `../${testf}`; const contentType = mime.lookup(filename); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 814601f89..51f48237a 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -165,12 +165,12 @@ exports.padOptions = { showChat: true, showLineNumbers: true, useMonospaceFont: false, - userName: false, - userColor: false, + userName: null, + userColor: null, rtl: false, alwaysShowChat: false, chatAndUsers: false, - lang: 'en-gb', + lang: null, }; /** @@ -322,6 +322,8 @@ exports.cookie = { * https://stackoverflow.com/q/41841880 for discussion. */ sameSite: 'Lax', + sessionLifetime: 10 * 24 * 60 * 60 * 1000, + sessionRefreshInterval: 1 * 24 * 60 * 60 * 1000, }; /* diff --git a/src/node/utils/Stream.js b/src/node/utils/Stream.js new file mode 100644 index 000000000..611b83b33 --- /dev/null +++ b/src/node/utils/Stream.js @@ -0,0 +1,134 @@ +'use strict'; + +/** + * Wrapper around any iterable that adds convenience methods that standard JavaScript iterable + * objects lack. + */ +class Stream { + /** + * @returns {Stream} A Stream that yields values in the half-open range [start, end). + */ + static range(start, end) { + return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })()); + } + + /** + * @param {Iterable} values - Any iterable of values. + */ + constructor(values) { + this._iter = values[Symbol.iterator](); + this._next = null; + } + + /** + * Read values a chunk at a time from the underlying iterable. Once a full batch is read (or there + * aren't enough values to make a full batch), all of the batch's values are yielded before the + * next batch is read. + * + * This is useful for triggering groups of asynchronous tasks via Promises yielded from a + * synchronous generator. A for-await-of (or for-of with an await) loop consumes those Promises + * and automatically triggers the next batch of tasks when needed. For example: + * + * const resources = (function* () { + * for (let i = 0; i < 100; ++i) yield fetchResource(i); + * }).call(this); + * + * // Fetch 10 items at a time so that the fetch engine can bundle multiple requests into a + * // single query message. + * for await (const r of new Stream(resources).batch(10)) { + * processResource(r); + * } + * + * Chaining .buffer() after .batch() like stream.batch(n).buffer(m) will fetch in batches of n as + * needed to ensure that at least m are in flight at all times. + * + * Any Promise yielded by the underlying iterable has its rejection suppressed to prevent + * unhandled rejection errors while the Promise is sitting in the batch waiting to be yielded. It + * is assumed that the consumer of any yielded Promises will await the Promise (or call .catch() + * or .then()) to prevent the rejection from going unnoticed. If iteration is aborted early, any + * Promises read from the underlying iterable that have not yet been yielded will have their + * rejections un-suppressed to trigger unhandled rejection errors. + * + * @param {number} size - The number of values to read at a time. + * @returns {Stream} A new Stream that gets its values from this Stream. + */ + batch(size) { + return new Stream((function* () { + const b = []; + try { + for (const v of this) { + Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. + b.push(v); + if (b.length < size) continue; + while (b.length) yield b.shift(); + } + while (b.length) yield b.shift(); + } finally { + for (const v of b) Promise.resolve(v).then(() => {}); // Un-suppress unhandled rejections. + } + }).call(this)); + } + + /** + * Pre-fetch a certain number of values from the underlying iterable before yielding the first + * value. Each time a value is yielded (consumed from the buffer), another value is read from the + * underlying iterable and added to the buffer. + * + * This is useful for maintaining a constant number of in-flight asynchronous tasks via Promises + * yielded from a synchronous generator. A for-await-of (or for-of with an await) loop should be + * used to control the scheduling of the next task. For example: + * + * const resources = (function* () { + * for (let i = 0; i < 100; ++i) yield fetchResource(i); + * }).call(this); + * + * // Fetching a resource is high latency, so keep multiple in flight at all times until done. + * for await (const r of new Stream(resources).buffer(10)) { + * processResource(r); + * } + * + * Chaining after .batch() like stream.batch(n).buffer(m) will fetch in batches of n as needed to + * ensure that at least m are in flight at all times. + * + * Any Promise yielded by the underlying iterable has its rejection suppressed to prevent + * unhandled rejection errors while the Promise is sitting in the batch waiting to be yielded. It + * is assumed that the consumer of any yielded Promises will await the Promise (or call .catch() + * or .then()) to prevent the rejection from going unnoticed. If iteration is aborted early, any + * Promises read from the underlying iterable that have not yet been yielded will have their + * rejections un-suppressed to trigger unhandled rejection errors. + * + * @param {number} capacity - The number of values to keep buffered. + * @returns {Stream} A new Stream that gets its values from this Stream. + */ + buffer(capacity) { + return new Stream((function* () { + const b = []; + try { + for (const v of this) { + Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. + // Note: V8 has good Array push+shift optimization. + while (b.length >= capacity) yield b.shift(); + b.push(v); + } + while (b.length) yield b.shift(); + } finally { + for (const v of b) Promise.resolve(v).then(() => {}); // Un-suppress unhandled rejections. + } + }).call(this)); + } + + /** + * Like Array.map(). + * + * @param {(v: any) => any} fn - Value transformation function. + * @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`. + */ + map(fn) { return new Stream((function* () { for (const v of this) yield fn(v); }).call(this)); } + + /** + * Implements the JavaScript iterable protocol. + */ + [Symbol.iterator]() { return this._iter; } +} + +module.exports = Stream; diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 670e8d6a1..4ab276b4b 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -1,5 +1,8 @@ 'use strict'; + +const AttributeMap = require('../../static/js/AttributeMap'); const Changeset = require('../../static/js/Changeset'); +const attributes = require('../../static/js/attributes'); const exportHtml = require('./ExportHtml'); function PadDiff(pad, fromRev, toRev) { @@ -32,16 +35,10 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { return false; } - // lets iterator over the operators - const iterator = Changeset.opIterator(unpacked.ops); - - // get the first operator, this should be a clear operator - const clearOperator = iterator.next(); + const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops); // check if there is only one operator - if (iterator.hasNext() === true) { - return false; - } + if (anotherOp != null) return false; // check if this operator doesn't change text if (clearOperator.opcode !== '=') { @@ -54,17 +51,11 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { return false; } - const attributes = []; - Changeset.eachAttribNumber(changeset, (attrNum) => { - attributes.push(attrNum); - }); + const [appliedAttribute, anotherAttribute] = + attributes.attribsFromString(clearOperator.attribs, this._pad.pool); - // check that this changeset uses only one attribute - if (attributes.length !== 1) { - return false; - } - - const appliedAttribute = this._pad.pool.getAttrib(attributes[0]); + // Check that the operation has exactly one attribute. + if (appliedAttribute == null || anotherAttribute != null) return false; // check if the applied attribute is an anonymous author attribute if (appliedAttribute[0] !== 'author' || appliedAttribute[1] !== '') { @@ -215,7 +206,6 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { // unpack const unpacked = Changeset.unpack(changeset); - const iterator = Changeset.opIterator(unpacked.ops); const assem = Changeset.opAssembler(); // create deleted attribs @@ -223,10 +213,7 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { const deletedAttrib = apool.putAttrib(['removed', true]); const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; - // iteratore over the operators of the changeset - while (iterator.hasNext()) { - const operator = iterator.next(); - + for (const operator of Changeset.deserializeOps(unpacked.ops)) { if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; @@ -271,22 +258,23 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curLine = 0; let curChar = 0; - let curLineOpIter = null; - let curLineOpIterLine; - let curLineNextOp = Changeset.newOp('+'); + let curLineOps = null; + let curLineOpsNext = null; + let curLineOpsLine; + let curLineNextOp = new Changeset.Op('+'); const unpacked = Changeset.unpack(cs); - const csIter = Changeset.opIterator(unpacked.ops); const builder = Changeset.builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { - if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); - curLineOpIterLine = curLine; + if (!curLineOps || curLineOpsLine !== curLine) { + curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + curLineOpsLine = curLine; let indexIntoLine = 0; - while (curLineOpIter.hasNext()) { - curLineNextOp = curLineOpIter.next(); + while (!curLineOpsNext.done) { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); break; @@ -296,16 +284,22 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { } while (numChars > 0) { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; - curLineOpIterLine = curLine; + curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); + curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { - curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : Changeset.newOp(); + if (curLineOpsNext.done) { + curLineNextOp = new Changeset.Op(); + } else { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + } } const charsToUse = Math.min(numChars, curLineNextOp.chars); @@ -317,7 +311,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curChar += charsToUse; } - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; } @@ -327,7 +321,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { if (L) { curLine += L; curChar = 0; - } else if (curLineOpIter && curLineOpIterLine === curLine) { + } else if (curLineOps && curLineOpsLine === curLine) { consumeAttribRuns(N, () => {}); } else { curChar += N; @@ -364,13 +358,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { }; }; - const attribKeys = []; - const attribValues = []; - - // iterate over all operators of this changeset - while (csIter.hasNext()) { - const csOp = csIter.next(); - + for (const csOp of Changeset.deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { const textBank = nextText(csOp.chars); @@ -379,38 +367,21 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { // 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 !== '*') { - const deletedAttrib = apool.putAttrib(['removed', true]); - let authorAttrib = apool.putAttrib(['author', '']); - - attribKeys.length = 0; - attribValues.length = 0; - Changeset.eachAttribNumber(csOp.attribs, (n) => { - attribKeys.push(apool.getAttribKey(n)); - attribValues.push(apool.getAttribValue(n)); - - if (apool.getAttribKey(n) === 'author') { - authorAttrib = n; + const attribs = AttributeMap.fromString(csOp.attribs, apool); + const undoBackToAttribs = cachedStrFunc((oldAttribsStr) => { + const oldAttribs = AttributeMap.fromString(oldAttribsStr, apool); + const backAttribs = new AttributeMap(apool) + .set('author', '') + .set('removed', 'true'); + for (const [key, value] of attribs) { + const oldValue = oldAttribs.get(key); + if (oldValue !== value) backAttribs.set(key, oldValue); } + // TODO: backAttribs does not restore removed attributes (it is missing attributes that + // are in oldAttribs but not in attribs). I don't know if that is intentional. + return backAttribs.toString(); }); - const 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]); - } - } - - return Changeset.makeAttribsString('=', backAttribs, apool); - }); - - const oldAttribsAddition = - `*${Changeset.numToString(deletedAttrib)}*${Changeset.numToString(authorAttrib)}`; - let textLeftToProcess = textBank; while (textLeftToProcess.length > 0) { @@ -443,7 +414,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let textBankIndex = 0; consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => { // get the old attributes back - const oldAttribs = (undoBackToAttribs(attribs) || '') + oldAttribsAddition; + const oldAttribs = undoBackToAttribs(attribs); builder.insert(processText.substr(textBankIndex, len), oldAttribs); textBankIndex += len; diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index 0c8dc24f3..bc9f8c2dc 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -54,3 +54,23 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => { } await Promise.all(promises); }; + +/** + * An ordinary Promise except the `resolve` and `reject` executor functions are exposed as + * properties. + */ +class Gate extends Promise { + // Coax `.then()` into returning an ordinary Promise, not a Gate. See + // https://stackoverflow.com/a/65669070 for the rationale. + static get [Symbol.species]() { return Promise; } + + constructor() { + // `this` is assigned when `super()` returns, not when it is called, so it is not acceptable to + // do the following because it will throw a ReferenceError when it dereferences `this`: + // super((resolve, reject) => Object.assign(this, {resolve, reject})); + let props; + super((resolve, reject) => props = {resolve, reject}); + Object.assign(this, props); + } +} +exports.Gate = Gate; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 896913ffe..08ae93f6b 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -54,6 +54,8 @@ , "broadcast_revisions.js" , "socketio.js" , "AttributeManager.js" + , "AttributeMap.js" + , "attributes.js" , "ChangesetUtils.js" ] , "ace2_inner.js": [ @@ -71,6 +73,8 @@ , "linestylefilter.js" , "domline.js" , "AttributeManager.js" + , "AttributeMap.js" + , "attributes.js" , "scroll.js" , "caretPosition.js" , "pad_utils.js" diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js index 3e435d28b..40a476878 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.js @@ -103,9 +103,7 @@ _.extend(Button.prototype, { tag('button', { 'class': ` ${this.attributes.class}`, 'data-l10n-id': this.attributes.localizationId, - }) - ) - ); + }))); }, }); @@ -144,9 +142,7 @@ _.extend(SelectButton.prototype, Button.prototype, { '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})); }, }); diff --git a/src/package-lock.json b/src/package-lock.json index 6e70f432c..a65b5bb70 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.8.18", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -16,11 +16,11 @@ } }, "@azure/abort-controller": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz", - "integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@azure/core-asynciterator-polyfill": { @@ -38,28 +38,38 @@ } }, "@azure/core-client": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.5.0.tgz", - "integrity": "sha512-YNk8i9LT6YcFdFO+RRU0E4Ef+A8Y5lhXo6lz61rwbG8Uo7kSqh0YqK04OexiilM43xd6n3Y9yBhLnb1NFNI9dA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.6.0.tgz", + "integrity": "sha512-YhSf4cb61ApSjItscp9XoaLq8KRnacPDAhmjAZSMnn/gs6FhFbZNfOBOErG2dDj7JRknVtCmJ5mLmfR2sLa11A==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.5.0", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "requires": { + "tslib": "^2.2.0" + } + } } }, "@azure/core-http": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.4.tgz", - "integrity": "sha512-QmmJmexXKtPyc3/rsZR/YTLDvMatzbzAypJmLzvlfxgz/SkgnqV/D4f6F2LsK6tBj1qhyp8BoXiOebiej0zz3A==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.2.tgz", + "integrity": "sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", @@ -70,13 +80,31 @@ "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "dependencies": { + "@azure/core-util": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz", + "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } } } }, @@ -101,13 +129,14 @@ } }, "@azure/core-rest-pipeline": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.8.0.tgz", - "integrity": "sha512-o8eZr96erQpiq8EZhZU/SyN6ncOfZ6bexwN2nMm9WpDmZGvaq907kopADt8XvNhbEF7kRA1l901Pg8mXjWp3UQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.8.1.tgz", + "integrity": "sha512-R/XpxZcDgGbnneEifnsAcjLoR2NCmlDxKDmzd8oi5jx5YEnPE6gsxHQWAk2+uY55Ka717x/fdctyoCYKnumrqw==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^4.0.1", @@ -116,6 +145,42 @@ "uuid": "^8.3.0" }, "dependencies": { + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -133,11 +198,11 @@ } }, "@azure/core-util": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0-beta.1.tgz", - "integrity": "sha512-pS6cup979/qyuyNP9chIybK2qVkJ3MarbY/bx3JcGKE6An6dRweLnsfJfU2ydqUI/B51Rjnn59ajHIhCUTwWZw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0.tgz", + "integrity": "sha512-yWshY9cdPthlebnb3Zuz/j0Lv4kjU6u7PR5sW7A9FF7EX+0irMRJAtyTq5TPiDHJfjH8gTSlnIYFj9m7Ed76IQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@azure/identity": { @@ -231,21 +296,52 @@ } }, "@azure/msal-node": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.9.0.tgz", - "integrity": "sha512-lw6ejz1WPqcdjkwp91Gidte98+kfGxHk9eYSmmpUChzrUUrZMFGvrtrvG3Qnr6bp5d4WijVge9LMe+2QQUMhoA==", + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.14.6.tgz", + "integrity": "sha512-em/qqFL5tLMxMPl9vormAs13OgZpmQoJbiQ/GlWr+BA77eCLoL+Ehr5xRHowYo+LFe5b+p+PJVkRvT+mLvOkwA==", "requires": { - "@azure/msal-common": "^6.3.0", - "axios": "^0.21.4", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^8.5.1", + "@azure/msal-common": "^9.0.2", + "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, "dependencies": { "@azure/msal-common": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-6.3.0.tgz", - "integrity": "sha512-ZyLq9GdnLBi/83YpysE86TFKbA0TuvfNAN5Psqu20cdAjLo/4rw4ttiItdh1G//XeGErHk9qn57gi2AYU1b5/Q==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-9.0.2.tgz", + "integrity": "sha512-qzwxuF8kZAp+rNUactMCgJh8fblq9D4lSqrrIxMDzLjgSZtjN32ix7r/HBe8QdOr76II9SVVPcMkX4sPzPfQ7w==" + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } }, "uuid": { "version": "8.3.2", @@ -254,45 +350,19 @@ } } }, - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", + "debug": "^4.3.2", + "espree": "^9.3.1", "globals": "^13.9.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, @@ -309,34 +379,15 @@ "uri-js": "^4.2.2" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" } }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -348,36 +399,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "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 } } }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.4" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -392,11 +431,54 @@ } }, "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, "@js-joda/core": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-4.3.1.tgz", @@ -452,11 +534,43 @@ "tar": "^6.1.11" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@opentelemetry/api": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.1.0.tgz", "integrity": "sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ==" }, + "@rushstack/eslint-patch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", + "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", + "dev": true + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -467,18 +581,18 @@ } }, "@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/samsam": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", - "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", + "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -498,9 +612,17 @@ "integrity": "sha512-d/keJiNKfpHo+GmSB8QcsAwBx8h+V1UbdozA5TD+eSLXprNY53JAYub47J9evsSKWDdNG5uVj0FiMozLKuzowQ==" }, "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "@types/es-aggregate-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.2.tgz", + "integrity": "sha512-erqUpFXksaeR2kejKnhnjZjbFxUpGZx4Z7ydNL9ie8tEhXPiZTsLeUDJ6aR1F8j5wWUAtOAQWUqkc7givBJbBA==", + "requires": { + "@types/node": "*" + } }, "@types/hast": { "version": "2.3.4", @@ -515,6 +637,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -547,9 +675,9 @@ } }, "@types/parse5": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", - "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" }, "@types/tough-cookie": { "version": "4.0.2", @@ -569,6 +697,187 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "@typescript-eslint/eslint-plugin": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz", + "integrity": "sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/type-utils": "5.22.0", + "@typescript-eslint/utils": "5.22.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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 + } + } + }, + "@typescript-eslint/parser": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", + "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/typescript-estree": "5.22.0", + "debug": "^4.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz", + "integrity": "sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.22.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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 + } + } + }, + "@typescript-eslint/types": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", + "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "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 + } + } + }, + "@typescript-eslint/utils": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.22.0.tgz", + "integrity": "sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/typescript-estree": "5.22.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "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" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "eslint-visitor-keys": "^3.0.0" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -587,18 +896,18 @@ "optional": true }, "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" } }, "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" }, "acorn-globals": { "version": "6.0.0", @@ -646,9 +955,9 @@ }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -669,9 +978,9 @@ } }, "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", + "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -699,11 +1008,11 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "anymatch": { @@ -768,17 +1077,53 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } }, "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==" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { "safer-buffer": "~2.1.0" } @@ -788,16 +1133,10 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, "async": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", - "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "asynckit": { "version": "0.4.0", @@ -837,9 +1176,9 @@ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" }, "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==" }, "balanced-match": { "version": "1.0.2", @@ -939,34 +1278,22 @@ "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" }, "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "requires": { - "bytes": "3.1.0", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "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==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - } + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" } }, "brace-expansion": { @@ -1027,10 +1354,19 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "builtins": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-4.1.0.tgz", + "integrity": "sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { "version": "1.0.2", @@ -1052,9 +1388,9 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, "caseless": { @@ -1074,34 +1410,33 @@ } }, "ccount": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", - "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "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==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" }, "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==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -1112,6 +1447,17 @@ "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "chownr": { @@ -1121,9 +1467,9 @@ "optional": true }, "clean-css": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.1.tgz", - "integrity": "sha512-ooQCa1/70oRfVdUUGjKpbHuxgMgm8BsDT5EBqBGvPxMoRoGXf4PNx5mMnkjzJ9Ptx4vvmDdha0QVh86QtYIk1g==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", + "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", "requires": { "source-map": "~0.6.0" } @@ -1140,9 +1486,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -1152,23 +1498,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -1176,20 +1522,21 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "color-support": { "version": "1.1.3", @@ -1206,9 +1553,9 @@ } }, "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==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz", + "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==" }, "commander": { "version": "2.20.3", @@ -1242,11 +1589,18 @@ "optional": true }, "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "content-type": { @@ -1255,16 +1609,16 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "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==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", "requires": { - "cookie": "0.4.0", + "cookie": "0.4.1", "cookie-signature": "1.0.6" } }, @@ -1274,15 +1628,14 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, "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=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "cross-spawn": { "version": "7.0.3", @@ -1323,13 +1676,13 @@ } }, "data-urls": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.0.tgz", - "integrity": "sha512-4AefxbTTdFtxDUdh0BuMBs2qJVL25Mow2zlcuuePegQwgD6GEmQao42LLEeksOui8nL4RcNEugIpFP7eRd33xg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", + "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", "requires": { "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^9.0.0" + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0" } }, "debug": { @@ -1352,15 +1705,24 @@ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" }, "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=" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1378,14 +1740,14 @@ "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "detect-libc": { "version": "2.0.1", @@ -1393,12 +1755,30 @@ "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "optional": true }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "dirty": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.3.tgz", @@ -1414,18 +1794,11 @@ } }, "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" - } + "webidl-conversions": "^7.0.0" } }, "ecc-jsbn": { @@ -1448,14 +1821,14 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "ejs": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", - "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", + "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", "requires": { - "jake": "^10.6.1" + "jake": "^10.8.5" } }, "elasticsearch": { @@ -1500,7 +1873,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { "version": "3.5.0", @@ -1515,11 +1888,6 @@ "ws": "~7.4.2" }, "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1585,21 +1953,79 @@ "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" - } - }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "optional": true }, + "es-abstract": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", + "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + } + } + }, + "es-aggregate-error": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.8.tgz", + "integrity": "sha512-AKUb5MKLWMozPlFRHOKqWD7yta5uaEhH21qwtnf6FlKjNjTJOoqFi0/G14+FfSkIQhhu6X68Af4xgRC6y8qG4A==", + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "function-bind": "^1.1.1", + "functions-have-names": "^1.2.3", + "get-intrinsic": "^1.1.1", + "globalthis": "^1.0.2", + "has-property-descriptors": "^1.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "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==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1609,7 +2035,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { "version": "1.0.5", @@ -1629,49 +2055,44 @@ } }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", + "@eslint/eslintrc": "^1.2.2", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", + "glob-parent": "^6.0.1", "globals": "^13.6.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "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", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -1689,9 +2110,9 @@ } }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -1703,15 +2124,6 @@ "color-convert": "^2.0.1" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1738,9 +2150,9 @@ "dev": true }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -1758,16 +2170,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1810,27 +2212,15 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, - "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", @@ -1852,15 +2242,113 @@ } }, "eslint-config-etherpad": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-2.0.0.tgz", - "integrity": "sha512-ejBTLZiXkreSHNsdHWk/vCRkieYb6CpVZb/DH2QKbYktqRN/EFgaSISLb/8n8HZA5XvLVLbRDvDyBc/h3tIEcA==", - "dev": true + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-3.0.13.tgz", + "integrity": "sha512-Bwt1gDxThlXhY6wan1fb3Jy9kI+yFGctp7+JX6Xs+BwbOdrB4qObgnLKdcLYPKPqv9c4xTSKo3C4BdhTkg7WtQ==", + "dev": true, + "requires": { + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-mocha": "^10.0.4", + "eslint-plugin-n": "^15.2.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } }, "eslint-plugin-cypress": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.3.tgz", - "integrity": "sha512-hOoAid+XNFtpvOzZSNWP5LDrQBEJwbZwjib4XJ1KcRYKjeVj0mAmPmucG4Egli4j/aruv+Ow/acacoloWWCl9Q==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", + "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", "dev": true, "requires": { "globals": "^11.12.0" @@ -1875,13 +2363,30 @@ } }, "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", "dev": true, "requires": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" + }, + "dependencies": { + "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" + } + }, + "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-plugin-eslint-comments": { @@ -1892,57 +2397,66 @@ "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": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", - "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, "requires": { - "eslint-utils": "^3.0.0", - "ramda": "^0.27.1" + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" }, "dependencies": { - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "eslint-visitor-keys": "^2.0.0" + "esutils": "^2.0.2" } } } }, - "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==", + "eslint-plugin-mocha": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.0.4.tgz", + "integrity": "sha512-8wzAeepVY027oBHz/TmBmUr7vhVqoC1KTFeDybFLhbaWKx+aQ7fJJVuUsqcUy+L+G+XvgQBJY+cbAf7hl5DF7Q==", "dev": true, "requires": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", + "eslint-utils": "^3.0.0", + "ramda": "^0.28.0" + } + }, + "eslint-plugin-n": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.0.tgz", + "integrity": "sha512-lWLg++jGwC88GDGGBX3CMkk0GIWq0y41aH51lavWApOKcMQcYoL3Ayd0lEdtD3SnQtR+3qBvWQS3qGbR2BxRWg==", + "dev": true, + "requires": { + "builtins": "^4.0.0", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", "ignore": "^5.1.1", + "is-core-module": "^2.3.0", "minimatch": "^3.0.4", "resolve": "^1.10.1", - "semver": "^6.1.0" + "semver": "^6.3.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 - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -1958,9 +2472,9 @@ "dev": true }, "eslint-plugin-promise": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz", - "integrity": "sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", + "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==", "dev": true }, "eslint-plugin-you-dont-need-lodash-underscore": { @@ -1973,74 +2487,53 @@ } }, "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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, "requires": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - } + "estraverse": "^5.2.0" } }, "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "eslint-visitor-keys": "^2.0.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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true } } }, "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "optional": true }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", + "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", "dev": true, "requires": { - "acorn": "^7.4.0", + "acorn": "^8.7.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "eslint-visitor-keys": "^3.3.0" } }, "esprima": { @@ -2067,9 +2560,9 @@ } }, "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" }, "esutils": { "version": "2.0.3", @@ -2079,17 +2572,85 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "etherpad-cli-client": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.1.12.tgz", - "integrity": "sha512-7Cz9Ofd2xa4OJwOHNHyWdzKhRLLa17Mqbav2IV2old+DoVPUiFyOXz6YXaqBvkj09bS8BuTEQVajqo/rQ6N0LA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-2.0.1.tgz", + "integrity": "sha512-cv7ep8NEkrebTIgWS/SBvpt6DhcMKSNu1zZNMFOWdoQkNRn3hVXZU8dedr4Xt5M1zBwPBSBTjisU436/TkEESA==", "dev": true, "requires": { "async": "^3.2.1", "socket.io-client": "^2.3.0", - "superagent": "^6.1.0" + "superagent": "^7.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "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": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "superagent": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.5.tgz", + "integrity": "sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "^2.5.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + } + } } }, "etherpad-require-kernel": { @@ -2111,51 +2672,64 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "requires": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.11.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", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "express-rate-limit": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.0.tgz", - "integrity": "sha512-/1mrKggjXMxd1/ghPub5N3d36u5VlK8KjbQFQLxYub09BWSSgSXMQbXgFiIW0BYxjM49YCj8bkihONZR2U4+mQ==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.4.0.tgz", + "integrity": "sha512-lxQRZI4gi3qAWTf0/Uqsyugsz57h8bd7QyllXBgJvd6DJKokzW7C5DTaNvwzvAQzwHGFaItybfYGhC8gpu0V2A==" }, "express-session": { - "version": "1.17.2", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", - "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "version": "npm:@etherpad/express-session@1.18.1", + "resolved": "https://registry.npmjs.org/@etherpad/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-K+dYeES5iKPib+NRJ+/tX04IfTnCUnpV/aiuifI1ecQGnrWSVMuK5MrwDD0lYC/OBYWS5ovar5E+R4/IbjXknA==", "requires": { "cookie": "0.4.1", "cookie-signature": "1.0.6", @@ -2167,16 +2741,6 @@ "uid-safe": "~2.1.5" }, "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2199,6 +2763,30 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "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", @@ -2210,10 +2798,18 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fast-safe-stringify": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz", - "integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==", - "dev": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } }, "file-entry-cache": { "version": "6.0.1", @@ -2225,11 +2821,29 @@ } }, "filelist": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", - "integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz", + "integrity": "sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==", "requires": { - "minimatch": "^3.0.4" + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "fill-range": { @@ -2242,16 +2856,16 @@ } }, "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" } }, @@ -2261,13 +2875,12 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "locate-path": "^2.0.0" } }, "flat": { @@ -2284,23 +2897,12 @@ "requires": { "flatted": "^3.1.0", "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, "follow-redirects": { @@ -2324,9 +2926,15 @@ } }, "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } }, "forwarded": { "version": "0.2.0", @@ -2336,7 +2944,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "fs-minipass": { "version": "2.1.0", @@ -2370,6 +2978,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, "gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -2393,23 +3006,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "optional": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2437,6 +3033,15 @@ "has-symbols": "^1.0.1" } }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2446,9 +3051,9 @@ } }, "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2459,23 +3064,45 @@ } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, + "globalthis": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", + "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -2536,6 +3163,11 @@ "ansi-regex": "^2.0.0" } }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", @@ -2557,15 +3189,31 @@ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -2573,68 +3221,77 @@ "optional": true }, "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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.0.tgz", + "integrity": "sha512-vEr54rDu2CheBM4nLkWbW8Rycf8HhkA/KsrDnlyKnvBTyhyO+vAG6twHnfUbiRGo56YeUBNCI4HFfHg3Wu+tig==", "requires": { - "hast-util-is-element": "^1.1.0" + "hast-util-is-element": "^2.0.0" } }, "hast-util-from-parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", - "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", "requires": { - "@types/parse5": "^5.0.0", - "hastscript": "^6.0.0", - "property-information": "^5.0.0", - "vfile": "^4.0.0", - "vfile-location": "^3.2.0", - "web-namespaces": "^1.0.0" + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" } }, "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==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", + "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==", + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0" + } }, "hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "requires": { + "@types/hast": "^2.0.0" + } }, "hast-util-to-html": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz", - "integrity": "sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", + "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==", "requires": { - "ccount": "^1.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-is-element": "^1.0.0", - "hast-util-whitespace": "^1.0.0", - "html-void-elements": "^1.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0", - "stringify-entities": "^3.0.1", - "unist-util-is": "^4.0.0", - "xtend": "^4.0.0" + "@types/hast": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.2", + "unist-util-is": "^5.0.0" } }, "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==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", + "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==" }, "hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz", + "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==", "requires": { "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" } }, "he": { @@ -2643,31 +3300,41 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "requires": { - "whatwg-encoding": "^1.0.5" + "whatwg-encoding": "^2.0.0" } }, "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==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" }, "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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2677,23 +3344,33 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" } } }, "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "requires": { - "@tootallnate/once": "1", + "@tootallnate/once": "2", "agent-base": "6", "debug": "4" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -2725,9 +3402,9 @@ }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -2761,9 +3438,9 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, "immediate": { @@ -2807,11 +3484,29 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "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==" }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2821,19 +3516,41 @@ "binary-extensions": "^2.0.0" } }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + }, "is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "requires": { "has": "^1.0.3" } }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -2846,37 +3563,48 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true }, "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==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" } }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-observable": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==" }, "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==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz", + "integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==" }, "is-potential-custom-element-name": { "version": "1.0.1", @@ -2894,6 +3622,39 @@ "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2905,6 +3666,14 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -2929,21 +3698,14 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", "requires": { - "async": "0.9.x", - "chalk": "^2.4.2", + "async": "^3.2.3", + "chalk": "^4.0.2", "filelist": "^1.0.1", "minimatch": "^3.0.4" - }, - "dependencies": { - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - } } }, "js-cookie": { @@ -2951,12 +3713,6 @@ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2976,22 +3732,22 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-17.0.0.tgz", - "integrity": "sha512-MUq4XdqwtNurZDVeKScENMPHnkgmdIvMzZ1r1NSwHkDuaqI6BouPjr+17COo4/19oLNnmdpFDPOHVpgIZmZ+VA==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", + "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", "requires": { "abab": "^2.0.5", - "acorn": "^8.4.1", + "acorn": "^8.5.0", "acorn-globals": "^6.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", - "data-urls": "^3.0.0", + "data-urls": "^3.0.1", "decimal.js": "^10.3.1", - "domexception": "^2.0.1", + "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.0", @@ -3000,19 +3756,19 @@ "symbol-tree": "^3.2.4", "tough-cookie": "^4.0.0", "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^9.0.0", - "ws": "^8.0.0", - "xml-name-validator": "^3.0.0" + "w3c-xmlserializer": "^3.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0", + "ws": "^8.2.3", + "xml-name-validator": "^4.0.0" } }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { "version": "1.0.0", @@ -3030,10 +3786,19 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "jsonminify": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", - "integrity": "sha1-gF2vuzk5UYjO6atYLIHvlZ1+cQw=" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.2.tgz", + "integrity": "sha512-mEtP5ECD0293D+s45JhDutqF5mFCkWY8ClrPFxjSFR2KUoantofky7noSzyKnAnD9Gd8pXHZSUd5bgzLDUBbfA==" }, "jsonschema": { "version": "1.2.4", @@ -3047,71 +3812,33 @@ "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=", "dev": true }, - "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, "jszip": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", - "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "requires": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "setimmediate": "^1.0.5" }, "dependencies": { "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "readable-stream": { @@ -3207,12 +3934,13 @@ } }, "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "^5.0.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" } }, "lodash": { @@ -3231,52 +3959,11 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, "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==" }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" - }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3396,7 +4083,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memory-pager": { "version": "1.5.0", @@ -3407,39 +4094,60 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.50.0" + "mime-db": "1.52.0" } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, "minipass": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", @@ -3466,42 +4174,41 @@ "optional": true }, "mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.1.7", + "glob": "7.2.0", "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "3.0.4", + "minimatch": "4.2.1", "ms": "2.1.3", - "nanoid": "3.1.23", + "nanoid": "3.3.1", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.5", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -3521,18 +4228,70 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3665,22 +4424,12 @@ "axios-cookiejar-support": "^1.0.1", "qs": "^6.9.4", "tough-cookie": "^4.0.0" - }, - "dependencies": { - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } - } } }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true }, "native-duplexpair": { @@ -3695,18 +4444,18 @@ "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==" + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "nise": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", - "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" @@ -3813,6 +4562,15 @@ "wide-align": "^1.1.0" } }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3846,6 +4604,17 @@ "util-deprecate": "~1.0.1" } }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3883,9 +4652,9 @@ "dev": true }, "npm": { - "version": "6.14.15", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.15.tgz", - "integrity": "sha512-dkcQc4n+DiJAMYG2haNAMyJbmuvevjXz+WC9dCUzodw8EovwTIc6CATSsTEplCY6c0jG4OshxFGFJsrnKJguWA==", + "version": "6.14.16", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.16.tgz", + "integrity": "sha512-LMiLGYsVNJfVPlQg7v2NYjG7iRIapcLv+oMunlq7fkXVx0BATCjRu7XyWl0G+iuZzHy4CjtM32QB8ox8juTgaw==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -5259,7 +6028,7 @@ "bundled": true }, "json-schema": { - "version": "0.2.3", + "version": "0.4.0", "bundled": true }, "json-stringify-safe": { @@ -5271,12 +6040,12 @@ "bundled": true }, "jsprim": { - "version": "1.4.1", + "version": "1.4.2", "bundled": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -5584,17 +6353,6 @@ "mime-db": "~1.35.0" } }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true - }, "minizlib": { "version": "1.3.3", "bundled": true, @@ -5633,12 +6391,6 @@ "bundled": true, "requires": { "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "bundled": true - } } }, "move-concurrently": { @@ -6107,8 +6859,9 @@ "bundled": true }, "qs": { - "version": "6.5.2", - "bundled": true + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "query-string": { "version": "6.8.2", @@ -6999,7 +7752,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "nwsapi": { "version": "2.2.0", @@ -7018,9 +7772,36 @@ "optional": true }, "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "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" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } }, "observable-fns": { "version": "0.6.1", @@ -7028,9 +7809,9 @@ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==" }, "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "requires": { "ee-first": "1.1.1" } @@ -7059,29 +7840,19 @@ } }, "openapi-backend": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-4.2.0.tgz", - "integrity": "sha512-eqdgJAjDbVZ7zhiIF68mlItFxqE48OPAM9nHHYx6BJMoGK2xInSBc2Oqp4dzsrsLIzoY8nVzK/vUtYktyXGb9Q==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-5.3.0.tgz", + "integrity": "sha512-wl9LxdM0hgpy+I1cWsjEn4jPnBprd8QTLVwILG/wPc83cQ1CZ8anNmq0CUSYfhW4jG0oEik8TprSUO+pLeznhA==", "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.7", - "ajv": "^8.5.0", + "ajv": "^8.6.2", "bath-es5": "^3.0.3", "cookie": "^0.4.0", "lodash": "^4.17.15", "mock-json-schema": "^1.0.7", - "openapi-schema-validator": "^9.2.0", - "openapi-types": "^9.2.0", + "openapi-schema-validator": "^10.0.0", + "openapi-types": "^10.0.0", "qs": "^6.9.3" - }, - "dependencies": { - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "requires": { - "side-channel": "^1.0.4" - } - } } }, "openapi-schema-validation": { @@ -7096,20 +7867,20 @@ } }, "openapi-schema-validator": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.3.0.tgz", - "integrity": "sha512-KlvgZMWTu+H1FHFSZNAGj369uXl3BD1nXSIq+sXlG6P+OrsAHd3YORx0ZEZ3WGdu2LQrPGmtowGQavYXL+PLwg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-10.0.0.tgz", + "integrity": "sha512-tfXR7kLhI2f7QgArfJn5imkFJ1dvLOO5r4b1JZQoW3PgPb3caOiaWZ+H1iLzFj3IG1gmM3BrYZD1sAvWtDwUCA==", "requires": { "ajv": "^8.1.0", "ajv-formats": "^2.0.2", "lodash.merge": "^4.6.1", - "openapi-types": "^9.3.0" + "openapi-types": "^10.0.0" } }, "openapi-types": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.0.tgz", - "integrity": "sha512-sR23YjmuwDSMsQVZDHbV9mPgi0RyniQlqR0AQxTC2/F3cpSjRFMH3CFPjoWvNqhC4OxPkDYNb2l8Mc1Me6D/KQ==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-10.0.0.tgz", + "integrity": "sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==" }, "optional-js": { "version": "2.3.0", @@ -7138,23 +7909,29 @@ } }, "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "yocto-queue": "^0.1.0" + "p-try": "^1.0.0" } }, "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "^3.0.2" + "p-limit": "^1.1.0" } }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -7196,9 +7973,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "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==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, "path-is-absolute": { @@ -7219,7 +7996,13 @@ "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=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "performance-now": { "version": "2.1.0", @@ -7281,9 +8064,9 @@ } }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pify": { @@ -7329,12 +8112,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "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", @@ -7345,12 +8122,9 @@ } }, "property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "requires": { - "xtend": "^4.0.0" - } + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", + "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" }, "proxy-addr": { "version": "2.0.7", @@ -7372,20 +8146,29 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, "ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz", + "integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==", "dev": true }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" }, "randombytes": { "version": "2.1.0", @@ -7402,33 +8185,19 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.1.tgz", - "integrity": "sha512-u4Ual0ssf/RHHxK3rqKo9W2S7ulVoNdCAOrsk1gR9JLtzqg7fGw+yaCeyBAEncsL2n6XqHh/0qJk3BPDn49BjA==" + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz", + "integrity": "sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g==" }, "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==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.2", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" - }, - "dependencies": { - "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==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - } } }, "readable-stream": { @@ -7487,41 +8256,48 @@ "dev": true }, "rehype": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-11.0.0.tgz", - "integrity": "sha512-qXqRqiCFJD5CJ61CSJuNImTFrm3zVkOU9XywHDwrUuvWN74MWt72KJ67c5CM5x8g0vGcOkRVCrYj85vqkmHulQ==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", "requires": { - "rehype-parse": "^7.0.0", - "rehype-stringify": "^8.0.0", - "unified": "^9.0.0" + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" } }, "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==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", + "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==", "requires": { - "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" + "@types/hast": "^2.0.0", + "hast-util-embedded": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "unified": "^10.0.0", + "unist-util-is": "^5.0.0" } }, "rehype-parse": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-7.0.1.tgz", - "integrity": "sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", "requires": { - "hast-util-from-parse5": "^6.0.0", - "parse5": "^6.0.0" + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" } }, "rehype-stringify": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-8.0.0.tgz", - "integrity": "sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", "requires": { - "hast-util-to-html": "^7.1.1" + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" } }, "request": { @@ -7562,9 +8338,9 @@ } }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "tough-cookie": { "version": "2.5.0", @@ -7594,12 +8370,13 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "resolve-from": { @@ -7616,6 +8393,12 @@ "bluebird": ">= 2.3.2 < 3" } }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -7625,11 +8408,19 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "optional": true, "requires": { "glob": "^7.1.3" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7668,77 +8459,48 @@ "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" }, "selenium-webdriver": { - "version": "4.0.0-rc-1", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", - "integrity": "sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.1.tgz", + "integrity": "sha512-Fr9e9LC6zvD6/j7NO8M1M/NVxFX67abHcxDJoP5w2KN/Xb1SyYLjMVPGgD14U2TOiKe4XKHf42OmFw9g2JgCBQ==", "dev": true, "requires": { "jszip": "^3.6.0", - "rimraf": "^3.0.2", "tmp": "^0.2.1", "ws": ">=7.4.6" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "requires": { "lru-cache": "^6.0.0" } }, "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "requires": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.7.2", + "http-errors": "2.0.0", "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", + "ms": "2.1.3", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "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" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -7752,14 +8514,14 @@ } }, "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==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.1" + "send": "0.18.0" } }, "set-blocking": { @@ -7774,16 +8536,16 @@ "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", "dev": true }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "shebang-command": { "version": "2.0.0", @@ -7815,13 +8577,13 @@ "optional": true }, "simple-git": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.7.1.tgz", - "integrity": "sha512-+Osjtsumbtew2y9to0pOYjNzSIr4NkKGBg7Po5SUtjQhaJf2QBmiTX/9E9cv9rmc7oUiSGFIB9e7ys5ibnT9+A==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz", + "integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.3" + "debug": "^4.3.4" }, "dependencies": { "debug": { @@ -7840,16 +8602,16 @@ } }, "sinon": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", - "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", + "integrity": "sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^7.1.2", - "@sinonjs/samsam": "^6.0.2", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^6.1.1", "diff": "^5.0.0", - "nise": "^5.1.0", + "nise": "^5.1.1", "supports-color": "^7.2.0" }, "dependencies": { @@ -7870,48 +8632,11 @@ } } }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } - } + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true }, "socket.io": { "version": "2.4.1", @@ -7978,9 +8703,9 @@ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" }, "socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz", + "integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==", "requires": { "component-emitter": "~1.3.0", "debug": "~3.1.0", @@ -7990,9 +8715,9 @@ } }, "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==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.2.tgz", + "integrity": "sha512-QFZBaZDNqZXeemwejc7D39jrq2eGK/qZuVDiMPKzZK1hLlNvjGilGt4ckfQZeVX4dGmuPzCytN9ZW1nQlEWjgA==", "requires": { "component-emitter": "1.2.1", "debug": "~4.1.0", @@ -8002,7 +8727,7 @@ "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==" }, "debug": { "version": "4.1.1", @@ -8015,7 +8740,7 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" }, "ms": { "version": "2.1.3", @@ -8030,18 +8755,18 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "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==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", + "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==" }, "sparse-bitfield": { "version": "3.0.3", @@ -8084,9 +8809,9 @@ "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==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -8100,9 +8825,9 @@ } }, "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, "stoppable": { "version": "1.1.0", @@ -8110,13 +8835,49 @@ "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "string_decoder": { @@ -8125,13 +8886,12 @@ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "stringify-entities": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", - "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.2.tgz", + "integrity": "sha512-MTxTVcEkorNtBbNpoFJPEh0kKdM6+QbMjLbaxmvaPMmayOXdr/AIVIIJX7FReUVweRBFJfZepK4A4AKgwuFpMQ==", "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "xtend": "^4.0.0" + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" } }, "strip-ansi": { @@ -8142,49 +8902,114 @@ "ansi-regex": "^2.0.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "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 + }, "superagent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", - "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", - "dev": true, + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", "requires": { "component-emitter": "^1.3.0", - "cookiejar": "^2.1.2", - "debug": "^4.1.1", - "fast-safe-stringify": "^2.0.7", - "form-data": "^3.0.0", - "formidable": "^1.2.2", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", "methods": "^1.1.2", - "mime": "^2.4.6", - "qs": "^6.9.4", - "readable-stream": "^3.6.0", - "semver": "^7.3.2" + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "supertest": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.4.tgz", + "integrity": "sha512-M8xVnCNv+q2T2WXVzxDECvL2695Uv2uUj2O0utxsld/HRyJvOU8W9f1gvsYxSNU4wmIe0/L/ItnpU4iKq0emDA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^8.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" } }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true }, "ms": { @@ -8193,15 +9018,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -8227,26 +9043,40 @@ "requires": { "safe-buffer": "~5.2.0" } + }, + "superagent": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.0.tgz", + "integrity": "sha512-iudipXEel+SzlP9y29UBWGDjB+Zzag+eeA1iLosaR2YHBRr1Q1kC29iBrF2zIVD9fqVbpZnXkN/VJmwFMVyNWg==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + } } } }, - "supertest": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.1.6.tgz", - "integrity": "sha512-0hACYGNJ8OHRg8CRITeZOdbjur7NLuNs0mBjVhdpxi7hP6t3QIbOzLON5RTUmZcy2I9riuII3+Pr2C7yztrIIg==", - "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==", "requires": { - "methods": "^1.1.2", - "superagent": "^6.1.0" + "has-flag": "^4.0.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" - } + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "swagger-schema-official": { "version": "2.0.0-bab6bed", @@ -8259,54 +9089,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "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 - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, "tar": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", @@ -8327,14 +9109,16 @@ "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" }, "tedious": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-14.4.0.tgz", - "integrity": "sha512-vZQzqg3o7S1CddD1JxwxC+/Crq4kNSHV7NCiK64txURZKKvnc0wFF4mU0eeX1NXkw5m8mSbLX8wSj9EUZAN+fA==", + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-14.5.0.tgz", + "integrity": "sha512-Mr/ku6J0yku9MvWKO7e//awwI52122jS5AYRz/VOI2jZZawv84iHPKF/FnHBoIEKlRjzahrtevfpNktw/eBAEw==", "requires": { "@azure/identity": "^2.0.1", "@azure/keyvault-keys": "^4.3.0", "@js-joda/core": "^4.0.0", + "@types/es-aggregate-error": "^1.0.2", "bl": "^5.0.0", + "es-aggregate-error": "^1.0.7", "iconv-lite": "^0.6.3", "jsbi": "^3.2.1", "native-duplexpair": "^1.0.0", @@ -8392,20 +9176,14 @@ } }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.5.tgz", + "integrity": "sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==", "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } } }, "text-table": { @@ -8427,9 +9205,9 @@ }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -8445,6 +9223,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "optional": true, "requires": { "esm": "^3.2.25" } @@ -8461,17 +9240,6 @@ "dev": true, "requires": { "rimraf": "^3.0.0" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "to-array": { @@ -8488,11 +9256,6 @@ "is-number": "^7.0.0" } }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -8504,23 +9267,52 @@ } }, "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "requires": { "punycode": "^2.1.1" } }, "trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz", + "integrity": "sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w==" + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } }, "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -8568,6 +9360,12 @@ "mime-types": "~2.1.24" } }, + "typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true + }, "ueberdb2": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-2.2.4.tgz", @@ -8586,13 +9384,6 @@ "rethinkdb": "^2.4.2", "simple-git": "^3.6.0", "sqlite3": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a" - }, - "dependencies": { - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - } } }, "uid-safe": { @@ -8603,35 +9394,54 @@ "random-bytes": "~1.0.0" } }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + } + } + }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", + "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" }, "unified": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz", + "integrity": "sha512-v4ky1+6BN9X3pQrOdkFIPWAaeDsHPE1svRDxq7YpTc2plkIqFMwukfqM+l0ewpP9EfwARlt9pPFAeWYhHm8X9w==", "requires": { - "bail": "^1.0.0", + "@types/unist": "^2.0.0", + "bail": "^2.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" + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" } }, "unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz", + "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==" }, "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==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz", + "integrity": "sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==", "requires": { - "@types/unist": "^2.0.2" + "@types/unist": "^2.0.0" } }, "universalify": { @@ -8647,7 +9457,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "uri-js": { "version": "4.4.1", @@ -8665,7 +9475,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { "version": "3.4.0", @@ -8681,7 +9491,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "verror": { "version": "1.10.0", @@ -8691,31 +9501,42 @@ "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" + }, + "dependencies": { + "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=" + } } }, "vfile": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.0.tgz", + "integrity": "sha512-Tj44nY/48OQvarrE4FAjUfrv7GZOYzPbl5OD65HxVKwLJKMPU7zmfV8cCgCnzKWnSfYG2f3pxu+ALqs7j22xQQ==", "requires": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" } }, "vfile-location": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", - "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==" - }, - "vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", "requires": { "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" + "vfile": "^5.0.0" + } + }, + "vfile-message": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.0.tgz", + "integrity": "sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" } }, "w3c-hr-time": { @@ -8727,43 +9548,53 @@ } }, "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", + "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", "requires": { - "xml-name-validator": "^3.0.0" + "xml-name-validator": "^4.0.0" } }, "web-namespaces": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", - "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" }, "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "requires": { - "iconv-lite": "0.4.24" + "iconv-lite": "0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" }, "whatwg-url": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz", - "integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", + "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", "requires": { - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" } }, "which": { @@ -8774,12 +9605,25 @@ "isexe": "^2.0.0" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "requires": { - "string-width": "^1.0.2 || 2" + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "word-wrap": { @@ -8788,9 +9632,9 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, "workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "wrap-ansi": { @@ -8805,9 +9649,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -8841,23 +9685,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -8868,9 +9712,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.1.tgz", - "integrity": "sha512-XkgWpJU3sHU7gX8f13NqTn6KQ85bd1WU7noBHTT8fSohx7OS1TPY8k+cyRPCzFkia7C4mM229yeHr1qK9sM4JQ==" + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==" }, "wtfnode": { "version": "0.9.1", @@ -8878,18 +9722,9 @@ "integrity": "sha512-Ip6C2KeQPl/F3aP1EfOnPoQk14Udd9lffpoqWDNH3Xt78svxPbv53ngtmtfI0q2Te3oTq79XKTnRNXVIn/GsPA==" }, "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" }, "xmlbuilder": { "version": "11.0.1", @@ -8938,9 +9773,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -8950,23 +9785,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -8987,6 +9822,14 @@ "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "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==", + "dev": true + } } }, "yeast": { diff --git a/src/package.json b/src/package.json index c8b4e3408..2f845b1bd 100644 --- a/src/package.json +++ b/src/package.json @@ -30,212 +30,71 @@ } ], "dependencies": { - "async": "^3.2.1", - "clean-css": "^5.2.1", - "cookie-parser": "1.4.5", + "async": "^3.2.2", + "clean-css": "^5.3.0", + "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", - "ejs": "^3.1.6", + "ejs": "^3.1.7", "etherpad-require-kernel": "^1.0.15", "etherpad-yajsml": "0.0.12", - "express": "4.17.1", - "express-rate-limit": "5.5.0", - "express-session": "1.17.2", + "express": "4.18.2", + "express-rate-limit": "^6.4.0", + "express-session": "npm:@etherpad/express-session@^1.18.1", "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", - "formidable": "1.2.2", - "http-errors": "1.8.0", + "formidable": "^2.1.2", + "http-errors": "^2.0.0", "js-cookie": "^3.0.1", - "jsdom": "^17.0.0", - "jsonminify": "0.4.1", + "jsdom": "^19.0.0", + "jsonminify": "0.4.2", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", "log4js": "0.6.38", "measured-core": "^2.0.0", - "mime-types": "^2.1.33", + "mime-types": "^2.1.35", "npm": "^6.14.15", - "openapi-backend": "^4.2.0", + "openapi-backend": "^5.3.0", "proxy-addr": "^2.0.7", - "rate-limiter-flexible": "^2.3.1", - "rehype": "^11.0.0", - "rehype-minify-whitespace": "^4.0.5", + "rate-limiter-flexible": "^2.4.1", + "rehype": "^12.0.1", + "rehype-minify-whitespace": "^5.0.1", "request": "2.88.2", - "resolve": "1.20.0", + "resolve": "1.22.0", "security": "1.0.0", - "semver": "^7.3.5", + "semver": "^7.3.7", "socket.io": "^2.4.1", - "terser": "^5.9.0", + "superagent": "^8.0.9", + "terser": "^5.16.5", "threads": "^1.7.0", - "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^2.2.4", - "underscore": "1.13.1", + "underscore": "1.13.3", "unorm": "1.6.0", "wtfnode": "^0.9.1" }, "bin": { + "etherpad": "node/server.js", + "etherpad-healthcheck": "bin/etherpad-healthcheck", "etherpad-lite": "node/server.js" }, "devDependencies": { - "eslint": "^7.32.0", - "eslint-config-etherpad": "^2.0.0", - "eslint-plugin-cypress": "^2.11.3", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-mocha": "^9.0.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", - "etherpad-cli-client": "^0.1.12", - "mocha": "^9.1.1", + "eslint": "^8.14.0", + "eslint-config-etherpad": "^3.0.13", + "etherpad-cli-client": "^2.0.1", + "mocha": "^9.2.2", "mocha-froth": "^0.2.10", "nodeify": "^1.0.1", "openapi-schema-validation": "^0.4.2", - "selenium-webdriver": "^4.0.0-rc-1", + "selenium-webdriver": "^4.1.1", "set-cookie-parser": "^2.4.8", - "sinon": "^11.1.2", + "sinon": "^13.0.2", "split-grid": "^1.0.11", - "superagent": "^6.1.0", - "supertest": "^6.1.6" - }, - "eslintConfig": { - "ignorePatterns": [ - "/static/js/admin/jquery.autosize.js", - "/static/js/admin/minify.json.js", - "/static/js/vendors/browser.js", - "/static/js/vendors/farbtastic.js", - "/static/js/vendors/gritter.js", - "/static/js/vendors/html10n.js", - "/static/js/vendors/jquery.js", - "/static/js/vendors/nice-select.js", - "/tests/frontend/lib/" - ], - "overrides": [ - { - "files": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/node" - }, - { - "files": [ - "**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js", - "tests/frontend/**/*" - ], - "extends": "etherpad/node" - }, - { - "files": [ - "static/**/*", - "tests/frontend/helper.js", - "tests/frontend/helper/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/browser", - "env": { - "shared-node-browser": true - }, - "overrides": [ - { - "files": [ - "tests/frontend/helper/**/*" - ], - "globals": { - "helper": "readonly" - } - } - ] - }, - { - "files": [ - "tests/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js", - "tests/frontend/cypress/**/*", - "tests/frontend/helper.js", - "tests/frontend/helper/**/*", - "tests/frontend/travis/**/*", - "tests/ratelimit/**/*" - ], - "extends": "etherpad/tests", - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - }, - { - "files": [ - "tests/backend/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js" - ], - "extends": "etherpad/tests/backend", - "overrides": [ - { - "files": [ - "tests/backend/**/*" - ], - "excludedFiles": [ - "tests/backend/specs/**/*" - ], - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - } - ] - }, - { - "files": [ - "tests/frontend/**/*" - ], - "excludedFiles": [ - "**/.eslintrc.js", - "tests/frontend/cypress/**/*", - "tests/frontend/helper.js", - "tests/frontend/helper/**/*", - "tests/frontend/travis/**/*" - ], - "extends": "etherpad/tests/frontend", - "overrides": [ - { - "files": [ - "tests/frontend/**/*" - ], - "excludedFiles": [ - "tests/frontend/specs/**/*" - ], - "rules": { - "mocha/no-exports": "off", - "mocha/no-top-level-hooks": "off" - } - } - ] - }, - { - "files": [ - "tests/frontend/cypress/**/*" - ], - "extends": "etherpad/tests/cypress" - }, - { - "files": [ - "tests/frontend/travis/**/*" - ], - "extends": "etherpad/node" - } - ], - "root": true + "supertest": "^6.2.4", + "typescript": "^4.6.4" }, "engines": { - "node": ">=12.13.0", - "npm": ">=5.5.1" + "node": ">=14.15.0", + "npm": ">=6.14.0" }, "repository": { "type": "git", @@ -246,6 +105,6 @@ "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --timeout 5000 tests/container/specs/api" }, - "version": "1.8.18", + "version": "1.9.0", "license": "Apache-2.0" } diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 6de108e40..e9d9ff65d 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -1,25 +1,20 @@ -.popup.popup-show, .popup#users.chatAndUsers { - visibility: visible; -} - -.popup > .popup-content { - transform: scale(0.7); - opacity: 0; - transition: all 0.3s cubic-bezier(0.74, -0.05, 0.27, 1.75) -} - -.popup.popup-show > .popup-content, .popup#users.chatAndUsers > .popup-content { - transform: scale(1); - opacity: 1; -} - .popup { position: absolute; top: 10px; right: 30px; - visibility: hidden; + /* visibility must transition immediately so that input elements inside the popup can get focus */ + transition: all 0.3s cubic-bezier(0.74, -0.05, 0.27, 1.75), visibility 0s; z-index: 500; } + +.popup:not(.popup-show):not(#users.chatAndUsers) { + opacity: 0; + transform: scale(0.7); + /* visibility must not change to hidden until the end of the transition */ + transition: all 0.3s cubic-bezier(0.74, -0.05, 0.27, 1.75); + visibility: hidden; +} + #mycolorpicker { top: 0; } diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 124434031..f508af641 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -1,7 +1,9 @@ 'use strict'; +const AttributeMap = require('./AttributeMap'); const Changeset = require('./Changeset'); const ChangesetUtils = require('./ChangesetUtils'); +const attributes = require('./attributes'); const _ = require('./underscore'); const lineMarkerAttribute = 'lmkr'; @@ -147,13 +149,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ getAttributeOnLine(lineNum, attributeName) { // get `attributeName` attribute of first char of line 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) || ''; - } - } - return ''; + if (!aline) return ''; + const [op] = Changeset.deserializeOps(aline); + if (op == null) return ''; + return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; }, /* @@ -163,21 +162,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ getAttributesOnLine(lineNum) { // get attributes of first char of line 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 []; + if (!aline) return []; + const [op] = Changeset.deserializeOps(aline); + if (op == null) return []; + return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; }, /* @@ -201,9 +189,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ } } - const withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'], - ], rep.apool); + const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); const hasIt = (attribs) => withItRegex.test(attribs); @@ -234,13 +220,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ const end = selEnd[1]; let hasAttrib = true; - // Iterate over attribs on this line - - const opIter = Changeset.opIterator(rep.alines[lineNum]); let indexIntoLine = 0; - - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -273,32 +254,16 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ if (!aline) { return []; } - // iterate through all operations of a line - const opIter = Changeset.opIterator(aline); // we need to sum up how much characters each operations take until the wanted position let currentPointer = 0; - const attributes = []; - let currentOperation; - while (opIter.hasNext()) { - currentOperation = opIter.next(); + for (const currentOperation of Changeset.deserializeOps(aline)) { currentPointer += currentOperation.chars; - - if (currentPointer > column) { - // we got the operation of the wanted position, now collect all its attributes - Changeset.eachAttribNumber(currentOperation.attribs, (n) => { - attributes.push([ - this.rep.apool.getAttribKey(n), - this.rep.apool.getAttribValue(n), - ]); - }); - - // skip the loop - return attributes; - } + if (currentPointer <= column) continue; + return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; } - return attributes; + return []; }, /* diff --git a/src/static/js/AttributeMap.js b/src/static/js/AttributeMap.js new file mode 100644 index 000000000..55640eb8b --- /dev/null +++ b/src/static/js/AttributeMap.js @@ -0,0 +1,91 @@ +'use strict'; + +const attributes = require('./attributes'); + +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ + +/** + * A concatenated sequence of zero or more attribute identifiers, each one represented by an + * asterisk followed by a base-36 encoded attribute number. + * + * Examples: '', '*0', '*3*j*z*1q' + * + * @typedef {string} AttributeString + */ + +/** + * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs. + */ +class AttributeMap extends Map { + /** + * Converts an attribute string into an AttributeMap. + * + * @param {AttributeString} str - The attribute string to convert into an AttributeMap. + * @param {AttributePool} pool - Attribute pool. + * @returns {AttributeMap} + */ + static fromString(str, pool) { + return new AttributeMap(pool).updateFromString(str); + } + + /** + * @param {AttributePool} pool - Attribute pool. + */ + constructor(pool) { + super(); + /** @public */ + this.pool = pool; + } + + /** + * @param {string} k - Attribute name. + * @param {string} v - Attribute value. + * @returns {AttributeMap} `this` (for chaining). + */ + set(k, v) { + k = k == null ? '' : String(k); + v = v == null ? '' : String(v); + this.pool.putAttrib([k, v]); + return super.set(k, v); + } + + toString() { + return attributes.attribsToString(attributes.sort([...this]), this.pool); + } + + /** + * @param {Iterable} entries - [key, value] pairs to insert into this map. + * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the + * key is removed from this map (if present). + * @returns {AttributeMap} `this` (for chaining). + */ + update(entries, emptyValueIsDelete = false) { + for (let [k, v] of entries) { + k = k == null ? '' : String(k); + v = v == null ? '' : String(v); + if (!v && emptyValueIsDelete) { + this.delete(k); + } else { + this.set(k, v); + } + } + return this; + } + + /** + * @param {AttributeString} str - The attribute string identifying the attributes to insert into + * this map. + * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the + * key is removed from this map (if present). + * @returns {AttributeMap} `this` (for chaining). + */ + updateFromString(str, emptyValueIsDelete = false) { + return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete); + } +} + +module.exports = AttributeMap; diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 3479e03d1..ccdd2eb35 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -91,6 +91,17 @@ class AttributePool { this.nextNum = 0; } + /** + * @returns {AttributePool} A deep copy of this attribute pool. + */ + clone() { + const c = new AttributePool(); + for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]]; + Object.assign(c.attribToNum, this.attribToNum); + c.nextNum = this.nextNum; + return c; + } + /** * Add an attribute to the attribute set, or query for an existing attribute identifier. * @@ -164,7 +175,10 @@ class AttributePool { /** * @returns {Jsonable} An object that can be passed to `fromJsonable` to reconstruct this - * attribute pool. The returned object can be converted to JSON. + * attribute pool. The returned object can be converted to JSON. WARNING: The returned object + * has references to internal state (it is not a deep copy). Use the `clone()` method to copy + * a pool -- do NOT do `new AttributePool().fromJsonable(pool.toJsonable())` to copy because + * the resulting shared state will lead to pool corruption. */ toJsonable() { return { @@ -177,7 +191,10 @@ class AttributePool { * Replace the contents of this attribute pool with values from a previous call to `toJsonable`. * * @param {Jsonable} obj - Object returned by `toJsonable` containing the attributes and their - * identifiers. + * identifiers. WARNING: This function takes ownership of the object (it does not make a deep + * copy). Use the `clone()` method to copy a pool -- do NOT do + * `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared + * state will lead to pool corruption. */ fromJsonable(obj) { this.numToAttrib = obj.numToAttrib; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index c16a3a75c..53b3f2c8f 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -22,7 +22,9 @@ * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js */ +const AttributeMap = require('./AttributeMap'); const AttributePool = require('./AttributePool'); +const attributes = require('./attributes'); const {padutils} = require('./pad_utils'); /** @@ -31,6 +33,15 @@ const {padutils} = require('./pad_utils'); * @typedef {[string, string]} Attribute */ +/** + * A concatenated sequence of zero or more attribute identifiers, each one represented by an + * asterisk followed by a base-36 encoded attribute number. + * + * Examples: '', '*0', '*3*j*z*1q' + * + * @typedef {string} AttributeString + */ + /** * This method is called whenever there is an error in the sync process. * @@ -72,31 +83,74 @@ exports.numToString = (num) => num.toString(36).toLowerCase(); /** * An operation to apply to a shared document. - * - * @typedef {object} Op - * @property {('+'|'-'|'='|'')} opcode - The operation's operator: - * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base - * document. - * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base - * document. - * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in - * the document. The inserted characters come from the changeset's character bank. - * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an - * operation. - * @property {number} chars - The number of characters to keep, insert, or delete. - * @property {number} lines - The number of characters among the `chars` characters that are - * newlines. If non-zero, the last character must be a newline. - * @property {string} attribs - Identifiers of attributes to apply to the text, represented as a - * repeated (zero or more) sequence of asterisk followed by a non-negative base-36 (lower-case) - * integer. For example, '*2*1o' indicates that attributes 2 and 60 apply to the text affected - * by the operation. The identifiers come from the document's attribute pool. This is the empty - * string for remove ('-') operations. For keep ('=') operations, the attributes are merged with - * the base text's existing attributes: - * - A keep op attribute with a non-empty value replaces an existing base text attribute that - * has the same key. - * - A keep op attribute with an empty value is interpreted as an instruction to remove an - * existing base text attribute that has the same key, if one exists. */ +class Op { + /** + * @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property. + */ + constructor(opcode = '') { + /** + * The operation's operator: + * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in + * the document. The inserted characters come from the changeset's character bank. + * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an + * operation. + * + * @type {(''|'='|'+'|'-')} + * @public + */ + this.opcode = opcode; + + /** + * The number of characters to keep, insert, or delete. + * + * @type {number} + * @public + */ + this.chars = 0; + + /** + * The number of characters among the `chars` characters that are newlines. If non-zero, the + * last character must be a newline. + * + * @type {number} + * @public + */ + this.lines = 0; + + /** + * Identifiers of attributes to apply to the text, represented as a repeated (zero or more) + * sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example, + * '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The + * identifiers come from the document's attribute pool. + * + * For keep ('=') operations, the attributes are merged with the base text's existing + * attributes: + * - A keep op attribute with a non-empty value replaces an existing base text attribute that + * has the same key. + * - A keep op attribute with an empty value is interpreted as an instruction to remove an + * existing base text attribute that has the same key, if one exists. + * + * This is the empty string for remove ('-') operations. + * + * @type {string} + * @public + */ + this.attribs = ''; + } + + toString() { + if (!this.opcode) throw new TypeError('null op'); + if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string'); + const l = this.lines ? `|${exports.numToString(this.lines)}` : ''; + return this.attribs + l + this.opcode + exports.numToString(this.chars); + } +} +exports.Op = Op; /** * Describes changes to apply to a document. Does not include the attribute pool or the original @@ -126,55 +180,82 @@ exports.oldLen = (cs) => exports.unpack(cs).oldLen; */ exports.newLen = (cs) => exports.unpack(cs).newLen; +/** + * Parses a string of serialized changeset operations. + * + * @param {string} ops - Serialized changeset operations. + * @yields {Op} + * @returns {Generator} + */ +exports.deserializeOps = function* (ops) { + // TODO: Migrate to String.prototype.matchAll() once there is enough browser support. + const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; + let match; + while ((match = regex.exec(ops)) != null) { + if (match[5] === '$') return; // Start of the insert operation character bank. + if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`); + const op = new Op(match[3]); + op.lines = exports.parseNum(match[2] || '0'); + op.chars = exports.parseNum(match[4]); + op.attribs = match[1]; + yield op; + } +}; + /** * Iterator over a changeset's operations. * * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. * - * @typedef {object} OpIter - * @property {Function} hasNext - - * @property {Function} next - + * @deprecated Use `deserializeOps` instead. */ +class OpIter { + /** + * @param {string} ops - String encoding the change operations to iterate over. + */ + constructor(ops) { + this._gen = exports.deserializeOps(ops); + this._next = this._gen.next(); + } + + /** + * @returns {boolean} Whether there are any remaining operations. + */ + hasNext() { + return !this._next.done; + } + + /** + * Returns the next operation object and advances the iterator. + * + * Note: This does NOT implement the ECMAScript iterator protocol. + * + * @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value. + * @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are + * no more operations. + */ + next(opOut = new Op()) { + if (this.hasNext()) { + copyOp(this._next.value, opOut); + this._next = this._gen.next(); + } else { + clearOp(opOut); + } + return opOut; + } +} /** * Creates an iterator which decodes string changeset operations. * + * @deprecated Use `deserializeOps` instead. * @param {string} opsStr - String encoding of the change operations to perform. * @returns {OpIter} Operator iterator object. */ exports.opIterator = (opsStr) => { - const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - - const nextRegexMatch = () => { - const result = regex.exec(opsStr); - if (result[0] === '?') { - error('Hit error opcode in op stream'); - } - - return result; - }; - let regexResult = nextRegexMatch(); - - const next = (optOp) => { - const op = optOp || exports.newOp(); - if (regexResult[0]) { - op.attribs = regexResult[1]; - op.lines = exports.parseNum(regexResult[2] || '0'); - op.opcode = regexResult[3]; - op.chars = exports.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } else { - clearOp(op); - } - return op; - }; - - const hasNext = () => !!(regexResult[0]); - - return { - next, - hasNext, - }; + padutils.warnDeprecated( + 'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead'); + return new OpIter(opsStr); }; /** @@ -192,15 +273,14 @@ const clearOp = (op) => { /** * Creates a new Op object * + * @deprecated Use the `Op` class instead. * @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator. * @returns {Op} */ -exports.newOp = (optOpcode) => ({ - opcode: (optOpcode || ''), - chars: 0, - lines: 0, - attribs: '', -}); +exports.newOp = (optOpcode) => { + padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead'); + return new Op(optOpcode); +}; /** * Copies op1 to op2 @@ -209,7 +289,7 @@ exports.newOp = (optOpcode) => ({ * @param {Op} [op2] - dest Op. If not given, a new Op is used. * @returns {Op} `op2` */ -const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1); +const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1); /** * Serializes a sequence of Ops. @@ -236,15 +316,19 @@ const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1); * * @param {('-'|'+'|'=')} opcode - The operator to use. * @param {string} text - The text to remove/add/keep. - * @param {(string|Attribute[])} [attribs] - The attributes to apply to the operations. See - * `makeAttribsString`. - * @param {?AttributePool} [pool] - See `makeAttribsString`. + * @param {(Iterable|AttributeString)} [attribs] - The attributes to insert into the pool + * (if necessary) and encode. If an attribute string, no checking is performed to ensure that + * the attributes exist in the pool, are in the canonical order, and contain no duplicate keys. + * If this is an iterable of attributes, `pool` must be non-null. + * @param {?AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of + * attributes, ignored if `attribs` is an attribute string. * @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text. * @returns {Generator} */ const opsFromText = function* (opcode, text, attribs = '', pool = null) { - const op = exports.newOp(opcode); - op.attribs = exports.makeAttribsString(opcode, attribs, pool); + const op = new Op(opcode); + op.attribs = typeof attribs === 'string' + ? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString(); const lastNewlinePos = text.lastIndexOf('\n'); if (lastNewlinePos < 0) { op.chars = text.length; @@ -296,9 +380,7 @@ exports.checkRep = (cs) => { const assem = exports.smartOpAssembler(); let oldPos = 0; let calcNewLen = 0; - const iter = exports.opIterator(ops); - while (iter.hasNext()) { - const o = iter.next(); + for (const o of exports.deserializeOps(ops)) { switch (o.opcode) { case '=': oldPos += o.chars; @@ -390,13 +472,13 @@ exports.smartOpAssembler = () => { * @deprecated Use `opsFromText` instead. * @param {('-'|'+'|'=')} opcode - The operator to use. * @param {string} text - The text to remove/add/keep. - * @param {(string|Attribute[])} attribs - The attributes to apply to the operations. See - * `makeAttribsString`. - * @param {?AttributePool} pool - See `makeAttribsString`. + * @param {(string|Iterable)} attribs - The attributes to apply to the operations. + * @param {?AttributePool} pool - Attribute pool. Only required if `attribs` is an iterable of + * attribute key, value pairs. */ const appendOpWithText = (opcode, text, attribs, pool) => { - padutils.warnWithStack('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + - 'use opsFromText() instead.'); + padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + + 'use opsFromText() instead.'); for (const op of opsFromText(opcode, text, attribs, pool)) append(op); }; @@ -435,7 +517,7 @@ exports.smartOpAssembler = () => { */ exports.mergingOpAssembler = () => { const assem = exports.opAssembler(); - const bufOp = exports.newOp(); + const bufOp = new Op(); // If we get, for example, insertions [xxx\n,yyy], those don't merge, // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. @@ -443,6 +525,9 @@ exports.mergingOpAssembler = () => { // ops immediately after it. let bufOpAdditionalCharsAfterNewline = 0; + /** + * @param {boolean} [isEndDocument] + */ const flush = (isEndDocument) => { if (!bufOp.opcode) return; if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { @@ -511,12 +596,8 @@ exports.opAssembler = () => { * @param {Op} op - Operation to add. Ownership remains with the caller. */ const append = (op) => { - if (!op.opcode) throw new TypeError('null op'); - if (typeof op.attribs !== 'string') throw new TypeError('attribs must be a string'); - serialized += op.attribs; - if (op.lines) serialized += `|${exports.numToString(op.lines)}`; - serialized += op.opcode; - serialized += exports.numToString(op.chars); + assert(op instanceof Op, 'argument must be an instance of Op'); + serialized += op.toString(); }; const toString = () => serialized; @@ -598,6 +679,7 @@ exports.stringIterator = (str) => { */ exports.stringAssembler = () => ({ _str: '', + clear() { this._str = ''; }, /** * @param {string} x - */ @@ -627,43 +709,34 @@ exports.stringAssembler = () => ({ * actually a newline, but for the purposes of N and L values, the caller should pretend it is, and * for things to work right in that case, the input to the `insert` method should be a single line * with no newlines. - * - * @typedef {object} TextLinesMutator - * @property {Function} close - - * @property {Function} hasMore - - * @property {Function} insert - - * @property {Function} remove - - * @property {Function} removeLines - - * @property {Function} skip - - * @property {Function} skipLines - */ - -/** - * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). - * @returns {TextLinesMutator} - */ -const textLinesMutator = (lines) => { +class TextLinesMutator { /** - * curSplice holds values that will be passed as arguments to lines.splice() to insert, delete, or - * change lines: - * - curSplice[0] is an index into the lines array. - * - curSplice[1] is the number of lines that will be removed from the lines array starting at - * the index. - * - The other elements represent mutated (changed by ops) lines or new lines (added by ops) to - * insert at the index. - * - * @type {[number, number?, ...string[]?]} + * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). */ - const curSplice = [0, 0]; - let inSplice = false; - - // position in lines after curSplice is applied: - let curLine = 0; - let curCol = 0; - // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && - // curLine >= curSplice[0] - // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then - // curCol == 0 + constructor(lines) { + this._lines = lines; + /** + * this._curSplice holds values that will be passed as arguments to this._lines.splice() to + * insert, delete, or change lines: + * - this._curSplice[0] is an index into the this._lines array. + * - this._curSplice[1] is the number of lines that will be removed from the this._lines array + * starting at the index. + * - The other elements represent mutated (changed by ops) lines or new lines (added by ops) + * to insert at the index. + * + * @type {[number, number?, ...string[]?]} + */ + this._curSplice = [0, 0]; + this._inSplice = false; + // position in lines after curSplice is applied: + this._curLine = 0; + this._curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + } /** * Get a line from `lines` at given index. @@ -671,13 +744,13 @@ const textLinesMutator = (lines) => { * @param {number} idx - an index * @returns {string} */ - const linesGet = (idx) => { - if ('get' in lines) { - return lines.get(idx); + _linesGet(idx) { + if ('get' in this._lines) { + return this._lines.get(idx); } else { - return lines[idx]; + return this._lines[idx]; } - }; + } /** * Return a slice from `lines`. @@ -686,105 +759,108 @@ const textLinesMutator = (lines) => { * @param {number} end - the end index * @returns {string[]} */ - const linesSlice = (start, end) => { - if (lines.slice) { - return lines.slice(start, end); + _linesSlice(start, end) { + // can be unimplemented if removeLines's return value not needed + if (this._lines.slice) { + return this._lines.slice(start, end); } else { return []; } - }; + } /** * Return the length of `lines`. * * @returns {number} */ - const linesLength = () => { - if ((typeof lines.length) === 'number') { - return lines.length; + _linesLength() { + if (typeof this._lines.length === 'number') { + return this._lines.length; } else { - return lines.length(); + return this._lines.length(); } - }; + } /** * Starts a new splice. */ - const enterSplice = () => { - curSplice[0] = curLine; - curSplice[1] = 0; + _enterSplice() { + this._curSplice[0] = this._curLine; + this._curSplice[1] = 0; // TODO(doc) when is this the case? // check all enterSplice calls and changes to curCol - if (curCol > 0) { - putCurLineInSplice(); - } - inSplice = true; - }; + if (this._curCol > 0) this._putCurLineInSplice(); + this._inSplice = true; + } /** * Changes the lines array according to the values in curSplice and resets curSplice. Called via * close or TODO(doc). */ - const leaveSplice = () => { - lines.splice(...curSplice); - curSplice.length = 2; - curSplice[0] = curSplice[1] = 0; - inSplice = false; - }; + _leaveSplice() { + this._lines.splice(...this._curSplice); + this._curSplice.length = 2; + this._curSplice[0] = this._curSplice[1] = 0; + this._inSplice = false; + } /** * Indicates if curLine is already in the splice. This is necessary because the last element in - * curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting). - * - * TODO(doc) why aren't removals considered? + * curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting). * * @returns {boolean} true if curLine is in splice */ - const isCurLineInSplice = () => (curLine - curSplice[0] < (curSplice.length - 2)); + _isCurLineInSplice() { + // The value of `this._curSplice[1]` does not matter when determining the return value because + // `this._curLine` refers to the line number *after* the splice is applied (so after those lines + // are deleted). + return this._curLine - this._curSplice[0] < this._curSplice.length - 2; + } /** * Incorporates current line into the splice and marks its old position to be deleted. * * @returns {number} the index of the added line in curSplice */ - const putCurLineInSplice = () => { - if (!isCurLineInSplice()) { - curSplice.push(linesGet(curSplice[0] + curSplice[1])); - curSplice[1]++; + _putCurLineInSplice() { + if (!this._isCurLineInSplice()) { + this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1])); + this._curSplice[1]++; } - return 2 + curLine - curSplice[0]; // TODO should be the same as curSplice.length - 1 - }; + // TODO should be the same as this._curSplice.length - 1 + return 2 + this._curLine - this._curSplice[0]; + } /** * It will skip some newlines by putting them into the splice. * * @param {number} L - - * @param {boolean} includeInSplice - indicates if attributes are present + * @param {boolean} includeInSplice - Indicates that attributes are present. */ - const skipLines = (L, includeInSplice) => { + skipLines(L, includeInSplice) { if (!L) return; if (includeInSplice) { - if (!inSplice) enterSplice(); + if (!this._inSplice) this._enterSplice(); // TODO(doc) should this count the number of characters that are skipped to check? for (let i = 0; i < L; i++) { - curCol = 0; - putCurLineInSplice(); - curLine++; + this._curCol = 0; + this._putCurLineInSplice(); + this._curLine++; } } else { - if (inSplice) { + if (this._inSplice) { if (L > 1) { // TODO(doc) figure out why single lines are incorporated into splice instead of ignored - leaveSplice(); + this._leaveSplice(); } else { - putCurLineInSplice(); + this._putCurLineInSplice(); } } - curLine += L; - curCol = 0; + this._curLine += L; + this._curCol = 0; } // tests case foo in remove(), which isn't otherwise covered in current impl - }; + } /** * Skip some characters. Can contain newlines. @@ -793,20 +869,20 @@ const textLinesMutator = (lines) => { * @param {number} L - number of newlines to skip * @param {boolean} includeInSplice - indicates if attributes are present */ - const skip = (N, L, includeInSplice) => { + skip(N, L, includeInSplice) { if (!N) return; if (L) { - skipLines(L, includeInSplice); + this.skipLines(L, includeInSplice); } else { - if (includeInSplice && !inSplice) enterSplice(); - if (inSplice) { + if (includeInSplice && !this._inSplice) this._enterSplice(); + if (this._inSplice) { // although the line is put into splice curLine is not increased, because // only some chars are skipped, not the whole line - putCurLineInSplice(); + this._putCurLineInSplice(); } - curCol += N; + this._curCol += N; } - }; + } /** * Remove whole lines from lines array. @@ -814,9 +890,9 @@ const textLinesMutator = (lines) => { * @param {number} L - number of lines to remove * @returns {string} */ - const removeLines = (L) => { + removeLines(L) { if (!L) return ''; - if (!inSplice) enterSplice(); + if (!this._inSplice) this._enterSplice(); /** * Gets a string of joined lines after the end of the splice. @@ -825,32 +901,32 @@ const textLinesMutator = (lines) => { * @returns {string} joined lines */ const nextKLinesText = (k) => { - const m = curSplice[0] + curSplice[1]; - return linesSlice(m, m + k).join(''); + const m = this._curSplice[0] + this._curSplice[1]; + return this._linesSlice(m, m + k).join(''); }; let removed = ''; - if (isCurLineInSplice()) { - if (curCol === 0) { - removed = curSplice[curSplice.length - 1]; - curSplice.length--; + if (this._isCurLineInSplice()) { + if (this._curCol === 0) { + removed = this._curSplice[this._curSplice.length - 1]; + this._curSplice.length--; removed += nextKLinesText(L - 1); - curSplice[1] += L - 1; + this._curSplice[1] += L - 1; } else { removed = nextKLinesText(L - 1); - curSplice[1] += L - 1; - const sline = curSplice.length - 1; - removed = curSplice[sline].substring(curCol) + removed; - curSplice[sline] = curSplice[sline].substring(0, curCol) + - linesGet(curSplice[0] + curSplice[1]); - curSplice[1] += 1; + this._curSplice[1] += L - 1; + const sline = this._curSplice.length - 1; + removed = this._curSplice[sline].substring(this._curCol) + removed; + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + + this._linesGet(this._curSplice[0] + this._curSplice[1]); + this._curSplice[1] += 1; } } else { removed = nextKLinesText(L); - curSplice[1] += L; + this._curSplice[1] += L; } return removed; - }; + } /** * Remove text from lines array. @@ -859,18 +935,18 @@ const textLinesMutator = (lines) => { * @param {number} L - lines to delete * @returns {string} */ - const remove = (N, L) => { + remove(N, L) { if (!N) return ''; - if (L) return removeLines(L); - if (!inSplice) enterSplice(); + if (L) return this.removeLines(L); + if (!this._inSplice) this._enterSplice(); // although the line is put into splice, curLine is not increased, because // only some chars are removed not the whole line - const sline = putCurLineInSplice(); - const removed = curSplice[sline].substring(curCol, curCol + N); - curSplice[sline] = curSplice[sline].substring(0, curCol) + - curSplice[sline].substring(curCol + N); + const sline = this._putCurLineInSplice(); + const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N); + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + + this._curSplice[sline].substring(this._curCol + N); return removed; - }; + } /** * Inserts text into lines array. @@ -878,82 +954,68 @@ const textLinesMutator = (lines) => { * @param {string} text - the text to insert * @param {number} L - number of newlines in text */ - const insert = (text, L) => { + insert(text, L) { if (!text) return; - if (!inSplice) enterSplice(); + if (!this._inSplice) this._enterSplice(); if (L) { const newLines = exports.splitTextLines(text); - if (isCurLineInSplice()) { - const sline = curSplice.length - 1; + if (this._isCurLineInSplice()) { + const sline = this._curSplice.length - 1; /** @type {string} */ - const theLine = curSplice[sline]; - const lineCol = curCol; - // insert the first new line - curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - curLine++; + const theLine = this._curSplice[sline]; + const lineCol = this._curCol; + // Insert the chars up to `curCol` and the first new line. + this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + this._curLine++; newLines.splice(0, 1); // insert the remaining new lines - curSplice.push(...newLines); - curLine += newLines.length; + this._curSplice.push(...newLines); + this._curLine += newLines.length; // insert the remaining chars from the "old" line (e.g. the line we were in // when we started to insert new lines) - curSplice.push(theLine.substring(lineCol)); - curCol = 0; // TODO(doc) why is this not set to the length of last line? + this._curSplice.push(theLine.substring(lineCol)); + this._curCol = 0; // TODO(doc) why is this not set to the length of last line? } else { - curSplice.push(...newLines); - curLine += newLines.length; + this._curSplice.push(...newLines); + this._curLine += newLines.length; } } else { - // there are no additional lines - // although the line is put into splice, curLine is not increased, because - // there may be more chars in the line (newline is not reached) - const sline = putCurLineInSplice(); - if (!curSplice[sline]) { + // There are no additional lines. Although the line is put into splice, curLine is not + // increased because there may be more chars in the line (newline is not reached). + const sline = this._putCurLineInSplice(); + if (!this._curSplice[sline]) { const err = new Error( 'curSplice[sline] not populated, actual curSplice contents is ' + - `${JSON.stringify(curSplice)}. Possibly related to ` + + `${JSON.stringify(this._curSplice)}. Possibly related to ` + 'https://github.com/ether/etherpad-lite/issues/2802'); console.error(err.stack || err.toString()); } - curSplice[sline] = curSplice[sline].substring(0, curCol) + text + - curSplice[sline].substring(curCol); - curCol += text.length; + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text + + this._curSplice[sline].substring(this._curCol); + this._curCol += text.length; } - }; + } /** * Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`. * * @returns {boolean} indicates if there are lines left */ - const hasMore = () => { - let docLines = linesLength(); - if (inSplice) { - docLines += curSplice.length - 2 - curSplice[1]; + hasMore() { + let docLines = this._linesLength(); + if (this._inSplice) { + docLines += this._curSplice.length - 2 - this._curSplice[1]; } - return curLine < docLines; - }; + return this._curLine < docLines; + } /** * Closes the splice */ - const close = () => { - if (inSplice) { - leaveSplice(); - } - }; - - const self = { - skip, - remove, - insert, - close, - hasMore, - removeLines, - skipLines, - }; - return self; -}; + close() { + if (this._inSplice) this._leaveSplice(); + } +} /** * Apply operations to other operations. @@ -977,15 +1039,18 @@ const textLinesMutator = (lines) => { * @returns {string} the integrated changeset */ const applyZip = (in1, in2, func) => { - const iter1 = exports.opIterator(in1); - const iter2 = exports.opIterator(in2); + const ops1 = exports.deserializeOps(in1); + const ops2 = exports.deserializeOps(in2); + let next1 = ops1.next(); + let next2 = ops2.next(); const assem = exports.smartOpAssembler(); - const op1 = exports.newOp(); - const op2 = 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); - const opOut = func(op1, op2); + while (!next1.done || !next2.done) { + if (!next1.done && !next1.value.opcode) next1 = ops1.next(); + if (!next2.done && !next2.value.opcode) next2 = ops2.next(); + if (next1.value == null) next1.value = new Op(); + if (next2.value == null) next2.value = new Op(); + if (!next1.value.opcode && !next2.value.opcode) break; + const opOut = func(next1.value, next2.value); if (opOut && opOut.opcode) assem.append(opOut); } assem.endDocument(); @@ -1045,12 +1110,10 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); 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()) { - const op = csIter.next(); + for (const op of exports.deserializeOps(unpacked.ops)) { switch (op.opcode) { case '+': // op is + and op.lines 0: no newlines must be in op.chars @@ -1090,11 +1153,9 @@ exports.applyToText = (cs, str) => { */ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); - const mut = textLinesMutator(lines); - while (csIter.hasNext()) { - const op = csIter.next(); + const mut = new TextLinesMutator(lines); + for (const op of exports.deserializeOps(unpacked.ops)) { switch (op.opcode) { case '+': mut.insert(bankIter.take(op.chars), op.lines); @@ -1110,20 +1171,11 @@ exports.mutateTextLines = (cs, lines) => { mut.close(); }; -/** - * Sorts an array of attributes by key. - * - * @param {Attribute[]} attribs - The array of attributes to sort in place. - * @returns {Attribute[]} The `attribs` array. - */ -const sortAttribs = - (attribs) => attribs.sort((a, b) => (a[0] > b[0] ? 1 : 0) - (a[0] < b[0] ? 1 : 0)); - /** * Composes two attribute strings (see below) into one. * - * @param {string} att1 - first attribute string - * @param {string} att2 - second attribue string + * @param {AttributeString} att1 - first attribute string + * @param {AttributeString} att2 - second attribue string * @param {boolean} resultIsMutation - * @param {AttributePool} pool - attribute pool * @returns {string} @@ -1150,27 +1202,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { return att2; } if (!att2) return att1; - const atts = new Map(); - att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - const [key, val] = pool.getAttrib(exports.parseNum(a)); - atts.set(key, val); - return ''; - }); - att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - const [key, val] = pool.getAttrib(exports.parseNum(a)); - if (val || resultIsMutation) { - atts.set(key, val); - } else { - atts.delete(key); - } - return ''; - }); - const buf = exports.stringAssembler(); - for (const att of sortAttribs([...atts])) { - buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(att))); - } - return buf.toString(); + return AttributeMap.fromString(att1, pool).updateFromString(att2, !resultIsMutation).toString(); }; /** @@ -1183,7 +1215,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { * @returns {Op} The result of applying `csOp` to `attOp`. */ const slicerZipperFunc = (attOp, csOp, pool) => { - const opOut = exports.newOp(); + const opOut = new Op(); if (!attOp.opcode) { copyOp(csOp, opOut); csOp.opcode = ''; @@ -1248,29 +1280,63 @@ exports.applyToAttribution = (cs, astr, pool) => { return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool)); }; +/** + * Applies a changeset to an array of attribute lines. + * + * @param {string} cs - The encoded changeset. + * @param {Array} lines - Attribute lines. Modified in place. + * @param {AttributePool} pool - Attribute pool. + */ exports.mutateAttributionLines = (cs, lines, pool) => { const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); + const csOps = exports.deserializeOps(unpacked.ops); + let csOpsNext = csOps.next(); const csBank = unpacked.charBank; let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time - const mut = textLinesMutator(lines); + const mut = new TextLinesMutator(lines); - /** @type {?OpIter} */ - let lineIter = null; + /** + * The Ops in the current line from `lines`. + * + * @type {?Generator} + */ + let lineOps = null; + let lineOpsNext = null; - const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); + const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done; + /** + * Returns false if we are on the last attribute line in `lines` and there is no additional op in + * that line. + * + * @returns {boolean} True if there are more ops to go through. + */ + const isNextMutOp = () => lineOpsHasNext() || mut.hasMore(); + /** + * @returns {Op} The next Op from `lineIter`. If there are no more Ops, `lineIter` is reset to + * iterate over the next line, which is consumed from `mut`. If there are no more lines, + * returns a null Op. + */ const nextMutOp = () => { - if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + if (!lineOpsHasNext() && mut.hasMore()) { + // There are more attribute lines in `lines` to do AND either we just started so `lineIter` is + // still null or there are no more ops in current `lineIter`. const line = mut.removeLines(1); - lineIter = exports.opIterator(line); + lineOps = exports.deserializeOps(line); + lineOpsNext = lineOps.next(); } - if (!lineIter || !lineIter.hasNext()) return exports.newOp(); - return lineIter.next(); + if (!lineOpsHasNext()) return new Op(); // No more ops and no more lines. + const op = lineOpsNext.value; + lineOpsNext = lineOps.next(); + return op; }; let lineAssem = null; + /** + * Appends an op to `lineAssem`. In case `lineAssem` includes one single newline, adds it to the + * `lines` mutator. + */ const outputMutOp = (op) => { if (!lineAssem) { lineAssem = exports.mergingOpAssembler(); @@ -1283,26 +1349,33 @@ exports.mutateAttributionLines = (cs, lines, pool) => { lineAssem = null; }; - let csOp = exports.newOp(); - let attOp = exports.newOp(); - while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { - if (!csOp.opcode && csIter.hasNext()) csOp = csIter.next(); - if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { + let csOp = new Op(); + let attOp = new Op(); + while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) { + if (!csOp.opcode && !csOpsNext.done) { + // coOp done, but more ops in cs. + csOp = csOpsNext.value; + csOpsNext = csOps.next(); + } + if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) { break; // done - } else if (csOp.opcode === '=' && csOp.lines > 0 && (!csOp.attribs) && - (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { - // skip multiple lines; this is what makes small changes not order of the document size + } else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode && + !lineAssem && !lineOpsHasNext()) { + // Skip multiple lines without attributes; this is what makes small changes not order of the + // document size. mut.skipLines(csOp.lines); csOp.opcode = ''; } else if (csOp.opcode === '+') { const opOut = copyOp(csOp); if (csOp.lines > 1) { + // Copy the first line from `csOp` to `opOut`. const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; csOp.chars -= firstLineLen; csOp.lines--; opOut.lines = 1; opOut.chars = firstLineLen; } else { + // Either one or no newlines in '+' `csOp`, copy to `opOut` and reset `csOp`. csOp.opcode = ''; } outputMutOp(opOut); @@ -1327,16 +1400,12 @@ exports.mutateAttributionLines = (cs, lines, pool) => { exports.joinAttributionLines = (theAlines) => { const assem = exports.mergingOpAssembler(); for (const aline of theAlines) { - const iter = exports.opIterator(aline); - while (iter.hasNext()) { - assem.append(iter.next()); - } + for (const op of exports.deserializeOps(aline)) assem.append(op); } return assem.toString(); }; exports.splitAttributionLines = (attrOps, text) => { - const iter = exports.opIterator(attrOps); const assem = exports.mergingOpAssembler(); const lines = []; let pos = 0; @@ -1350,8 +1419,7 @@ exports.splitAttributionLines = (attrOps, text) => { pos += op.chars; }; - while (iter.hasNext()) { - const op = iter.next(); + for (const op of exports.deserializeOps(attrOps)) { let numChars = op.chars; let numLines = op.lines; while (numLines > 1) { @@ -1450,35 +1518,29 @@ exports.identity = (N) => exports.pack(N, N, '', ''); * spliceStart+numRemoved and inserts newText instead. Also gives possibility to add attributes * optNewTextAPairs for the new text. * - * @param {string} oldFullText - old text - * @param {number} spliceStart - where splicing starts - * @param {number} numRemoved - number of characters to remove - * @param {string} newText - string to insert - * @param {string} optNewTextAPairs - new pairs to insert - * @param {AttributePool} pool - Attribute pool + * @param {string} orig - Original text. + * @param {number} start - Index into `orig` where characters should be removed and inserted. + * @param {number} ndel - Number of characters to delete at `start`. + * @param {string} ins - Text to insert at `start` (after deleting `ndel` characters). + * @param {string} [attribs] - Optional attributes to apply to the inserted text. + * @param {AttributePool} [pool] - Attribute pool. * @returns {string} */ -exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) => { - const oldLen = oldFullText.length; - - if (spliceStart >= oldLen) { - spliceStart = oldLen - 1; - } - if (numRemoved > oldFullText.length - spliceStart) { - numRemoved = oldFullText.length - spliceStart; - } - const oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); - const newLen = oldLen + newText.length - oldText.length; - +exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => { + if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); + if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); + if (start > orig.length) start = orig.length; + if (ndel > orig.length - start) ndel = orig.length - start; + const deleted = orig.substring(start, start + ndel); const assem = exports.smartOpAssembler(); const ops = (function* () { - yield* opsFromText('=', oldFullText.substring(0, spliceStart)); - yield* opsFromText('-', oldText); - yield* opsFromText('+', newText, optNewTextAPairs, pool); + yield* opsFromText('=', orig.substring(0, start)); + yield* opsFromText('-', deleted); + yield* opsFromText('+', ins, attribs, pool); })(); for (const op of ops) assem.append(op); assem.endDocument(); - return exports.pack(oldLen, newLen, assem.toString(), newText); + return exports.pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins); }; /** @@ -1494,11 +1556,9 @@ const toSplices = (cs) => { const splices = []; let oldPos = 0; - const iter = exports.opIterator(unpacked.ops); const charIter = exports.stringIterator(unpacked.charBank); let inSplice = false; - while (iter.hasNext()) { - const op = iter.next(); + for (const op of exports.deserializeOps(unpacked.ops)) { if (op.opcode === '=') { oldPos += op.chars; inSplice = false; @@ -1612,16 +1672,21 @@ exports.makeAttribution = (text) => { * Iterates over attributes in exports, attribution string, or attribs property of an op and runs * function func on them. * + * @deprecated Use `attributes.decodeAttribString()` instead. * @param {string} cs - changeset * @param {Function} func - function to call */ exports.eachAttribNumber = (cs, func) => { + padutils.warnDeprecated( + 'Changeset.eachAttribNumber() is deprecated; use attributes.decodeAttribString() instead'); let dollarPos = cs.indexOf('$'); if (dollarPos < 0) { dollarPos = cs.length; } const upToDollar = cs.substring(0, dollarPos); + // WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()` + // because that function only works on attribute strings, not serialized operations or changesets. upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { func(exports.parseNum(a)); return ''; @@ -1728,18 +1793,18 @@ exports.copyAText = (atext1, atext2) => { }; /** - * Append the set of operations from atext to an assembler. + * Convert AText to a series of operations. Strips final newline. * - * @param {AText} atext - - * @param assem - Assembler like SmartOpAssembler TODO add desc + * @param {AText} atext - The AText to convert. + * @yields {Op} + * @returns {Generator} */ -exports.appendATextToAssembler = (atext, assem) => { +exports.opsFromAText = function* (atext) { // intentionally skips last newline char of atext - const iter = exports.opIterator(atext.attribs); let lastOp = null; - while (iter.hasNext()) { - if (lastOp != null) assem.append(lastOp); - lastOp = iter.next(); + for (const op of exports.deserializeOps(atext.attribs)) { + if (lastOp != null) yield lastOp; + lastOp = op; } if (lastOp == null) return; // exclude final newline @@ -1751,11 +1816,24 @@ exports.appendATextToAssembler = (atext, assem) => { const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; lastOp.lines--; lastOp.chars -= (lastLineLength + 1); - assem.append(lastOp); + yield copyOp(lastOp); lastOp.lines = 0; lastOp.chars = lastLineLength; } - if (lastOp.chars) assem.append(lastOp); + if (lastOp.chars) yield lastOp; +}; + +/** + * Append the set of operations from atext to an assembler. + * + * @deprecated Use `opsFromAText` instead. + * @param {AText} atext - + * @param assem - Assembler like SmartOpAssembler TODO add desc + */ +exports.appendATextToAssembler = (atext, assem) => { + padutils.warnDeprecated( + 'Changeset.appendATextToAssembler() is deprecated; use Changeset.opsFromAText() instead'); + for (const op of exports.opsFromAText(atext)) assem.append(op); }; /** @@ -1785,33 +1863,45 @@ exports.isIdentity = (cs) => { return unpacked.ops === '' && unpacked.oldLen === unpacked.newLen; }; +/** + * @deprecated Use an AttributeMap instead. + */ +const attribsAttributeValue = (attribs, key, pool) => { + if (!attribs) return ''; + for (const [k, v] of attributes.attribsFromString(attribs, pool)) { + if (k === key) return v; + } + return ''; +}; + /** * Returns all the values of attributes with a certain key in an Op attribs string. * + * @deprecated Use an AttributeMap instead. * @param {Op} op - Op * @param {string} key - string to search for * @param {AttributePool} pool - attribute pool * @returns {string} */ -exports.opAttributeValue = (op, key, pool) => exports.attribsAttributeValue(op.attribs, key, pool); +exports.opAttributeValue = (op, key, pool) => { + padutils.warnDeprecated( + 'Changeset.opAttributeValue() is deprecated; use an AttributeMap instead'); + return attribsAttributeValue(op.attribs, key, pool); +}; /** * Returns all the values of attributes with a certain key in an attribs string. * - * @param {string} attribs - Attribute string + * @deprecated Use an AttributeMap instead. + * @param {AttributeString} attribs - Attribute string * @param {string} key - string to search for * @param {AttributePool} pool - attribute pool * @returns {string} */ exports.attribsAttributeValue = (attribs, key, pool) => { - if (!attribs) return ''; - let value = ''; - exports.eachAttribNumber(attribs, (n) => { - if (pool.getAttribKey(n) === key) { - value = pool.getAttribValue(n); - } - }); - return value; + padutils.warnDeprecated( + 'Changeset.attribsAttributeValue() is deprecated; use an AttributeMap instead'); + return attribsAttributeValue(attribs, key, pool); }; /** @@ -1831,7 +1921,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => { */ exports.builder = (oldLen) => { const assem = exports.smartOpAssembler(); - const o = exports.newOp(); + const o = new Op(); const charBank = exports.stringAssembler(); const self = { @@ -1847,7 +1937,8 @@ exports.builder = (oldLen) => { */ keep: (N, L, attribs, pool) => { o.opcode = '='; - o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; + o.attribs = typeof attribs === 'string' + ? attribs : new AttributeMap(pool).update(attribs || []).toString(); o.chars = N; o.lines = (L || 0); assem.append(o); @@ -1906,40 +1997,45 @@ exports.builder = (oldLen) => { return self; }; +/** + * Constructs an attribute string from a sequence of attributes. + * + * @deprecated Use `AttributeMap.prototype.toString()` or `attributes.attribsToString()` instead. + * @param {string} opcode - The opcode for the Op that will get the resulting attribute string. + * @param {?(Iterable|AttributeString)} attribs - The attributes to insert into the pool + * (if necessary) and encode. If an attribute string, no checking is performed to ensure that + * the attributes exist in the pool, are in the canonical order, and contain no duplicate keys. + * If this is an iterable of attributes, `pool` must be non-null. + * @param {AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of attributes, + * ignored if `attribs` is an attribute string. + * @returns {AttributeString} + */ exports.makeAttribsString = (opcode, attribs, pool) => { - // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work - if (!attribs) { - return ''; - } else if ((typeof attribs) === 'string') { - return attribs; - } else if (pool && attribs.length) { - if (attribs.length > 1) { - attribs = attribs.slice(); - sortAttribs(attribs); - } - const result = []; - for (const pair of attribs) { - if (opcode === '=' || (opcode === '+' && pair[1])) { - result.push(`*${exports.numToString(pool.putAttrib(pair))}`); - } - } - return result.join(''); - } + padutils.warnDeprecated( + 'Changeset.makeAttribsString() is deprecated; ' + + 'use AttributeMap.prototype.toString() or attributes.attribsToString() instead'); + if (!attribs || !['=', '+'].includes(opcode)) return ''; + if (typeof attribs === 'string') return attribs; + return new AttributeMap(pool).update(attribs, opcode === '+').toString(); }; /** * Like "substring" but on a single-line attribution string. */ exports.subattribution = (astr, start, optEnd) => { - const iter = exports.opIterator(astr); + const attOps = exports.deserializeOps(astr); + let attOpsNext = attOps.next(); const assem = exports.smartOpAssembler(); - let attOp = exports.newOp(); - const csOp = exports.newOp(); + let attOp = new Op(); + const csOp = new Op(); const doCsOp = () => { if (!csOp.chars) return; - while (csOp.opcode && (attOp.opcode || iter.hasNext())) { - if (!attOp.opcode) attOp = iter.next(); + while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) { + if (!attOp.opcode) { + attOp = attOpsNext.value; + attOpsNext = attOps.next(); + } if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) { csOp.lines++; @@ -1958,7 +2054,10 @@ exports.subattribution = (astr, start, optEnd) => { if (attOp.opcode) { assem.append(attOp); } - while (iter.hasNext()) assem.append(iter.next()); + while (!attOpsNext.done) { + assem.append(attOpsNext.value); + attOpsNext = attOps.next(); + } } else { csOp.opcode = '='; csOp.chars = optEnd - start; @@ -1995,22 +2094,23 @@ exports.inverse = (cs, lines, alines, pool) => { let curLine = 0; let curChar = 0; - let curLineOpIter = null; - let curLineOpIterLine; - let curLineNextOp = exports.newOp('+'); + let curLineOps = null; + let curLineOpsNext = null; + let curLineOpsLine; + let curLineNextOp = new Op('+'); const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); const builder = exports.builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { - if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { - // create curLineOpIter and advance it to curChar - curLineOpIter = exports.opIterator(alinesGet(curLine)); - curLineOpIterLine = curLine; + if (!curLineOps || curLineOpsLine !== curLine) { + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + curLineOpsLine = curLine; let indexIntoLine = 0; - while (curLineOpIter.hasNext()) { - curLineNextOp = curLineOpIter.next(); + while (!curLineOpsNext.done) { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); break; @@ -2020,15 +2120,21 @@ exports.inverse = (cs, lines, alines, pool) => { } while (numChars > 0) { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; - curLineOpIterLine = curLine; + curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = exports.opIterator(alinesGet(curLine)); + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { - curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : exports.newOp(); + if (curLineOpsNext.done) { + curLineNextOp = new Op(); + } else { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + } } const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && @@ -2038,7 +2144,7 @@ exports.inverse = (cs, lines, alines, pool) => { curChar += charsToUse; } - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; } @@ -2048,7 +2154,7 @@ exports.inverse = (cs, lines, alines, pool) => { if (L) { curLine += L; curChar = 0; - } else if (curLineOpIter && curLineOpIterLine === curLine) { + } else if (curLineOps && curLineOpsLine === curLine) { consumeAttribRuns(N, () => {}); } else { curChar += N; @@ -2083,21 +2189,20 @@ exports.inverse = (cs, lines, alines, pool) => { }; }; - while (csIter.hasNext()) { - const csOp = csIter.next(); + for (const csOp of exports.deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { if (csOp.attribs) { - const csAttribs = []; - exports.eachAttribNumber(csOp.attribs, (n) => csAttribs.push(pool.getAttrib(n))); - const undoBackToAttribs = cachedStrFunc((attribs) => { - const backAttribs = []; - for (const [appliedKey, appliedValue] of csAttribs) { - const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); - if (appliedValue !== oldValue) { - backAttribs.push([appliedKey, oldValue]); - } + const attribs = AttributeMap.fromString(csOp.attribs, pool); + const undoBackToAttribs = cachedStrFunc((oldAttribsStr) => { + const oldAttribs = AttributeMap.fromString(oldAttribsStr, pool); + const backAttribs = new AttributeMap(pool); + for (const [key, value] of attribs) { + const oldValue = oldAttribs.get(key) || ''; + if (oldValue !== value) backAttribs.set(key, oldValue); } - return exports.makeAttribsString('=', backAttribs, pool); + // TODO: backAttribs does not restore removed attributes (it is missing attributes that + // are in oldAttribs but not in attribs). I don't know if that is intentional. + return backAttribs.toString(); }); consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); @@ -2138,7 +2243,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { - const opOut = exports.newOp(); + const opOut = new Op(); if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { @@ -2293,7 +2398,7 @@ const followAttributes = (att1, att2, pool) => { }; exports.exportedForTestingOnly = { + TextLinesMutator, followAttributes, - textLinesMutator, toSplices, }; diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.js index b4658575e..a627f88f9 100644 --- a/src/static/js/ChatMessage.js +++ b/src/static/js/ChatMessage.js @@ -1,5 +1,7 @@ 'use strict'; +const {padutils: {warnDeprecated}} = require('./pad_utils'); + /** * Represents a chat message stored in the database and transmitted among users. Plugins can extend * the object with additional properties. @@ -8,6 +10,13 @@ */ class ChatMessage { static fromObject(obj) { + // The userId property was renamed to authorId, and userName was renamed to displayName. Accept + // the old names in case the db record was written by an older version of Etherpad. + obj = Object.assign({}, obj); // Don't mutate the caller's object. + if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId; + delete obj.userId; + if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName; + delete obj.userName; return Object.assign(new ChatMessage(), obj); } @@ -52,8 +61,14 @@ class ChatMessage { * @deprecated Use `authorId` instead. * @type {string} */ - get userId() { return this.authorId; } - set userId(val) { this.authorId = val; } + get userId() { + warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); + return this.authorId; + } + set userId(val) { + warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); + this.authorId = val; + } /** * Alias of `displayName`, for compatibility with old plugins. @@ -61,19 +76,22 @@ class ChatMessage { * @deprecated Use `displayName` instead. * @type {string} */ - get userName() { return this.displayName; } - set userName(val) { this.displayName = val; } + get userName() { + warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); + return this.displayName; + } + set userName(val) { + warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); + this.displayName = val; + } // TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that // doesn't support authorId and displayName. toJSON() { - return { - ...this, - authorId: undefined, - displayName: undefined, - userId: this.authorId, - userName: this.displayName, - }; + const {authorId, displayName, ...obj} = this; + obj.userId = authorId; + obj.userName = displayName; + return obj; } } diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 675427019..785473df7 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -18,6 +18,7 @@ */ let documentAttributeManager; +const AttributeMap = require('./AttributeMap'); const browser = require('./vendors/browser'); const padutils = require('./pad_utils').padutils; const Ace2Common = require('./ace2_common'); @@ -522,14 +523,14 @@ function Ace2Inner(editorInfo, cssManagers) { const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; const assem = Changeset.smartOpAssembler(); - const o = Changeset.newOp('-'); + const o = new Changeset.Op('-'); o.chars = upToLastLine; o.lines = numLines - 1; assem.append(o); o.chars = lastLineLength; o.lines = 0; assem.append(o); - Changeset.appendATextToAssembler(atext, assem); + for (const op of Changeset.opsFromAText(atext)) assem.append(op); const newLen = oldLen + assem.getLengthChange(); const changeset = Changeset.checkRep( Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); @@ -1542,9 +1543,7 @@ function Ace2Inner(editorInfo, cssManagers) { } } - const withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'], - ], rep.apool); + const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); const hasIt = (attribs) => withItRegex.test(attribs); @@ -1577,13 +1576,8 @@ function Ace2Inner(editorInfo, cssManagers) { const end = selEnd[1]; let hasAttrib = true; - // Iterate over attribs on this line - - const opIter = Changeset.opIterator(rep.alines[lineNum]); let indexIntoLine = 0; - - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -1608,9 +1602,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (!(rep.selStart && rep.selEnd)) return; let selectionAllHasIt = true; - const withIt = Changeset.makeAttribsString('+', [ - [attributeName, 'true'], - ], rep.apool); + const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); const hasIt = (attribs) => withItRegex.test(attribs); @@ -1618,7 +1610,6 @@ function Ace2Inner(editorInfo, cssManagers) { 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)) { @@ -1631,8 +1622,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (n === selEndLine) { selectionEndInLine = rep.selEnd[1]; } - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[n])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -1757,12 +1747,10 @@ function Ace2Inner(editorInfo, cssManagers) { }; const eachAttribRun = (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(); + for (const op of Changeset.deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); @@ -1820,22 +1808,15 @@ function Ace2Inner(editorInfo, cssManagers) { } let isNewTextMultiauthor = false; - const authorAtt = Changeset.makeAttribsString('+', (thisAuthor ? [ - ['author', thisAuthor], - ] : []), rep.apool); const authorizer = cachedStrFunc((oldAtts) => { - if (isNewTextMultiauthor) { - // prefer colors from DOM - return Changeset.composeAttributes(authorAtt, oldAtts, true, rep.apool); - } else { - // use this author's color - return Changeset.composeAttributes(oldAtts, authorAtt, true, rep.apool); - } + const attribs = AttributeMap.fromString(oldAtts, rep.apool); + if (!isNewTextMultiauthor || !attribs.has('author')) attribs.set('author', thisAuthor); + return attribs.toString(); }); let foundDomAuthor = ''; eachAttribRun(newAttribs, (start, end, attribs) => { - const a = Changeset.attribsAttributeValue(attribs, 'author', rep.apool); + const a = AttributeMap.fromString(attribs, rep.apool).get('author'); if (a && a !== foundDomAuthor) { if (!foundDomAuthor) { foundDomAuthor = a; @@ -1874,7 +1855,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const analyzeChange = ( - oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) => { + 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 @@ -1883,9 +1864,7 @@ function Ace2Inner(editorInfo, cssManagers) { const attribRuns = (attribs) => { const lengs = []; const atts = []; - const iter = Changeset.opIterator(attribs); - while (iter.hasNext()) { - const op = iter.next(); + for (const op of Changeset.deserializeOps(attribs)) { lengs.push(op.chars); atts.push(op.attribs); } @@ -2629,11 +2608,9 @@ function Ace2Inner(editorInfo, cssManagers) { // TODO: There appears to be a race condition or so. const authorIds = new Set(); if (alineAttrs) { - const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()) { - const op = opIter.next(); - const authorId = Changeset.opAttributeValue(op, 'author', apool); - if (authorId !== '') authorIds.add(authorId); + for (const op of Changeset.deserializeOps(alineAttrs)) { + const authorId = AttributeMap.fromString(op.attribs, apool).get('author'); + if (authorId) authorIds.add(authorId); } } const idToName = new Map(parent.parent.pad.userList().map((a) => [a.userId, a.name])); diff --git a/src/static/js/admin/settings.js b/src/static/js/admin/settings.js index ada694f81..a53937471 100644 --- a/src/static/js/admin/settings.js +++ b/src/static/js/admin/settings.js @@ -28,7 +28,7 @@ $(document).ready(() => { $('.settings').focus(); $('.settings').autosize(); } else { - alert('YOUR JSON IS BAD AND YOU SHOULD FEEL BAD'); + alert('Invalid JSON'); } }); @@ -39,7 +39,7 @@ $(document).ready(() => { // JSON is clean so emit it to the server socket.emit('saveSettings', $('.settings').val()); } else { - alert('YOUR JSON IS BAD AND YOU SHOULD FEEL BAD'); + alert('Invalid JSON'); $('.settings').focus(); } }); diff --git a/src/static/js/attributes.js b/src/static/js/attributes.js new file mode 100644 index 000000000..4ab347019 --- /dev/null +++ b/src/static/js/attributes.js @@ -0,0 +1,130 @@ +'use strict'; + +// Low-level utilities for manipulating attribute strings. For a high-level API, see AttributeMap. + +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ + +/** + * A concatenated sequence of zero or more attribute identifiers, each one represented by an + * asterisk followed by a base-36 encoded attribute number. + * + * Examples: '', '*0', '*3*j*z*1q' + * + * @typedef {string} AttributeString + */ + +/** + * Converts an attribute string into a sequence of attribute identifier numbers. + * + * WARNING: This only works on attribute strings. It does NOT work on serialized operations or + * changesets. + * + * @param {AttributeString} str - Attribute string. + * @yields {number} The attribute numbers (to look up in the associated pool), in the order they + * appear in `str`. + * @returns {Generator} + */ +exports.decodeAttribString = function* (str) { + const re = /\*([0-9a-z]+)|./gy; + let match; + while ((match = re.exec(str)) != null) { + const [m, n] = match; + if (n == null) throw new Error(`invalid character in attribute string: ${m}`); + yield Number.parseInt(n, 36); + } +}; + +const checkAttribNum = (n) => { + if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`); + if (n < 0) throw new Error(`attribute number is negative: ${n}`); + if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`); +}; + +/** + * Inverse of `decodeAttribString`. + * + * @param {Iterable} attribNums - Sequence of attribute numbers. + * @returns {AttributeString} + */ +exports.encodeAttribString = (attribNums) => { + let str = ''; + for (const n of attribNums) { + checkAttribNum(n); + str += `*${n.toString(36).toLowerCase()}`; + } + return str; +}; + +/** + * Converts a sequence of attribute numbers into a sequence of attributes. + * + * @param {Iterable} attribNums - Attribute numbers to look up in the pool. + * @param {AttributePool} pool - Attribute pool. + * @yields {Attribute} The identified attributes, in the same order as `attribNums`. + * @returns {Generator} + */ +exports.attribsFromNums = function* (attribNums, pool) { + for (const n of attribNums) { + checkAttribNum(n); + const attrib = pool.getAttrib(n); + if (attrib == null) throw new Error(`attribute ${n} does not exist in pool`); + yield attrib; + } +}; + +/** + * Inverse of `attribsFromNums`. + * + * @param {Iterable} attribs - Attributes. Any attributes not already in `pool` are + * inserted into `pool`. No checking is performed to ensure that the attributes are in the + * canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if + * required.) + * @param {AttributePool} pool - Attribute pool. + * @yields {number} The attribute number of each attribute in `attribs`, in order. + * @returns {Generator} + */ +exports.attribsToNums = function* (attribs, pool) { + for (const attrib of attribs) yield pool.putAttrib(attrib); +}; + +/** + * Convenience function that is equivalent to `attribsFromNums(decodeAttribString(str), pool)`. + * + * WARNING: This only works on attribute strings. It does NOT work on serialized operations or + * changesets. + * + * @param {AttributeString} str - Attribute string. + * @param {AttributePool} pool - Attribute pool. + * @yields {Attribute} The attributes identified in `str`, in order. + * @returns {Generator} + */ +exports.attribsFromString = function* (str, pool) { + yield* exports.attribsFromNums(exports.decodeAttribString(str), pool); +}; + +/** + * Inverse of `attribsFromString`. + * + * @param {Iterable} attribs - Attributes. The attributes to insert into the pool (if + * necessary) and encode. No checking is performed to ensure that the attributes are in the + * canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if + * required.) + * @param {AttributePool} pool - Attribute pool. + * @returns {AttributeString} + */ +exports.attribsToString = + (attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool)); + +/** + * Sorts the attributes in canonical order. The order of entries with the same attribute name is + * unspecified. + * + * @param {Attribute[]} attribs - Attributes to sort in place. + * @returns {Attribute[]} `attribs` (for chaining). + */ +exports.sort = + (attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0)); diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 6ba2ef0ab..cd2211ae1 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -26,6 +26,7 @@ const makeCSSManager = require('./cssmanager').makeCSSManager; const domline = require('./domline').domline; const AttribPool = require('./AttributePool'); const Changeset = require('./Changeset'); +const attributes = require('./attributes'); const linestylefilter = require('./linestylefilter').linestylefilter; const colorutils = require('./colorutils').colorutils; const _ = require('./underscore'); @@ -114,24 +115,16 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro }, getActiveAuthors() { - const authors = []; - const seenNums = {}; - const alines = this.alines; - for (let i = 0; i < alines.length; i++) { - Changeset.eachAttribNumber(alines[i], (n) => { - if (!seenNums[n]) { - seenNums[n] = true; - if (this.apool.getAttribKey(n) === 'author') { - const a = this.apool.getAttribValue(n); - if (a) { - authors.push(a); - } - } + const authorIds = new Set(); + for (const aline of this.alines) { + for (const op of Changeset.deserializeOps(aline)) { + for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) { + if (k !== 'author') continue; + if (v) authorIds.add(v); } - }); + } } - authors.sort(); - return authors; + return [...authorIds].sort(); }, }; @@ -167,13 +160,8 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // some chars are replaced (no attributes change and no length change) // test if there are keep ops at the start of the cs if (lineChanged === undefined) { - lineChanged = 0; - const opIter = Changeset.opIterator(Changeset.unpack(changeset).ops); - - if (opIter.hasNext()) { - const op = opIter.next(); - if (op.opcode === '=') lineChanged += op.lines; - } + const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops); + lineChanged = op != null && op.opcode === '=' ? op.lines : 0; } const goToLineNumber = (lineNumber) => { diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index 94bc5071b..30c70aa74 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -22,6 +22,7 @@ * limitations under the License. */ +const AttributeMap = require('./AttributeMap'); const AttributePool = require('./AttributePool'); const Changeset = require('./Changeset'); @@ -139,46 +140,26 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { // Get my authorID const authorId = parent.parent.pad.myUserInfo.userId; - // Sanitize authorship - // We need to replace all author attribs with thisSession.author, - // in case they copy/pasted or otherwise inserted other peoples changes - if (apool.numToAttrib) { - let authorAttr; - for (const attr in apool.numToAttrib) { - if (apool.numToAttrib[attr][0] === 'author' && - apool.numToAttrib[attr][1] === authorId) { - authorAttr = Number(attr).toString(36); + // Sanitize authorship: Replace all author attributes with this user's author ID in case the + // text was copied from another author. + const cs = Changeset.unpack(userChangeset); + const assem = Changeset.mergingOpAssembler(); + + for (const op of Changeset.deserializeOps(cs.ops)) { + if (op.opcode === '+') { + const attribs = AttributeMap.fromString(op.attribs, apool); + const oldAuthorId = attribs.get('author'); + if (oldAuthorId != null && oldAuthorId !== authorId) { + attribs.set('author', authorId); + op.attribs = attribs.toString(); } } - - // Replace all added 'author' attribs with the value of the current user - const cs = Changeset.unpack(userChangeset); - const iterator = Changeset.opIterator(cs.ops); - let op; - const assem = Changeset.mergingOpAssembler(); - - while (iterator.hasNext()) { - op = iterator.next(); - if (op.opcode === '+') { - let newAttrs = ''; - - op.attribs.split('*').forEach((attrNum) => { - if (!attrNum) return; - const attr = apool.getAttrib(parseInt(attrNum, 36)); - if (!attr) return; - if ('author' === attr[0]) { - // replace that author with the current one - newAttrs += `*${authorAttr}`; - } else { newAttrs += `*${attrNum}`; } // overtake all other attribs as is - }); - op.attribs = newAttrs; - } - assem.append(op); - } - assem.endDocument(); - userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank); - Changeset.checkRep(userChangeset); + assem.append(op); } + assem.endDocument(); + userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank); + Changeset.checkRep(userChangeset); + if (Changeset.isIdentity(userChangeset)) toSubmit = null; else toSubmit = userChangeset; } diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 849fff5fc..74bc66f9f 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -207,8 +207,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }); } else if (msg.type === 'ACCEPT_COMMIT') { serverMessageTaskQueue.enqueue(() => { - const newRev = msg.newRev; - if (newRev !== (rev + 1)) { + const {newRev} = msg; + // newRev will equal rev if the changeset has no net effect (identity changeset, removing + // and re-adding the same characters with the same attributes, or retransmission of an + // already applied changeset). + if (![rev, rev + 1].includes(newRev)) { window.console.warn(`bad message revision on ACCEPT_COMMIT: ${newRev} not ${rev + 1}`); // setChannelState("DISCONNECTED", "badmessage_acceptcommit"); return; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 54c628804..7dd70e512 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -26,6 +26,7 @@ const _MAX_LIST_LEVEL = 16; +const AttributeMap = require('./AttributeMap'); const UNorm = require('unorm'); const Changeset = require('./Changeset'); const hooks = require('./pluginfw/hooks'); @@ -82,7 +83,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) const textArray = []; const attribsArray = []; let attribsBuilder = null; - const op = Changeset.newOp('+'); + const op = new Changeset.Op('+'); const self = { length: () => textArray.length, atColumnZero: () => textArray[textArray.length - 1] === '', @@ -227,13 +228,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) }; const _recalcAttribString = (state) => { - const lst = []; + const attribs = new AttributeMap(apool); for (const [a, count] of Object.entries(state.attribs)) { if (!count) continue; // The following splitting of the attribute name is a workaround // to enable the content collector to store key-value attributes // see https://github.com/ether/etherpad-lite/issues/2567 for more information // in long term the contentcollector should be refactored to get rid of this workaround + // + // TODO: This approach doesn't support changing existing values: if both 'foo::bar' and + // 'foo::baz' are in state.attribs then the last one encountered while iterating will win. const ATTRIBUTE_SPLIT_STRING = '::'; // see if attributeString is splittable @@ -241,32 +245,34 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) if (attributeSplits.length > 1) { // the attribute name follows the convention key::value // so save it as a key value attribute - lst.push([attributeSplits[0], attributeSplits[1]]); + const [k, v] = attributeSplits; + if (v) attribs.set(k, v); } else { // the "normal" case, the attribute is just a switch // so set it true - lst.push([a, 'true']); + attribs.set(a, 'true'); } } if (state.authorLevel > 0) { - const authorAttrib = ['author', state.author]; - if (apool.putAttrib(authorAttrib, true) >= 0) { + if (apool.putAttrib(['author', state.author], true) >= 0) { // require that author already be in pool // (don't add authors from other documents, etc.) - lst.push(authorAttrib); + if (state.author) attribs.set('author', state.author); } } - state.attribString = Changeset.makeAttribsString('+', lst, apool); + state.attribString = attribs.toString(); }; const _produceLineAttributesMarker = (state) => { // TODO: This has to go to AttributeManager. - const attributes = [ - ['lmkr', '1'], - ['insertorder', 'first'], - ...Object.entries(state.lineAttributes), - ]; - lines.appendText('*', Changeset.makeAttribsString('+', attributes, apool)); + const attribs = new AttributeMap(apool) + .set('lmkr', '1') + .set('insertorder', 'first') + // TODO: Converting all falsy values in state.lineAttributes into removals is awkward. + // Better would be to never add 0, false, null, or undefined to state.lineAttributes in the + // first place (I'm looking at you, state.lineAttributes.start). + .update(Object.entries(state.lineAttributes).map(([k, v]) => [k, v || '']), true); + lines.appendText('*', attribs.toString()); }; cc.startNewLine = (state) => { if (state) { diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 84668ea46..632e6b3cc 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -31,6 +31,7 @@ // requires: undefined const Changeset = require('./Changeset'); +const attributes = require('./attributes'); const hooks = require('./pluginfw/hooks'); const linestylefilter = {}; const AttributeManager = require('./AttributeManager'); @@ -73,42 +74,37 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool let classes = ''; let isLineAttribMarker = false; - // For each attribute number - Changeset.eachAttribNumber(attribs, (n) => { - // Give us this attributes key - const key = apool.getAttribKey(n); - if (key) { - const value = apool.getAttribValue(n); - if (value) { - if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) { - isLineAttribMarker = true; - } - if (key === 'author') { - classes += ` ${linestylefilter.getAuthorClassName(value)}`; - } else if (key === 'list') { - classes += ` list:${value}`; - } else if (key === 'start') { - // Needed to introduce the correct Ordered list item start number on import - classes += ` start:${value}`; - } else if (linestylefilter.ATTRIB_CLASSES[key]) { - classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; - } else { - const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value}); - classes += ` ${results.join(' ')}`; - } - } + for (const [key, value] of attributes.attribsFromString(attribs, apool)) { + if (!key || !value) continue; + if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) { + isLineAttribMarker = true; } - }); + if (key === 'author') { + classes += ` ${linestylefilter.getAuthorClassName(value)}`; + } else if (key === 'list') { + classes += ` list:${value}`; + } else if (key === 'start') { + // Needed to introduce the correct Ordered list item start number on import + classes += ` start:${value}`; + } else if (linestylefilter.ATTRIB_CLASSES[key]) { + classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; + } else { + const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value}); + classes += ` ${results.join(' ')}`; + } + } if (isLineAttribMarker) classes += ` ${lineAttributeMarker}`; return classes.substring(1); }; - const attributionIter = Changeset.opIterator(aline); + const attrOps = Changeset.deserializeOps(aline); + let attrOpsNext = attrOps.next(); let nextOp, nextOpClasses; const goNextOp = () => { - nextOp = attributionIter.hasNext() ? attributionIter.next() : Changeset.newOp(); + nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value; + if (!attrOpsNext.done) attrOpsNext = attrOps.next(); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); }; goNextOp(); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 306c2b191..c37920ead 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -95,24 +95,20 @@ const getParameters = [ settings.useMonospaceFontGlobal = true; }, }, - // If the username is set as a parameter we should set a global value that we can call once we - // have initiated the pad. { name: 'userName', checkVal: null, callback: (val) => { - settings.globalUserName = decodeURIComponent(val); - clientVars.userName = decodeURIComponent(val); + settings.globalUserName = val; + clientVars.userName = val; }, }, - // If the userColor is set as a parameter, set a global value to use once we have initiated the - // pad. { name: 'userColor', checkVal: null, callback: (val) => { - settings.globalUserColor = decodeURIComponent(val); - clientVars.userColor = decodeURIComponent(val); + settings.globalUserColor = val; + clientVars.userColor = val; }, }, { @@ -149,8 +145,10 @@ const getParameters = [ const getParams = () => { // Tries server enforced options first.. for (const setting of getParameters) { - const value = clientVars.padOptions[setting.name]; - if (value.toString() === setting.checkVal) { + let value = clientVars.padOptions[setting.name]; + if (value == null) continue; + value = value.toString(); + if (value === setting.checkVal || setting.checkVal == null) { setting.callback(value); } } @@ -179,8 +177,8 @@ const sendClientReady = (isReconnect) => { } let token = Cookies.get('token'); - if (token == null) { - token = `t.${randomString()}`; + if (token == null || !padutils.isValidAuthorToken(token)) { + token = padutils.generateAuthorToken(); Cookies.set('token', token, {expires: 60}); } @@ -295,6 +293,11 @@ const handshake = async () => { } else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { receivedClientVars = true; window.clientVars = obj.data; + if (window.clientVars.sessionRefreshInterval) { + const ping = + () => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {}); + setInterval(ping, window.clientVars.sessionRefreshInterval); + } } else if (obj.disconnect) { padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 9bea959da..e10841f50 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -88,24 +88,44 @@ const urlRegex = (() => { `(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g'); })(); +// https://stackoverflow.com/a/68957976 +const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/; + const padutils = { /** * Prints a warning message followed by a stack trace (to make it easier to figure out what code * is using the deprecated function). * + * Identical deprecation warnings (as determined by the stack trace, if available) are rate + * limited to avoid log spam. + * * Most browsers include UI widget to examine the stack at the time of the warning, but this * includes the stack in the log message for a couple of reasons: * - This makes it possible to see the stack if the code runs in Node.js. * - Users are more likely to paste the stack in bug reports they might file. * - * @param {...*} args - Passed to `console.warn`, with a stack trace appended. + * @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no + * logger is set), with a stack trace appended if available. */ - warnWithStack: (...args) => { + warnDeprecated: (...args) => { + if (padutils.warnDeprecated.disabledForTestingOnly) return; const err = new Error(); - if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnWithStack); + if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated); err.name = ''; + // Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam. + if (typeof err.stack === 'string') { + if (padutils.warnDeprecated._rl == null) { + padutils.warnDeprecated._rl = + {prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000}; + } + const rl = padutils.warnDeprecated._rl; + const now = rl.now(); + const prev = rl.prevs.get(err.stack); + if (prev != null && now - prev < rl.period) return; + rl.prevs.set(err.stack, now); + } if (err.stack) args.push(err.stack); - console.warn(...args); + (padutils.warnDeprecated.logger || console).warn(...args); }, escapeHtml: (x) => Security.escapeHTML(String(x)), @@ -310,6 +330,27 @@ const padutils = { return cc; } }), + + /** + * Returns whether a string has the expected format to be used as a secret token identifying an + * author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648 + * section 5 with padding). + * + * Being strict about what constitutes a valid token enables unambiguous extensibility (e.g., + * conditional transformation of a token to a database key in a way that does not allow a + * malicious user to impersonate another user). + */ + isValidAuthorToken: (t) => { + if (typeof t !== 'string' || !t.startsWith('t.')) return false; + const v = t.slice(2); + return v.length > 0 && base64url.test(v); + }, + + /** + * Returns a string that can be used in the `token` cookie as a secret that authenticates a + * particular author. + */ + generateAuthorToken: () => `t.${randomString()}`, }; let globalExceptionHandler = null; diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index a4a2df40d..ec3cfaa92 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -98,7 +98,10 @@ exports.update = async () => { defs.parts = sortParts(parts); defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); defs.loaded = true; - await Promise.all(Object.keys(defs.plugins).map((p) => hooks.aCallAll(`init_${p}`, {}))); + await Promise.all(Object.keys(defs.plugins).map(async (p) => { + const logger = log4js.getLogger(`plugin:${p}`); + await hooks.aCallAll(`init_${p}`, {logger}); + })); }; exports.getPackages = async () => { diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 7268f95f0..246872061 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -111,6 +111,12 @@ const handleClientVars = (message) => { // save the client Vars window.clientVars = message.data; + if (window.clientVars.sessionRefreshInterval) { + const ping = + () => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {}); + setInterval(ping, window.clientVars.sessionRefreshInterval); + } + // load all script that doesn't work without the clientVars BroadcastSlider = require('./broadcast_slider') .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); diff --git a/src/templates/pad.html b/src/templates/pad.html index 7ff447dc9..bc3cec88e 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -5,7 +5,7 @@ ; %> - + <% e.begin_block("htmlHead"); %> <% e.end_block(); %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index dc351b1d0..e26cd11e7 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -3,7 +3,7 @@ , langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs %> - + <%=settings.title%> Timeslider