mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-21 07:56:16 -04:00
Merge branch 'develop'
This commit is contained in:
commit
b1c34b5dac
90 changed files with 13938 additions and 10920 deletions
12
.github/workflows/frontend-admin-tests.yml
vendored
12
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -8,6 +8,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
withplugins:
|
withplugins:
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
name: with plugins
|
name: with plugins
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
@ -17,17 +18,6 @@ jobs:
|
||||||
node: [16, 18, 20]
|
node: [16, 18, 20]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Fail if Dependabot
|
|
||||||
if: github.actor == 'dependabot[bot]'
|
|
||||||
run: |
|
|
||||||
cat <<EOF >&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
|
name: Generate Sauce Labs strings
|
||||||
id: sauce_strings
|
id: sauce_strings
|
||||||
|
|
26
.github/workflows/frontend-tests.yml
vendored
26
.github/workflows/frontend-tests.yml
vendored
|
@ -10,18 +10,9 @@ jobs:
|
||||||
withoutplugins:
|
withoutplugins:
|
||||||
name: without plugins
|
name: without plugins
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Fail if Dependabot
|
|
||||||
if: github.actor == 'dependabot[bot]'
|
|
||||||
run: |
|
|
||||||
cat <<EOF >&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
|
name: Generate Sauce Labs strings
|
||||||
id: sauce_strings
|
id: sauce_strings
|
||||||
|
@ -74,18 +65,9 @@ jobs:
|
||||||
withplugins:
|
withplugins:
|
||||||
name: with plugins
|
name: with plugins
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Fail if Dependabot
|
|
||||||
if: github.actor == 'dependabot[bot]'
|
|
||||||
run: |
|
|
||||||
cat <<EOF >&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
|
name: Generate Sauce Labs strings
|
||||||
id: sauce_strings
|
id: sauce_strings
|
||||||
|
|
17
.github/workflows/windows.yml
vendored
17
.github/workflows/windows.yml
vendored
|
@ -15,13 +15,19 @@ jobs:
|
||||||
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
||||||
name: Build .zip
|
name: Build .zip
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: msys2 {0}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
with:
|
with:
|
||||||
|
release: false
|
||||||
|
update: false
|
||||||
path-type: inherit
|
path-type: inherit
|
||||||
install: >-
|
install: >-
|
||||||
zip
|
zip
|
||||||
|
rsync
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -35,16 +41,17 @@ jobs:
|
||||||
src/bin/doc/package-lock.json
|
src/bin/doc/package-lock.json
|
||||||
-
|
-
|
||||||
name: Install all dependencies and symlink for ep_etherpad-lite
|
name: Install all dependencies and symlink for ep_etherpad-lite
|
||||||
shell: msys2 {0}
|
run: |
|
||||||
run: src/bin/installDeps.sh
|
set MSYSTEM=winsymlinks:lnk
|
||||||
|
src/bin/installDeps.sh
|
||||||
-
|
-
|
||||||
name: Run the backend tests
|
name: Run the backend tests
|
||||||
shell: msys2 {0}
|
|
||||||
run: cd src && npm test
|
run: cd src && npm test
|
||||||
-
|
-
|
||||||
name: Build the .zip
|
name: Build the .zip
|
||||||
shell: msys2 {0}
|
run: |
|
||||||
run: src/bin/buildForWindows.sh
|
set MSYSTEM=winsymlinks:lnk
|
||||||
|
src/bin/buildForWindows.sh
|
||||||
-
|
-
|
||||||
name: Archive production artifacts
|
name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -1,3 +1,26 @@
|
||||||
|
# 1.9.2
|
||||||
|
|
||||||
|
### Notable enhancements and fixes
|
||||||
|
|
||||||
|
* Security
|
||||||
|
* Enable session key rotation: This setting can be enabled in the settings.json. It changes the signing key for the cookie authentication in a fixed interval.
|
||||||
|
|
||||||
|
* Bugfixes
|
||||||
|
* Fix appendRevision when creating a new pad via the API without a text.
|
||||||
|
|
||||||
|
|
||||||
|
* Enhancements
|
||||||
|
* Bump JQuery to version 3.7
|
||||||
|
* Update elasticsearch connector to version 8
|
||||||
|
|
||||||
|
### Compatibility changes
|
||||||
|
|
||||||
|
* No compability changes as JQuery maintains excellent backwards compatibility.
|
||||||
|
|
||||||
|
#### For plugin authors
|
||||||
|
|
||||||
|
* Please update to JQuery 3.7. There is an excellent deprecation guide over [here](https://api.jquery.com/category/deprecated/). Version 3.1 to 3.7 are relevant for the upgrade.
|
||||||
|
|
||||||
# 1.9.1
|
# 1.9.1
|
||||||
|
|
||||||
### Notable enhancements and fixes
|
### Notable enhancements and fixes
|
||||||
|
@ -37,6 +60,11 @@
|
||||||
session expires (with some exceptions that will be fixed in the future).
|
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.,
|
* Requests for static content (e.g., `/robots.txt`) and special pages (e.g.,
|
||||||
the HTTP API, `/stats`) no longer create login session state.
|
the HTTP API, `/stats`) no longer create login session state.
|
||||||
|
* The secret used to sign the `express_sid` cookie is now automatically
|
||||||
|
regenerated every day (called *key rotation*) by default. If key rotation is
|
||||||
|
enabled, the now-deprecated `SESSIONKEY.txt` file can be safely deleted
|
||||||
|
after Etherpad starts up (its content is read and saved to the database and
|
||||||
|
used to validate signatures from old cookies until they expire).
|
||||||
* The following settings from `settings.json` are now applied as expected (they
|
* The following settings from `settings.json` are now applied as expected (they
|
||||||
were unintentionally ignored before):
|
were unintentionally ignored before):
|
||||||
* `padOptions.lang`
|
* `padOptions.lang`
|
||||||
|
|
28
Dockerfile
28
Dockerfile
|
@ -4,15 +4,18 @@
|
||||||
#
|
#
|
||||||
# Author: muxator
|
# Author: muxator
|
||||||
|
|
||||||
FROM node:lts-slim
|
FROM node:lts-alpine
|
||||||
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
|
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
|
||||||
|
|
||||||
ARG TIMEZONE=
|
ARG TIMEZONE=
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
[ -z "${TIMEZONE}" ] || { \
|
[ -z "${TIMEZONE}" ] || { \
|
||||||
ln -sf /usr/share/zoneinfo/"${TIMEZONE#/usr/share/zoneinfo/}" /etc/localtime; \
|
apk add --no-cache tzdata && \
|
||||||
dpkg-reconfigure -f noninteractive tzdata; \
|
cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
|
||||||
|
echo "${TIMEZONE}" > /etc/timezone; \
|
||||||
}
|
}
|
||||||
|
ENV TIMEZONE=${TIMEZONE}
|
||||||
|
|
||||||
# plugins to install while building the container. By default no plugins are
|
# plugins to install while building the container. By default no plugins are
|
||||||
# installed.
|
# installed.
|
||||||
|
@ -42,7 +45,9 @@ ARG INSTALL_SOFFICE=
|
||||||
# leaner (development dependencies are not installed) and runs faster (among
|
# leaner (development dependencies are not installed) and runs faster (among
|
||||||
# other things, assets are minified & compressed).
|
# other things, assets are minified & compressed).
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV ETHERPAD_PRODUCTION=true
|
||||||
|
# Install dependencies required for modifying access.
|
||||||
|
RUN apk add shadow bash
|
||||||
# Follow the principle of least privilege: run as unprivileged user.
|
# Follow the principle of least privilege: run as unprivileged user.
|
||||||
#
|
#
|
||||||
# Running as non-root enables running this image in platforms like OpenShift
|
# Running as non-root enables running this image in platforms like OpenShift
|
||||||
|
@ -54,6 +59,9 @@ ARG EP_HOME=
|
||||||
ARG EP_UID=5001
|
ARG EP_UID=5001
|
||||||
ARG EP_GID=0
|
ARG EP_GID=0
|
||||||
ARG EP_SHELL=
|
ARG EP_SHELL=
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
|
RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
|
||||||
useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \
|
useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \
|
||||||
${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \
|
${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \
|
||||||
|
@ -64,18 +72,14 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
|
||||||
|
|
||||||
# the mkdir is needed for configuration of openjdk-11-jre-headless, see
|
# the mkdir is needed for configuration of openjdk-11-jre-headless, see
|
||||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive; \
|
RUN \
|
||||||
mkdir -p /usr/share/man/man1 && \
|
mkdir -p /usr/share/man/man1 && \
|
||||||
apt-get -qq update && \
|
apk update && apk upgrade && \
|
||||||
apt-get -qq dist-upgrade && \
|
apk add \
|
||||||
apt-get -qq --no-install-recommends install \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
${INSTALL_ABIWORD:+abiword} \
|
${INSTALL_ABIWORD:+abiword} \
|
||||||
${INSTALL_SOFFICE:+libreoffice default-jre libreoffice-java-common} \
|
${INSTALL_SOFFICE:+libreoffice openjdk8-jre libreoffice-common}
|
||||||
&& \
|
|
||||||
apt-get -qq clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
USER etherpad
|
USER etherpad
|
||||||
|
|
||||||
|
|
|
@ -370,6 +370,10 @@ For the editor container, you can also make it full width by adding `full-width-
|
||||||
| Description
|
| Description
|
||||||
| Default
|
| Default
|
||||||
|
|
||||||
|
|`COOKIE_KEY_ROTATION_INTERVAL`
|
||||||
|
|How often (ms) to rotate in a new secret for signing cookies
|
||||||
|
|`86400000` (1 day)
|
||||||
|
|
||||||
| `COOKIE_SAME_SITE`
|
| `COOKIE_SAME_SITE`
|
||||||
| Value of the SameSite cookie property.
|
| Value of the SameSite cookie property.
|
||||||
| `"Lax"`
|
| `"Lax"`
|
||||||
|
|
0
doc/docker.md
Normal file
0
doc/docker.md
Normal file
|
@ -363,6 +363,23 @@
|
||||||
* Settings controlling the session cookie issued by Etherpad.
|
* Settings controlling the session cookie issued by Etherpad.
|
||||||
*/
|
*/
|
||||||
"cookie": {
|
"cookie": {
|
||||||
|
/*
|
||||||
|
* How often (in milliseconds) the key used to sign the express_sid cookie
|
||||||
|
* should be rotated. Long rotation intervals reduce signature verification
|
||||||
|
* overhead (because there are fewer historical keys to check) and database
|
||||||
|
* load (fewer historical keys to store, and less frequent queries to
|
||||||
|
* get/update the keys). Short rotation intervals are slightly more secure.
|
||||||
|
*
|
||||||
|
* Multiple Etherpad processes sharing the same database (table) is
|
||||||
|
* supported as long as the clock sync error is significantly less than this
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* Key rotation can be disabled (not recommended) by setting this to 0 or
|
||||||
|
* null, or by disabling session expiration (see sessionLifetime).
|
||||||
|
*/
|
||||||
|
// 86400000 = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||||
|
"keyRotationInterval": "${COOKIE_KEY_ROTATION_INTERVAL:86400000}",
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Value of the SameSite cookie property. "Lax" is recommended unless
|
* Value of the SameSite cookie property. "Lax" is recommended unless
|
||||||
* Etherpad will be embedded in an iframe from another site, in which case
|
* Etherpad will be embedded in an iframe from another site, in which case
|
||||||
|
@ -392,6 +409,8 @@
|
||||||
* indefinitely without consulting authentication or authorization
|
* indefinitely without consulting authentication or authorization
|
||||||
* hooks, so once a user has accessed a pad, the user can continue to
|
* 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.
|
* use the pad until the user leaves for longer than sessionLifetime.
|
||||||
|
* - More historical keys (sessionLifetime / keyRotationInterval) must be
|
||||||
|
* checked when verifying signatures.
|
||||||
*
|
*
|
||||||
* Session lifetime can be set to infinity (not recommended) by setting this
|
* 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
|
* to null or 0. Note that if the session does not expire, most browsers
|
||||||
|
@ -634,5 +653,10 @@
|
||||||
"customLocaleStrings": {},
|
"customLocaleStrings": {},
|
||||||
|
|
||||||
/* Disable Admin UI tests */
|
/* Disable Admin UI tests */
|
||||||
"enableAdminUITests": false
|
"enableAdminUITests": false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable/Disable case-insensitive pad names.
|
||||||
|
*/
|
||||||
|
"lowerCasePadIds": "${LOWER_CASE_PAD_IDS:false}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -364,6 +364,22 @@
|
||||||
* Settings controlling the session cookie issued by Etherpad.
|
* Settings controlling the session cookie issued by Etherpad.
|
||||||
*/
|
*/
|
||||||
"cookie": {
|
"cookie": {
|
||||||
|
/*
|
||||||
|
* How often (in milliseconds) the key used to sign the express_sid cookie
|
||||||
|
* should be rotated. Long rotation intervals reduce signature verification
|
||||||
|
* overhead (because there are fewer historical keys to check) and database
|
||||||
|
* load (fewer historical keys to store, and less frequent queries to
|
||||||
|
* get/update the keys). Short rotation intervals are slightly more secure.
|
||||||
|
*
|
||||||
|
* Multiple Etherpad processes sharing the same database (table) is
|
||||||
|
* supported as long as the clock sync error is significantly less than this
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* Key rotation can be disabled (not recommended) by setting this to 0 or
|
||||||
|
* null, or by disabling session expiration (see sessionLifetime).
|
||||||
|
*/
|
||||||
|
"keyRotationInterval": 86400000, // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Value of the SameSite cookie property. "Lax" is recommended unless
|
* Value of the SameSite cookie property. "Lax" is recommended unless
|
||||||
* Etherpad will be embedded in an iframe from another site, in which case
|
* Etherpad will be embedded in an iframe from another site, in which case
|
||||||
|
@ -393,6 +409,8 @@
|
||||||
* indefinitely without consulting authentication or authorization
|
* indefinitely without consulting authentication or authorization
|
||||||
* hooks, so once a user has accessed a pad, the user can continue to
|
* 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.
|
* use the pad until the user leaves for longer than sessionLifetime.
|
||||||
|
* - More historical keys (sessionLifetime / keyRotationInterval) must be
|
||||||
|
* checked when verifying signatures.
|
||||||
*
|
*
|
||||||
* Session lifetime can be set to infinity (not recommended) by setting this
|
* 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
|
* to null or 0. Note that if the session does not expire, most browsers
|
||||||
|
@ -635,5 +653,10 @@
|
||||||
"customLocaleStrings": {},
|
"customLocaleStrings": {},
|
||||||
|
|
||||||
/* Disable Admin UI tests */
|
/* Disable Admin UI tests */
|
||||||
"enableAdminUITests": false
|
"enableAdminUITests": false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable/Disable case-insensitive pad names.
|
||||||
|
*/
|
||||||
|
"lowerCasePadIds": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ try cd "${workdir}"
|
||||||
[ -f src/package.json ] || fatal "failed to cd to etherpad root directory"
|
[ -f src/package.json ] || fatal "failed to cd to etherpad root directory"
|
||||||
|
|
||||||
# See https://github.com/msys2/MSYS2-packages/issues/1216
|
# See https://github.com/msys2/MSYS2-packages/issues/1216
|
||||||
export MSYS=winsymlinks:lnk
|
export MSYSTEM=winsymlinks:lnk
|
||||||
|
|
||||||
OUTPUT=${workdir}/etherpad-win.zip
|
OUTPUT=${workdir}/etherpad-win.zip
|
||||||
|
|
||||||
|
@ -29,10 +29,12 @@ trap 'exit 1' HUP INT TERM
|
||||||
trap 'log "cleaning up..."; try cd / && try rm -rf "${TMP_FOLDER}"' EXIT
|
trap 'log "cleaning up..."; try cd / && try rm -rf "${TMP_FOLDER}"' EXIT
|
||||||
|
|
||||||
log "create a clean environment in $TMP_FOLDER..."
|
log "create a clean environment in $TMP_FOLDER..."
|
||||||
try git archive --format=tar HEAD | (try cd "${TMP_FOLDER}" && try tar xf -) \
|
try export GIT_WORK_TREE=${TMP_FOLDER}; git checkout HEAD -f \
|
||||||
|| fatal "failed to copy etherpad to temporary folder"
|
|| fatal "failed to copy etherpad to temporary folder"
|
||||||
try mkdir "${TMP_FOLDER}"/.git
|
try mkdir "${TMP_FOLDER}"/.git
|
||||||
try git rev-parse HEAD >${TMP_FOLDER}/.git/HEAD
|
try git rev-parse HEAD >${TMP_FOLDER}/.git/HEAD
|
||||||
|
try cp -r ./src/node_modules "${TMP_FOLDER}"/src/node_modules
|
||||||
|
|
||||||
try cd "${TMP_FOLDER}"
|
try cd "${TMP_FOLDER}"
|
||||||
[ -f src/package.json ] || fatal "failed to copy etherpad to temporary folder"
|
[ -f src/package.json ] || fatal "failed to copy etherpad to temporary folder"
|
||||||
|
|
||||||
|
|
6
src/bin/doc/package-lock.json
generated
6
src/bin/doc/package-lock.json
generated
|
@ -5,9 +5,9 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": {
|
"marked": {
|
||||||
"version": "5.1.0",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-7.0.3.tgz",
|
||||||
"integrity": "sha512-z3/nBe7aTI8JDszlYLk7dDVNpngjw0o1ZJtrA9kIfkkHcIF+xH7mO23aISl4WxP83elU+MFROgahqdpd05lMEQ=="
|
"integrity": "sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"node": ">=12.17.0"
|
"node": ">=12.17.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "^5.1.0"
|
"marked": "^7.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"optionalDependencies": {},
|
"optionalDependencies": {},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
# Move to the Etherpad base directory.
|
# Move to the Etherpad base directory.
|
||||||
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
|
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
|
||||||
cd "${MY_DIR}/../.." || exit 1
|
cd "${MY_DIR}/../.." || exit 1
|
||||||
|
@ -36,14 +37,22 @@ if [ ! -f "$settings" ]; then
|
||||||
cp settings.json.template "$settings" || exit 1
|
cp settings.json.template "$settings" || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
log "Installing dependencies..."
|
log "Installing dependencies..."
|
||||||
(
|
(mkdir -p node_modules &&
|
||||||
mkdir -p node_modules &&
|
cd node_modules &&
|
||||||
cd node_modules &&
|
{ [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite; } &&
|
||||||
{ [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite; } &&
|
cd ep_etherpad-lite)
|
||||||
cd ep_etherpad-lite &&
|
|
||||||
npm ci --no-optional
|
cd src
|
||||||
) || exit 1
|
|
||||||
|
if [ -z "${ETHERPAD_PRODUCTION}" ]; then
|
||||||
|
log "Installing dev dependencies"
|
||||||
|
npm ci --no-optional --omit=optional --include=dev --lockfile-version 1 || exit 1
|
||||||
|
else
|
||||||
|
log "Installing production dependencies"
|
||||||
|
npm ci --no-optional --omit=optional --omit=dev --lockfile-version 1 --production || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove all minified data to force node creating it new
|
# Remove all minified data to force node creating it new
|
||||||
log "Clearing minified cache..."
|
log "Clearing minified cache..."
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"Mklehr",
|
"Mklehr",
|
||||||
"Nipsky",
|
"Nipsky",
|
||||||
"Predatorix",
|
"Predatorix",
|
||||||
|
"SamTV",
|
||||||
"Sebastian Wallroth",
|
"Sebastian Wallroth",
|
||||||
"Thargon",
|
"Thargon",
|
||||||
"Tim.krieger",
|
"Tim.krieger",
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"admin.page-title": "Admin Dashboard - Etherpad",
|
"admin.page-title": "Admin Dashboard - Etherpad",
|
||||||
"admin_plugins": "Plugins verwalten",
|
"admin_plugins": "Pluginverwaltung",
|
||||||
"admin_plugins.available": "Verfügbare Plugins",
|
"admin_plugins.available": "Verfügbare Plugins",
|
||||||
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
|
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
|
||||||
"admin_plugins.available_fetching": "Wird abgerufen...",
|
"admin_plugins.available_fetching": "Wird abgerufen...",
|
||||||
|
@ -40,12 +41,12 @@
|
||||||
"admin_plugins_info.plugins": "Installierte Plugins",
|
"admin_plugins_info.plugins": "Installierte Plugins",
|
||||||
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
|
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
|
||||||
"admin_plugins_info.version": "Etherpad Version",
|
"admin_plugins_info.version": "Etherpad Version",
|
||||||
"admin_plugins_info.version_latest": "Neueste Version",
|
"admin_plugins_info.version_latest": "Neueste verfügbare Version",
|
||||||
"admin_plugins_info.version_number": "Versionsnummer",
|
"admin_plugins_info.version_number": "Versionsnummer",
|
||||||
"admin_settings": "Einstellungen",
|
"admin_settings": "Einstellungen",
|
||||||
"admin_settings.current": "Derzeitige Konfiguration",
|
"admin_settings.current": "Derzeitige Konfiguration",
|
||||||
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
|
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
|
||||||
"admin_settings.current_example-prod": "Beispiel einer Vorlage für Produktionseinstellungen",
|
"admin_settings.current_example-prod": "Beispiel eines produktiven Templates",
|
||||||
"admin_settings.current_restart.value": "Etherpad neustarten",
|
"admin_settings.current_restart.value": "Etherpad neustarten",
|
||||||
"admin_settings.current_save.value": "Einstellungen speichern",
|
"admin_settings.current_save.value": "Einstellungen speichern",
|
||||||
"admin_settings.page-title": "Einstellungen - Etherpad",
|
"admin_settings.page-title": "Einstellungen - Etherpad",
|
||||||
|
@ -71,9 +72,9 @@
|
||||||
"pad.toolbar.showusers.title": "Benutzer dieses Pads anzeigen",
|
"pad.toolbar.showusers.title": "Benutzer dieses Pads anzeigen",
|
||||||
"pad.colorpicker.save": "Speichern",
|
"pad.colorpicker.save": "Speichern",
|
||||||
"pad.colorpicker.cancel": "Abbrechen",
|
"pad.colorpicker.cancel": "Abbrechen",
|
||||||
"pad.loading": "Lade …",
|
"pad.loading": "Laden …",
|
||||||
"pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.",
|
"pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.",
|
||||||
"pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen",
|
"pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen.",
|
||||||
"pad.settings.padSettings": "Pad-Einstellungen",
|
"pad.settings.padSettings": "Pad-Einstellungen",
|
||||||
"pad.settings.myView": "Eigene Ansicht",
|
"pad.settings.myView": "Eigene Ansicht",
|
||||||
"pad.settings.stickychat": "Unterhaltung immer anzeigen",
|
"pad.settings.stickychat": "Unterhaltung immer anzeigen",
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
"pad.settings.fontType.normal": "Normal",
|
"pad.settings.fontType.normal": "Normal",
|
||||||
"pad.settings.language": "Sprache:",
|
"pad.settings.language": "Sprache:",
|
||||||
"pad.settings.about": "Über",
|
"pad.settings.about": "Über",
|
||||||
"pad.settings.poweredBy": "Powered by",
|
"pad.settings.poweredBy": "Betrieben von",
|
||||||
"pad.importExport.import_export": "Import/Export",
|
"pad.importExport.import_export": "Import/Export",
|
||||||
"pad.importExport.import": "Textdatei oder Dokument hochladen",
|
"pad.importExport.import": "Textdatei oder Dokument hochladen",
|
||||||
"pad.importExport.importSuccessful": "Erfolgreich!",
|
"pad.importExport.importSuccessful": "Erfolgreich!",
|
||||||
|
@ -120,7 +121,7 @@
|
||||||
"pad.modals.corruptPad.cause": "Dies könnte an einer falschen Serverkonfiguration oder einem anderen unerwarteten Verhalten liegen. Bitte kontaktiere den Administrator dieses Dienstes.",
|
"pad.modals.corruptPad.cause": "Dies könnte an einer falschen Serverkonfiguration oder einem anderen unerwarteten Verhalten liegen. Bitte kontaktiere den Administrator dieses Dienstes.",
|
||||||
"pad.modals.deleted": "Gelöscht.",
|
"pad.modals.deleted": "Gelöscht.",
|
||||||
"pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.",
|
"pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.",
|
||||||
"pad.modals.rateLimited": "Begrenzte Rate.",
|
"pad.modals.rateLimited": "Durchsatzratenbegrenzt",
|
||||||
"pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.",
|
"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.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.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.",
|
||||||
|
@ -140,7 +141,7 @@
|
||||||
"timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
|
"timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
|
||||||
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
|
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
|
||||||
"timeslider.toolbar.authors": "Autoren:",
|
"timeslider.toolbar.authors": "Autoren:",
|
||||||
"timeslider.toolbar.authorsList": "keine Autoren",
|
"timeslider.toolbar.authorsList": "Keine Autoren",
|
||||||
"timeslider.toolbar.exportlink.title": "Diese Version exportieren",
|
"timeslider.toolbar.exportlink.title": "Diese Version exportieren",
|
||||||
"timeslider.exportCurrent": "Exportiere diese Version als:",
|
"timeslider.exportCurrent": "Exportiere diese Version als:",
|
||||||
"timeslider.version": "Version {{version}}",
|
"timeslider.version": "Version {{version}}",
|
||||||
|
@ -163,7 +164,7 @@
|
||||||
"timeslider.month.december": "Dezember",
|
"timeslider.month.december": "Dezember",
|
||||||
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: unbenannter Autor, other: unbenannte Autoren ]}",
|
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: unbenannter Autor, other: unbenannte Autoren ]}",
|
||||||
"pad.savedrevs.marked": "Diese Version wurde jetzt als gespeicherte Version gekennzeichnet",
|
"pad.savedrevs.marked": "Diese Version wurde jetzt als gespeicherte Version gekennzeichnet",
|
||||||
"pad.savedrevs.timeslider": "Du kannst gespeicherte Versionen durch den Aufruf des Bearbeitungsverlaufes ansehen.",
|
"pad.savedrevs.timeslider": "Du kannst gespeicherte Versionen durch den Aufruf des Bearbeitungsverlaufs ansehen.",
|
||||||
"pad.userlist.entername": "Dein Name?",
|
"pad.userlist.entername": "Dein Name?",
|
||||||
"pad.userlist.unnamed": "unbenannt",
|
"pad.userlist.unnamed": "unbenannt",
|
||||||
"pad.editbar.clearcolors": "Autorenfarben im gesamten Dokument zurücksetzen? Dies kann nicht rückgängig gemacht werden",
|
"pad.editbar.clearcolors": "Autorenfarben im gesamten Dokument zurücksetzen? Dies kann nicht rückgängig gemacht werden",
|
||||||
|
|
|
@ -75,7 +75,7 @@ class Pad {
|
||||||
|
|
||||||
async appendRevision(aChangeset, authorId = '') {
|
async appendRevision(aChangeset, authorId = '') {
|
||||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs) {
|
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && this.head !== -1) {
|
||||||
return this.head;
|
return this.head;
|
||||||
}
|
}
|
||||||
Changeset.copyAText(newAText, this.atext);
|
Changeset.copyAText(newAText, this.atext);
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require('../utils/customError');
|
||||||
const Pad = require('../db/Pad');
|
const Pad = require('../db/Pad');
|
||||||
const db = require('./DB');
|
const db = require('./DB');
|
||||||
|
const settings = require('../utils/Settings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache of all loaded Pads.
|
* A cache of all loaded Pads.
|
||||||
|
@ -170,6 +171,8 @@ exports.sanitizePadId = async (padId) => {
|
||||||
padId = padId.replace(from, to);
|
padId = padId.replace(from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.lowerCasePadIds) padId = padId.toLowerCase();
|
||||||
|
|
||||||
// we're out of possible transformations, so just return it
|
// we're out of possible transformations, so just return it
|
||||||
return padId;
|
return padId;
|
||||||
};
|
};
|
||||||
|
|
|
@ -89,24 +89,24 @@ const doImport = async (req, res, padId, authorId) => {
|
||||||
maxFileSize: settings.importMaxFileSize,
|
maxFileSize: settings.importMaxFileSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// locally wrapped Promise, since form.parse requires a callback
|
let srcFile;
|
||||||
let srcFile = await new Promise((resolve, reject) => {
|
let files;
|
||||||
form.parse(req, (err, fields, files) => {
|
let fields;
|
||||||
if (err != null) {
|
try {
|
||||||
|
[fields, files] = await form.parse(req);
|
||||||
|
} catch (err) {
|
||||||
logger.warn(`Import failed due to form error: ${err.stack || err}`);
|
logger.warn(`Import failed due to form error: ${err.stack || err}`);
|
||||||
// I hate doing indexOf here but I can't see anything to use...
|
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
|
||||||
if (err && err.stack && err.stack.indexOf('maxFileSize') !== -1) {
|
throw new ImportError('maxFileSize');
|
||||||
return reject(new ImportError('maxFileSize'));
|
|
||||||
}
|
}
|
||||||
return reject(new ImportError('uploadFailed'));
|
throw new ImportError('uploadFailed');
|
||||||
}
|
}
|
||||||
if (!files.file) {
|
if (!files.file) {
|
||||||
logger.warn('Import failed because form had no file');
|
logger.warn('Import failed because form had no file');
|
||||||
return reject(new ImportError('uploadFailed'));
|
throw new ImportError('uploadFailed');
|
||||||
|
} else {
|
||||||
|
srcFile = files.file[0].filepath;
|
||||||
}
|
}
|
||||||
resolve(files.file.filepath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ensure this is a file ending we know, else we change the file ending to .txt
|
// ensure this is a file ending we know, else we change the file ending to .txt
|
||||||
// this allows us to accept source code files like .c or .java
|
// this allows us to accept source code files like .c or .java
|
||||||
|
|
|
@ -236,6 +236,11 @@ exports.handleMessage = async (socket, message) => {
|
||||||
padID: message.padId,
|
padID: message.padId,
|
||||||
token: message.token,
|
token: message.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pad does not exist, so we need to sanitize the id
|
||||||
|
if (!(await padManager.doesPadExist(thisSession.auth.padID))) {
|
||||||
|
thisSession.auth.padID = await padManager.sanitizePadId(thisSession.auth.padID);
|
||||||
|
}
|
||||||
const padIds = await readOnlyManager.getIds(thisSession.auth.padID);
|
const padIds = await readOnlyManager.getIds(thisSession.auth.padID);
|
||||||
thisSession.padId = padIds.padId;
|
thisSession.padId = padIds.padId;
|
||||||
thisSession.readOnlyPadId = padIds.readOnlyPadId;
|
thisSession.readOnlyPadId = padIds.readOnlyPadId;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
|
const SecretRotator = require('../security/SecretRotator');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const events = require('events');
|
const events = require('events');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
@ -14,6 +15,7 @@ const stats = require('../stats');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const webaccess = require('./express/webaccess');
|
const webaccess = require('./express/webaccess');
|
||||||
|
|
||||||
|
let secretRotator = null;
|
||||||
const logger = log4js.getLogger('http');
|
const logger = log4js.getLogger('http');
|
||||||
let serverName;
|
let serverName;
|
||||||
let sessionStore;
|
let sessionStore;
|
||||||
|
@ -53,6 +55,8 @@ const closeServer = async () => {
|
||||||
}
|
}
|
||||||
if (sessionStore) sessionStore.shutdown();
|
if (sessionStore) sessionStore.shutdown();
|
||||||
sessionStore = null;
|
sessionStore = null;
|
||||||
|
if (secretRotator) secretRotator.stop();
|
||||||
|
secretRotator = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createServer = async () => {
|
exports.createServer = async () => {
|
||||||
|
@ -174,13 +178,23 @@ exports.restartServer = async () => {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(cookieParser(settings.sessionKey, {}));
|
const {keyRotationInterval, sessionLifetime} = settings.cookie;
|
||||||
|
let secret = settings.sessionKey;
|
||||||
|
if (keyRotationInterval && sessionLifetime) {
|
||||||
|
secretRotator = new SecretRotator(
|
||||||
|
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey);
|
||||||
|
await secretRotator.start();
|
||||||
|
secret = secretRotator.secrets;
|
||||||
|
}
|
||||||
|
if (!secret) throw new Error('missing cookie signing secret');
|
||||||
|
|
||||||
|
app.use(cookieParser(secret, {}));
|
||||||
|
|
||||||
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
||||||
exports.sessionMiddleware = expressSession({
|
exports.sessionMiddleware = expressSession({
|
||||||
propagateTouch: true,
|
propagateTouch: true,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
secret: settings.sessionKey,
|
secret,
|
||||||
store: sessionStore,
|
store: sessionStore,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
|
@ -188,7 +202,7 @@ exports.restartServer = async () => {
|
||||||
// cleaner :)
|
// cleaner :)
|
||||||
name: 'express_sid',
|
name: 'express_sid',
|
||||||
cookie: {
|
cookie: {
|
||||||
maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null.
|
maxAge: sessionLifetime || null, // Convert 0 to null.
|
||||||
sameSite: settings.cookie.sameSite,
|
sameSite: settings.cookie.sameSite,
|
||||||
|
|
||||||
// The automatic express-session mechanism for determining if the application is being served
|
// The automatic express-session mechanism for determining if the application is being served
|
||||||
|
|
|
@ -8,20 +8,19 @@ const util = require('util');
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName, {app}) => {
|
exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// The Etherpad client side sends information about how a disconnect happened
|
// The Etherpad client side sends information about how a disconnect happened
|
||||||
app.post('/ep/pad/connection-diagnostic-info', (req, res) => {
|
app.post('/ep/pad/connection-diagnostic-info', async (req, res) => {
|
||||||
new Formidable().parse(req, (err, fields, files) => {
|
const [fields, files] = await (new Formidable({})).parse(req);
|
||||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||||
res.end('OK');
|
res.end('OK');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const parseJserrorForm = async (req) => await new Promise((resolve, reject) => {
|
const parseJserrorForm = async (req) => {
|
||||||
const form = new Formidable({
|
const form = new Formidable({
|
||||||
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
|
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
|
||||||
});
|
});
|
||||||
form.on('error', (err) => reject(err));
|
const [fields, files] = await form.parse(req);
|
||||||
form.parse(req, (err, fields) => err != null ? reject(err) : resolve(fields.errorInfo));
|
return fields.errorInfo;
|
||||||
});
|
};
|
||||||
|
|
||||||
// The Etherpad client side sends information about client side javscript errors
|
// The Etherpad client side sends information about client side javscript errors
|
||||||
app.post('/jserror', (req, res, next) => {
|
app.post('/jserror', (req, res, next) => {
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OpenAPIBackend = require('openapi-backend').default;
|
const OpenAPIBackend = require('openapi-backend').default;
|
||||||
const formidable = require('formidable');
|
const IncomingForm = require('formidable').IncomingForm;
|
||||||
const {promisify} = require('util');
|
|
||||||
const cloneDeep = require('lodash.clonedeep');
|
const cloneDeep = require('lodash.clonedeep');
|
||||||
const createHTTPError = require('http-errors');
|
const createHTTPError = require('http-errors');
|
||||||
|
|
||||||
|
@ -596,9 +595,13 @@ exports.expressPreSession = async (hookName, {app}) => {
|
||||||
// read form data if method was POST
|
// read form data if method was POST
|
||||||
let formData = {};
|
let formData = {};
|
||||||
if (c.request.method === 'post') {
|
if (c.request.method === 'post') {
|
||||||
const form = new formidable.IncomingForm();
|
const form = new IncomingForm();
|
||||||
const parseForm = promisify(form.parse).bind(form);
|
formData = (await form.parse(req))[0];
|
||||||
formData = await parseForm(req);
|
for (const k of Object.keys(formData)) {
|
||||||
|
if (formData[k] instanceof Array) {
|
||||||
|
formData[k] = formData[k][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = Object.assign({}, header, params, query, formData);
|
const fields = Object.assign({}, header, params, query, formData);
|
||||||
|
|
|
@ -149,7 +149,10 @@ const checkAccess = async (req, res, next) => {
|
||||||
if (!(await aCallFirst0('authenticate', ctx))) {
|
if (!(await aCallFirst0('authenticate', ctx))) {
|
||||||
// Fall back to HTTP basic auth.
|
// Fall back to HTTP basic auth.
|
||||||
const {[ctx.username]: {password} = {}} = settings.users;
|
const {[ctx.username]: {password} = {}} = settings.users;
|
||||||
if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) {
|
|
||||||
|
if (!httpBasicAuth ||
|
||||||
|
!ctx.username ||
|
||||||
|
password == null || password.toString() !== ctx.password) {
|
||||||
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
||||||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
if (await aCallFirst0('authnFailure', {req, res})) return;
|
||||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||||
|
|
251
src/node/security/SecretRotator.js
Normal file
251
src/node/security/SecretRotator.js
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {Buffer} = require('buffer');
|
||||||
|
const crypto = require('./crypto');
|
||||||
|
const db = require('../db/DB');
|
||||||
|
const log4js = require('log4js');
|
||||||
|
|
||||||
|
class Kdf {
|
||||||
|
async generateParams() { throw new Error('not implemented'); }
|
||||||
|
async derive(params, info) { throw new Error('not implemented'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class LegacyStaticSecret extends Kdf {
|
||||||
|
async derive(params, info) { return params; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hkdf extends Kdf {
|
||||||
|
constructor(digest, keyLen) {
|
||||||
|
super();
|
||||||
|
this._digest = digest;
|
||||||
|
this._keyLen = keyLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateParams() {
|
||||||
|
const [secret, salt] = (await Promise.all([
|
||||||
|
crypto.randomBytes(this._keyLen),
|
||||||
|
crypto.randomBytes(this._keyLen),
|
||||||
|
])).map((b) => b.toString('hex'));
|
||||||
|
return {digest: this._digest, keyLen: this._keyLen, salt, secret};
|
||||||
|
}
|
||||||
|
|
||||||
|
async derive(p, info) {
|
||||||
|
return Buffer.from(
|
||||||
|
await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key derivation algorithms. Do not modify entries in this array, except:
|
||||||
|
// * It is OK to replace an unused algorithm with `null` after any entries in the database
|
||||||
|
// using the algorithm have been deleted.
|
||||||
|
// * It is OK to append a new algorithm to the end.
|
||||||
|
// If the entries are modified in any other way then key derivation might fail or produce invalid
|
||||||
|
// results due to broken compatibility with existing database records.
|
||||||
|
const algorithms = [
|
||||||
|
new LegacyStaticSecret(),
|
||||||
|
new Hkdf('sha256', 32),
|
||||||
|
];
|
||||||
|
const defaultAlgId = algorithms.length - 1;
|
||||||
|
|
||||||
|
// In JavaScript, the % operator is remainder, not modulus.
|
||||||
|
const mod = (a, n) => ((a % n) + n) % n;
|
||||||
|
const intervalStart = (t, interval) => t - mod(t, interval);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains an array of secrets across one or more Etherpad instances sharing the same database,
|
||||||
|
* periodically rotating in a new secret and removing the oldest secret.
|
||||||
|
*
|
||||||
|
* The secrets are generated using a key derivation function (KDF) with input keying material coming
|
||||||
|
* from a long-lived secret stored in the database (generated if missing).
|
||||||
|
*/
|
||||||
|
class SecretRotator {
|
||||||
|
/**
|
||||||
|
* @param {string} dbPrefix - Database key prefix to use for tracking secret metadata.
|
||||||
|
* @param {number} interval - How often to rotate in a new secret.
|
||||||
|
* @param {number} lifetime - How long after the end of an interval before the secret is no longer
|
||||||
|
* useful.
|
||||||
|
* @param {string} [legacyStaticSecret] - Optional secret to facilitate migration to secret
|
||||||
|
* rotation. If the oldest known secret starts after `lifetime` ago, this secret will cover
|
||||||
|
* the time period starting `lifetime` ago and ending at the start of that secret.
|
||||||
|
*/
|
||||||
|
constructor(dbPrefix, interval, lifetime, legacyStaticSecret = null) {
|
||||||
|
/**
|
||||||
|
* The secrets. The first secret in this array is the one that should be used to generate new
|
||||||
|
* MACs. All of the secrets in this array should be used when attempting to authenticate an
|
||||||
|
* existing MAC. The contents of this array will be updated every `interval` milliseconds, but
|
||||||
|
* the Array object itself will never be replaced with a new Array object.
|
||||||
|
*
|
||||||
|
* @type {string[]}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
this.secrets = [];
|
||||||
|
Object.defineProperty(this, 'secrets', {writable: false}); // Defend against bugs.
|
||||||
|
|
||||||
|
if (/[*:%]/.test(dbPrefix)) throw new Error(`dbPrefix contains an invalid char: ${dbPrefix}`);
|
||||||
|
this._dbPrefix = dbPrefix;
|
||||||
|
this._interval = interval;
|
||||||
|
this._legacyStaticSecret = legacyStaticSecret;
|
||||||
|
this._lifetime = lifetime;
|
||||||
|
this._logger = log4js.getLogger(`secret-rotation ${dbPrefix}`);
|
||||||
|
this._logger.debug(`new secret rotator (interval ${interval}, lifetime: ${lifetime})`);
|
||||||
|
this._updateTimeout = null;
|
||||||
|
|
||||||
|
// Indirections to facilitate testing.
|
||||||
|
this._t = {now: Date.now.bind(Date), setTimeout, clearTimeout, algorithms};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _publish(params, id = null) {
|
||||||
|
// Params are published to the db with a randomly generated key to avoid race conditions with
|
||||||
|
// other instances.
|
||||||
|
if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`;
|
||||||
|
await db.set(id, params);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
this._logger.debug('starting secret rotation');
|
||||||
|
if (this._updateTimeout != null) return; // Already started.
|
||||||
|
await this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this._logger.debug('stopping secret rotation');
|
||||||
|
this._t.clearTimeout(this._updateTimeout);
|
||||||
|
this._updateTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _deriveSecrets(p, now) {
|
||||||
|
this._logger.debug('deriving secrets from', p);
|
||||||
|
if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)];
|
||||||
|
const t0 = intervalStart(now, p.interval);
|
||||||
|
// Start of the first interval covered by these params. To accommodate clock skew, p.interval is
|
||||||
|
// subtracted. If we did not do this, then the following could happen:
|
||||||
|
// 1. Instance (A) starts up and publishes params starting at the current interval.
|
||||||
|
// 2. Instance (B) starts up with a clock that is in the previous interval.
|
||||||
|
// 3. Instance (B) reads the params published by instance (A) and sees that there's no
|
||||||
|
// coverage of what it thinks is the current interval.
|
||||||
|
// 4. Instance (B) generates and publishes new params that covers what it thinks is the
|
||||||
|
// current interval.
|
||||||
|
// 5. Instance (B) starts generating MACs from a secret derived from the new params.
|
||||||
|
// 6. Instance (A) fails to validate the MACs generated by instance (B) until it re-reads
|
||||||
|
// the published params, which might take as long as interval.
|
||||||
|
// An alternative approach is to backdate p.start by p.interval when creating new params, but
|
||||||
|
// this could affect the end time of legacy secrets.
|
||||||
|
const tA = intervalStart(p.start - p.interval, p.interval);
|
||||||
|
const tZ = intervalStart(p.end - 1, p.interval);
|
||||||
|
this._logger.debug('now:', now, 't0:', t0, 'tA:', tA, 'tZ:', tZ);
|
||||||
|
// Starts of intervals to derive keys for.
|
||||||
|
const tNs = [];
|
||||||
|
// Whether the derived secret for the interval starting at tN is still relevant. If there was no
|
||||||
|
// clock skew, a derived secret is relevant until p.lifetime has elapsed since the end of the
|
||||||
|
// interval. To accommodate clock skew, this end time is extended by p.interval.
|
||||||
|
const expired = (tN) => now >= tN + (2 * p.interval) + p.lifetime;
|
||||||
|
// Walk from t0 back until either the start of coverage or the derived secret is expired. t0
|
||||||
|
// must always be the first entry in case p is the current params. (The first derived secret is
|
||||||
|
// used for generating MACs, so the secret derived for t0 must be before the secrets derived for
|
||||||
|
// other times.)
|
||||||
|
for (let tN = Math.min(t0, tZ); tN >= tA && !expired(tN); tN -= p.interval) tNs.push(tN);
|
||||||
|
// Include a future derived secret to accommodate clock skew.
|
||||||
|
if (t0 + p.interval <= tZ) tNs.push(t0 + p.interval);
|
||||||
|
this._logger.debug('deriving secrets for intervals with start times:', tNs);
|
||||||
|
return await Promise.all(
|
||||||
|
tNs.map(async (tN) => await algorithms[p.algId].derive(p.algParams, `${tN}`)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _update() {
|
||||||
|
const now = this._t.now();
|
||||||
|
const t0 = intervalStart(now, this._interval);
|
||||||
|
let next = t0 + this._interval; // When this._update() should be called again.
|
||||||
|
let legacyEnd = now;
|
||||||
|
// TODO: This is racy. If two instances start up at the same time and there are no existing
|
||||||
|
// matching publications, each will generate and publish their own paramters. In practice this
|
||||||
|
// is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances.
|
||||||
|
const dbKeys = await db.findKeys(`${this._dbPrefix}:*`, null);
|
||||||
|
let currentParams = null;
|
||||||
|
let currentId = null;
|
||||||
|
const dbWrites = [];
|
||||||
|
const allParams = [];
|
||||||
|
const legacyParams = [];
|
||||||
|
await Promise.all(dbKeys.map(async (dbKey) => {
|
||||||
|
const p = await db.get(dbKey);
|
||||||
|
if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p);
|
||||||
|
if (p.start < legacyEnd) legacyEnd = p.start;
|
||||||
|
// Check if the params have expired. Params are still useful if a MAC generated by a secret
|
||||||
|
// derived from the params is still valid, which can be true up to p.end + p.lifetime if
|
||||||
|
// there was no clock skew. The p.interval factor is added to accommodate clock skew.
|
||||||
|
// p.interval is null for legacy secrets, so fall back to this._interval.
|
||||||
|
if (now >= p.end + p.lifetime + (p.interval || this._interval)) {
|
||||||
|
// This initial keying material (or legacy secret) is expired.
|
||||||
|
dbWrites.push(db.remove(dbKey));
|
||||||
|
dbWrites[dbWrites.length - 1].catch(() => {}); // Prevent unhandled Promise rejections.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t1 = p.interval && intervalStart(now, p.interval) + p.interval; // Start of next intrvl.
|
||||||
|
const tA = intervalStart(p.start, p.interval); // Start of interval containing p.start.
|
||||||
|
if (p.interval) next = Math.min(next, t1);
|
||||||
|
// Determine if these params can be used to generate the current (active) secret. Note that
|
||||||
|
// p.start is allowed to be in the next interval in case there is clock skew.
|
||||||
|
if (p.interval && p.interval === this._interval && p.lifetime === this._lifetime &&
|
||||||
|
tA <= t1 && p.end > now && (currentParams == null || p.start > currentParams.start)) {
|
||||||
|
if (currentParams) allParams.push(currentParams);
|
||||||
|
currentParams = p;
|
||||||
|
currentId = dbKey;
|
||||||
|
} else {
|
||||||
|
allParams.push(p);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if (this._legacyStaticSecret && now < legacyEnd + this._lifetime + this._interval &&
|
||||||
|
!legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) {
|
||||||
|
const d = new Date(legacyEnd).toJSON();
|
||||||
|
this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`);
|
||||||
|
const p = {
|
||||||
|
algId: 0,
|
||||||
|
algParams: this._legacyStaticSecret,
|
||||||
|
// The start time is equal to the end time so that this legacy secret does not affect the
|
||||||
|
// end times of any legacy secrets published by other instances.
|
||||||
|
start: legacyEnd,
|
||||||
|
end: legacyEnd,
|
||||||
|
interval: null,
|
||||||
|
lifetime: this._lifetime,
|
||||||
|
};
|
||||||
|
allParams.push(p);
|
||||||
|
dbWrites.push(this._publish(p));
|
||||||
|
dbWrites[dbWrites.length - 1].catch(() => {}); // Prevent unhandled Promise rejections.
|
||||||
|
}
|
||||||
|
if (currentParams == null) {
|
||||||
|
currentParams = {
|
||||||
|
algId: defaultAlgId,
|
||||||
|
algParams: await algorithms[defaultAlgId].generateParams(),
|
||||||
|
start: now,
|
||||||
|
end: now, // Extended below.
|
||||||
|
interval: this._interval,
|
||||||
|
lifetime: this._lifetime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Advance currentParams's expiration time to the end of the next interval if needed. (The next
|
||||||
|
// interval is used so that the parameters never expire under normal circumstances.) This must
|
||||||
|
// be done before deriving any secrets from currentParams so that a secret for the next interval
|
||||||
|
// can be included (in case there is clock skew).
|
||||||
|
currentParams.end = Math.max(currentParams.end, t0 + (2 * this._interval));
|
||||||
|
dbWrites.push(this._publish(currentParams, currentId));
|
||||||
|
dbWrites[dbWrites.length - 1].catch(() => {}); // Prevent unhandled Promise rejections.
|
||||||
|
// The secrets derived from currentParams MUST be the first secrets.
|
||||||
|
const secrets = await this._deriveSecrets(currentParams, now);
|
||||||
|
await Promise.all(
|
||||||
|
allParams.map(async (p) => secrets.push(...await this._deriveSecrets(p, now))));
|
||||||
|
// Update this.secrets all at once to avoid race conditions.
|
||||||
|
this.secrets.length = 0;
|
||||||
|
this.secrets.push(...secrets);
|
||||||
|
this._logger.debug('active secrets:', this.secrets);
|
||||||
|
// Wait for db writes to finish after updating this.secrets so that the new secrets become
|
||||||
|
// active as soon as possible.
|
||||||
|
await Promise.all(dbWrites);
|
||||||
|
// Use an async function so that test code can tell when it's done publishing the new secrets.
|
||||||
|
// The standard setTimeout() function ignores the callback's return value, but some of the tests
|
||||||
|
// await the returned Promise.
|
||||||
|
this._updateTimeout =
|
||||||
|
this._t.setTimeout(async () => await this._update(), next - this._t.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SecretRotator;
|
15
src/node/security/crypto.js
Normal file
15
src/node/security/crypto.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified version of Node.js's crypto.hkdf.
|
||||||
|
*/
|
||||||
|
exports.hkdf = util.promisify(crypto.hkdf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified version of Node.js's crypto.randomBytes
|
||||||
|
*/
|
||||||
|
exports.randomBytes = util.promisify(crypto.randomBytes);
|
|
@ -276,3 +276,4 @@ exports.exit = async (err = null) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (require.main === module) exports.start();
|
if (require.main === module) exports.start();
|
||||||
|
if (typeof(PhusionPassenger) !== 'undefined') exports.start();
|
||||||
|
|
|
@ -297,9 +297,9 @@ exports.indentationOnNewLine = true;
|
||||||
exports.logconfig = defaultLogConfig();
|
exports.logconfig = defaultLogConfig();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Session Key, do not sure this.
|
* Deprecated cookie signing key.
|
||||||
*/
|
*/
|
||||||
exports.sessionKey = false;
|
exports.sessionKey = null;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Trust Proxy, whether or not trust the x-forwarded-for header.
|
* Trust Proxy, whether or not trust the x-forwarded-for header.
|
||||||
|
@ -310,6 +310,7 @@ exports.trustProxy = false;
|
||||||
* Settings controlling the session cookie issued by Etherpad.
|
* Settings controlling the session cookie issued by Etherpad.
|
||||||
*/
|
*/
|
||||||
exports.cookie = {
|
exports.cookie = {
|
||||||
|
keyRotationInterval: 1 * 24 * 60 * 60 * 1000,
|
||||||
/*
|
/*
|
||||||
* Value of the SameSite cookie property. "Lax" is recommended unless
|
* Value of the SameSite cookie property. "Lax" is recommended unless
|
||||||
* Etherpad will be embedded in an iframe from another site, in which case
|
* Etherpad will be embedded in an iframe from another site, in which case
|
||||||
|
@ -430,6 +431,11 @@ exports.importMaxFileSize = 50 * 1024 * 1024;
|
||||||
*/
|
*/
|
||||||
exports.enableAdminUITests = false;
|
exports.enableAdminUITests = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable auto conversion of pad Ids to lowercase.
|
||||||
|
* e.g. /p/EtHeRpAd to /p/etherpad
|
||||||
|
*/
|
||||||
|
exports.lowerCasePadIds = false;
|
||||||
|
|
||||||
// checks if abiword is avaiable
|
// checks if abiword is avaiable
|
||||||
exports.abiwordAvailable = () => {
|
exports.abiwordAvailable = () => {
|
||||||
|
@ -800,12 +806,14 @@ exports.reloadSettings = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exports.sessionKey) {
|
|
||||||
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
|
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
|
||||||
|
if (!exports.sessionKey) {
|
||||||
try {
|
try {
|
||||||
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
||||||
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
|
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
|
||||||
} catch (e) {
|
} catch (err) { /* ignored */ }
|
||||||
|
const keyRotationEnabled = exports.cookie.keyRotationInterval && exports.cookie.sessionLifetime;
|
||||||
|
if (!exports.sessionKey && !keyRotationEnabled) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
|
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
|
||||||
exports.sessionKey = randomString(32);
|
exports.sessionKey = randomString(32);
|
||||||
|
@ -817,6 +825,10 @@ exports.reloadSettings = () => {
|
||||||
'If you are seeing this error after restarting using the Admin User ' +
|
'If you are seeing this error after restarting using the Admin User ' +
|
||||||
'Interface then you can ignore this message.');
|
'Interface then you can ignore this message.');
|
||||||
}
|
}
|
||||||
|
if (exports.sessionKey) {
|
||||||
|
logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` +
|
||||||
|
'use automatic key rotation instead (see the cookie.keyRotationInterval setting).');
|
||||||
|
}
|
||||||
|
|
||||||
if (exports.dbType === 'dirty') {
|
if (exports.dbType === 'dirty') {
|
||||||
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
||||||
|
|
|
@ -1,30 +1,34 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
const request = require('request');
|
const axios = require('axios');
|
||||||
|
|
||||||
let infos;
|
let infos;
|
||||||
|
|
||||||
const loadEtherpadInformations = () => new Promise((resolve, reject) => {
|
const loadEtherpadInformations = () =>
|
||||||
request('https://static.etherpad.org/info.json', (er, response, body) => {
|
axios.get('https://static.etherpad.org/info.json')
|
||||||
if (er) return reject(er);
|
.then(async resp => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
infos = JSON.parse(body);
|
infos = await resp.data;
|
||||||
return resolve(infos);
|
if (infos === undefined || infos === null) {
|
||||||
} catch (err) {
|
await Promise.reject("Could not retrieve current version")
|
||||||
return reject(err);
|
return
|
||||||
}
|
}
|
||||||
});
|
return await Promise.resolve(infos);
|
||||||
});
|
}
|
||||||
|
catch (err) {
|
||||||
|
return await Promise.reject(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.getLatestVersion = () => {
|
exports.getLatestVersion = () => {
|
||||||
exports.needsUpdate();
|
exports.needsUpdate();
|
||||||
return infos.latestVersion;
|
return infos.latestVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.needsUpdate = (cb) => {
|
exports.needsUpdate = async (cb) => {
|
||||||
loadEtherpadInformations().then((info) => {
|
await loadEtherpadInformations()
|
||||||
|
.then((info) => {
|
||||||
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
||||||
if (cb) return cb(true);
|
if (cb) return cb(true);
|
||||||
}
|
}
|
||||||
|
|
5034
src/package-lock.json
generated
5034
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -31,6 +31,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"clean-css": "^5.3.2",
|
"clean-css": "^5.3.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
|
@ -38,11 +39,11 @@
|
||||||
"etherpad-require-kernel": "^1.0.15",
|
"etherpad-require-kernel": "^1.0.15",
|
||||||
"etherpad-yajsml": "0.0.12",
|
"etherpad-yajsml": "0.0.12",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.9.0",
|
||||||
"express-session": "npm:@etherpad/express-session@^1.18.1",
|
"express-session": "npm:@etherpad/express-session@^1.18.2",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "^2.1.2",
|
"formidable": "^3.5.0",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsdom": "^20.0.0",
|
"jsdom": "^20.0.0",
|
||||||
|
@ -52,22 +53,21 @@
|
||||||
"log4js": "0.6.38",
|
"log4js": "0.6.38",
|
||||||
"measured-core": "^2.0.0",
|
"measured-core": "^2.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"npm": "^6.14.15",
|
"npm": "^6.14.18",
|
||||||
"openapi-backend": "^5.9.2",
|
"openapi-backend": "^5.9.2",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"rate-limiter-flexible": "^2.4.1",
|
"rate-limiter-flexible": "^2.4.2",
|
||||||
"rehype": "^12.0.1",
|
"rehype": "^12.0.1",
|
||||||
"rehype-minify-whitespace": "^5.0.1",
|
"rehype-minify-whitespace": "^5.0.1",
|
||||||
"request": "2.88.2",
|
"resolve": "1.22.4",
|
||||||
"resolve": "1.22.2",
|
|
||||||
"security": "1.0.0",
|
"security": "1.0.0",
|
||||||
"semver": "^7.5.3",
|
"semver": "^7.5.4",
|
||||||
"socket.io": "^2.4.1",
|
"socket.io": "^2.5.0",
|
||||||
"superagent": "^8.0.9",
|
"superagent": "^8.1.1",
|
||||||
"terser": "^5.18.1",
|
"terser": "^5.19.2",
|
||||||
"threads": "^1.7.0",
|
"threads": "^1.7.0",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"ueberdb2": "^4.0.1",
|
"ueberdb2": "^4.1.20",
|
||||||
"underscore": "1.13.6",
|
"underscore": "1.13.6",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.9.1"
|
"wtfnode": "^0.9.1"
|
||||||
|
@ -78,14 +78,14 @@
|
||||||
"etherpad-lite": "node/server.js"
|
"etherpad-lite": "node/server.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-config-etherpad": "^3.0.13",
|
"eslint-config-etherpad": "^3.0.21",
|
||||||
"etherpad-cli-client": "^2.0.1",
|
"etherpad-cli-client": "^2.0.2",
|
||||||
"mocha": "^10.0.0",
|
"mocha": "^10.0.0",
|
||||||
"mocha-froth": "^0.2.10",
|
"mocha-froth": "^0.2.10",
|
||||||
"nodeify": "^1.0.1",
|
"nodeify": "^1.0.1",
|
||||||
"openapi-schema-validation": "^0.4.2",
|
"openapi-schema-validation": "^0.4.2",
|
||||||
"selenium-webdriver": "^4.10.0",
|
"selenium-webdriver": "^4.11.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"sinon": "^15.2.0",
|
"sinon": "^15.2.0",
|
||||||
"split-grid": "^1.0.11",
|
"split-grid": "^1.0.11",
|
||||||
|
@ -103,8 +103,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||||
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
"test-container": "mocha --timeout 5000 tests/container/specs/api",
|
||||||
|
"dev": "bash ./bin/run.sh"
|
||||||
},
|
},
|
||||||
"version": "1.9.1",
|
"version": "1.9.2",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2585,17 +2585,17 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const firstEditbarElement = parent.parent.$('#editbar')
|
const firstEditbarElement = parent.parent.$('#editbar')
|
||||||
.children('ul').first().children().first()
|
.children('ul').first().children().first()
|
||||||
.children().first().children().first();
|
.children().first().children().first();
|
||||||
$(this).blur();
|
$(this).trigger('blur');
|
||||||
firstEditbarElement.focus();
|
firstEditbarElement.trigger('focus');
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
if (!specialHandled && type === 'keydown' &&
|
if (!specialHandled && type === 'keydown' &&
|
||||||
altKey && keyCode === 67 &&
|
altKey && keyCode === 67 &&
|
||||||
padShortcutEnabled.altC) {
|
padShortcutEnabled.altC) {
|
||||||
// Alt c focuses on the Chat window
|
// Alt c focuses on the Chat window
|
||||||
$(this).blur();
|
$(this).trigger('blur');
|
||||||
parent.parent.chat.show();
|
parent.parent.chat.show();
|
||||||
parent.parent.$('#chatinput').focus();
|
parent.parent.$('#chatinput').trigger('focus');
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
if (!specialHandled && type === 'keydown' &&
|
if (!specialHandled && type === 'keydown' &&
|
||||||
|
|
|
@ -164,7 +164,7 @@
|
||||||
$(window).resize(adjust);
|
$(window).resize(adjust);
|
||||||
|
|
||||||
// Allow for manual triggering if needed.
|
// Allow for manual triggering if needed.
|
||||||
$ta.bind('autosize', adjust);
|
$ta.on('autosize', adjust);
|
||||||
|
|
||||||
// Call adjust in case the textarea already contains text.
|
// Call adjust in case the textarea already contains text.
|
||||||
adjust();
|
adjust();
|
||||||
|
|
|
@ -112,15 +112,15 @@ $(document).ready(() => {
|
||||||
|
|
||||||
const updateHandlers = () => {
|
const updateHandlers = () => {
|
||||||
// Search
|
// Search
|
||||||
$('#search-query').unbind('keyup').keyup(() => {
|
$('#search-query').off('keyup').on('keyup', () => {
|
||||||
search($('#search-query').val());
|
search($('#search-query').val());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent form submit
|
// Prevent form submit
|
||||||
$('#search-query').parent().bind('submit', () => false);
|
$('#search-query').parent().on('submit', () => false);
|
||||||
|
|
||||||
// update & install
|
// update & install
|
||||||
$('.do-install, .do-update').unbind('click').click(function (e) {
|
$('.do-install, .do-update').off('click').on('click', function (e) {
|
||||||
const $row = $(e.target).closest('tr');
|
const $row = $(e.target).closest('tr');
|
||||||
const plugin = $row.data('plugin');
|
const plugin = $row.data('plugin');
|
||||||
if ($(this).hasClass('do-install')) {
|
if ($(this).hasClass('do-install')) {
|
||||||
|
@ -134,7 +134,7 @@ $(document).ready(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// uninstall
|
// uninstall
|
||||||
$('.do-uninstall').unbind('click').click((e) => {
|
$('.do-uninstall').off('click').on('click', (e) => {
|
||||||
const $row = $(e.target).closest('tr');
|
const $row = $(e.target).closest('tr');
|
||||||
const pluginName = $row.data('plugin');
|
const pluginName = $row.data('plugin');
|
||||||
socket.emit('uninstall', pluginName);
|
socket.emit('uninstall', pluginName);
|
||||||
|
@ -143,14 +143,14 @@ $(document).ready(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
$('.sort.up').unbind('click').click(function () {
|
$('.sort.up').off('click').on('click', function () {
|
||||||
search.sortBy = $(this).attr('data-label').toLowerCase();
|
search.sortBy = $(this).attr('data-label').toLowerCase();
|
||||||
search.sortDir = false;
|
search.sortDir = false;
|
||||||
search.offset = 0;
|
search.offset = 0;
|
||||||
search(search.searchTerm, search.results.length);
|
search(search.searchTerm, search.results.length);
|
||||||
search.results = [];
|
search.results = [];
|
||||||
});
|
});
|
||||||
$('.sort.down, .sort.none').unbind('click').click(function () {
|
$('.sort.down, .sort.none').off('click').on('click', function () {
|
||||||
search.sortBy = $(this).attr('data-label').toLowerCase();
|
search.sortBy = $(this).attr('data-label').toLowerCase();
|
||||||
search.sortDir = true;
|
search.sortDir = true;
|
||||||
search.offset = 0;
|
search.offset = 0;
|
||||||
|
@ -164,7 +164,7 @@ $(document).ready(() => {
|
||||||
if (data.query.offset === 0) search.results = [];
|
if (data.query.offset === 0) search.results = [];
|
||||||
search.messages.hide('nothing-found');
|
search.messages.hide('nothing-found');
|
||||||
search.messages.hide('fetching');
|
search.messages.hide('fetching');
|
||||||
$('#search-query').removeAttr('disabled');
|
$('#search-query').prop('disabled', false);
|
||||||
|
|
||||||
console.log('got search results', data);
|
console.log('got search results', data);
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ $(document).ready(() => {
|
||||||
/* Check to make sure the JSON is clean before proceeding */
|
/* Check to make sure the JSON is clean before proceeding */
|
||||||
if (isJSONClean(settings.results)) {
|
if (isJSONClean(settings.results)) {
|
||||||
$('.settings').append(settings.results);
|
$('.settings').append(settings.results);
|
||||||
$('.settings').focus();
|
$('.settings').trigger('focus');
|
||||||
$('.settings').autosize();
|
$('.settings').autosize();
|
||||||
} else {
|
} else {
|
||||||
alert('Invalid JSON');
|
alert('Invalid JSON');
|
||||||
|
@ -40,7 +40,7 @@ $(document).ready(() => {
|
||||||
socket.emit('saveSettings', $('.settings').val());
|
socket.emit('saveSettings', $('.settings').val());
|
||||||
} else {
|
} else {
|
||||||
alert('Invalid JSON');
|
alert('Invalid JSON');
|
||||||
$('.settings').focus();
|
$('.settings').trigger('focus');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ const isJSONClean = (data) => {
|
||||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||||
try {
|
try {
|
||||||
return typeof jQuery.parseJSON(cleanSettings) === 'object';
|
return typeof JSON.parse(cleanSettings) === 'object';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false; // the JSON failed to be parsed
|
return false; // the JSON failed to be parsed
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
newSavedRevision.css(
|
newSavedRevision.css(
|
||||||
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
|
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
|
||||||
$('#ui-slider-bar').append(newSavedRevision);
|
$('#ui-slider-bar').append(newSavedRevision);
|
||||||
newSavedRevision.mouseup((evt) => {
|
newSavedRevision.on('mouseup', (evt) => {
|
||||||
BroadcastSlider.setSliderPosition(position);
|
BroadcastSlider.setSliderPosition(position);
|
||||||
});
|
});
|
||||||
savedRevisions.push(newSavedRevision);
|
savedRevisions.push(newSavedRevision);
|
||||||
|
@ -209,21 +209,21 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
|
|
||||||
// assign event handlers to html UI elements after page load
|
// assign event handlers to html UI elements after page load
|
||||||
fireWhenAllScriptsAreLoaded.push(() => {
|
fireWhenAllScriptsAreLoaded.push(() => {
|
||||||
$(document).keyup((e) => {
|
$(document).on('keyup', (e) => {
|
||||||
if (!e) e = window.event;
|
if (!e) e = window.event;
|
||||||
const code = e.keyCode || e.which;
|
const code = e.keyCode || e.which;
|
||||||
|
|
||||||
if (code === 37) { // left
|
if (code === 37) { // left
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
$('#leftstar').click();
|
$('#leftstar').trigger('click');
|
||||||
} else {
|
} else {
|
||||||
$('#leftstep').click();
|
$('#leftstep').trigger('click');
|
||||||
}
|
}
|
||||||
} else if (code === 39) { // right
|
} else if (code === 39) { // right
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
$('#rightstar').click();
|
$('#rightstar').trigger('click');
|
||||||
} else {
|
} else {
|
||||||
$('#rightstep').click();
|
$('#rightstep').trigger('click');
|
||||||
}
|
}
|
||||||
} else if (code === 32) { // spacebar
|
} else if (code === 32) { // spacebar
|
||||||
$('#playpause_button_icon').trigger('click');
|
$('#playpause_button_icon').trigger('click');
|
||||||
|
@ -231,22 +231,22 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
$(window).resize(() => {
|
$(window).on('resize', () => {
|
||||||
updateSliderElements();
|
updateSliderElements();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Slider click
|
// Slider click
|
||||||
$('#ui-slider-bar').mousedown((evt) => {
|
$('#ui-slider-bar').on('mousedown', (evt) => {
|
||||||
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left));
|
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left));
|
||||||
$('#ui-slider-handle').trigger(evt);
|
$('#ui-slider-handle').trigger(evt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Slider dragging
|
// Slider dragging
|
||||||
$('#ui-slider-handle').mousedown(function (evt) {
|
$('#ui-slider-handle').on('mousedown', function (evt) {
|
||||||
this.startLoc = evt.clientX;
|
this.startLoc = evt.clientX;
|
||||||
this.currentLoc = parseInt($(this).css('left'));
|
this.currentLoc = parseInt($(this).css('left'));
|
||||||
sliderActive = true;
|
sliderActive = true;
|
||||||
$(document).mousemove((evt2) => {
|
$(document).on('mousemove', (evt2) => {
|
||||||
$(this).css('pointer', 'move');
|
$(this).css('pointer', 'move');
|
||||||
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
||||||
if (newloc < 0) newloc = 0;
|
if (newloc < 0) newloc = 0;
|
||||||
|
@ -257,9 +257,9 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
$(this).css('left', newloc);
|
$(this).css('left', newloc);
|
||||||
if (getSliderPosition() !== version) _callSliderCallbacks(version);
|
if (getSliderPosition() !== version) _callSliderCallbacks(version);
|
||||||
});
|
});
|
||||||
$(document).mouseup((evt2) => {
|
$(document).on('mouseup', (evt2) => {
|
||||||
$(document).unbind('mousemove');
|
$(document).off('mousemove');
|
||||||
$(document).unbind('mouseup');
|
$(document).off('mouseup');
|
||||||
sliderActive = false;
|
sliderActive = false;
|
||||||
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
||||||
if (newloc < 0) newloc = 0;
|
if (newloc < 0) newloc = 0;
|
||||||
|
@ -276,12 +276,12 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// play/pause toggling
|
// play/pause toggling
|
||||||
$('#playpause_button_icon').click((evt) => {
|
$('#playpause_button_icon').on('click', (evt) => {
|
||||||
BroadcastSlider.playpause();
|
BroadcastSlider.playpause();
|
||||||
});
|
});
|
||||||
|
|
||||||
// next/prev saved revision and changeset
|
// next/prev saved revision and changeset
|
||||||
$('.stepper').click(function (evt) {
|
$('.stepper').on('click', function (evt) {
|
||||||
switch ($(this).attr('id')) {
|
switch ($(this).attr('id')) {
|
||||||
case 'leftstep':
|
case 'leftstep':
|
||||||
setSliderPosition(getSliderPosition() - 1);
|
setSliderPosition(getSliderPosition() - 1);
|
||||||
|
|
|
@ -42,11 +42,14 @@ exports.chat = (() => {
|
||||||
},
|
},
|
||||||
focus: () => {
|
focus: () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$('#chatinput').focus();
|
$('#chatinput').trigger('focus');
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
// Make chat stick to right hand side of screen
|
// Make chat stick to right hand side of screen
|
||||||
stickToScreen(fromInitialCall) {
|
stickToScreen(fromInitialCall) {
|
||||||
|
if ($('#options-stickychat').prop('checked')) {
|
||||||
|
$('#options-stickychat').prop('checked', false);
|
||||||
|
}
|
||||||
if (pad.settings.hideChat) {
|
if (pad.settings.hideChat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +71,7 @@ exports.chat = (() => {
|
||||||
this.stickToScreen(true);
|
this.stickToScreen(true);
|
||||||
$('#options-stickychat').prop('checked', true);
|
$('#options-stickychat').prop('checked', true);
|
||||||
$('#options-chatandusers').prop('checked', true);
|
$('#options-chatandusers').prop('checked', true);
|
||||||
$('#options-stickychat').prop('disabled', 'disabled');
|
$('#options-stickychat').prop('disabled', true);
|
||||||
userAndChat = true;
|
userAndChat = true;
|
||||||
} else {
|
} else {
|
||||||
$('#options-stickychat').prop('disabled', false);
|
$('#options-stickychat').prop('disabled', false);
|
||||||
|
@ -223,14 +226,14 @@ exports.chat = (() => {
|
||||||
// Send the users focus back to the pad
|
// Send the users focus back to the pad
|
||||||
if ((evt.altKey === true && evt.which === 67) || evt.which === 27) {
|
if ((evt.altKey === true && evt.which === 67) || evt.which === 27) {
|
||||||
// If we're in chat already..
|
// If we're in chat already..
|
||||||
$(':focus').blur(); // required to do not try to remove!
|
$(':focus').trigger('blur'); // required to do not try to remove!
|
||||||
padeditor.ace.focus(); // Sends focus back to pad
|
padeditor.ace.focus(); // Sends focus back to pad
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Clear the chat mentions when the user clicks on the chat input box
|
// Clear the chat mentions when the user clicks on the chat input box
|
||||||
$('#chatinput').click(() => {
|
$('#chatinput').on('click', () => {
|
||||||
chatMentions = 0;
|
chatMentions = 0;
|
||||||
Tinycon.setBubble(0);
|
Tinycon.setBubble(0);
|
||||||
});
|
});
|
||||||
|
@ -239,14 +242,14 @@ exports.chat = (() => {
|
||||||
$('body:not(#chatinput)').on('keypress', function (evt) {
|
$('body:not(#chatinput)').on('keypress', function (evt) {
|
||||||
if (evt.altKey && evt.which === 67) {
|
if (evt.altKey && evt.which === 67) {
|
||||||
// Alt c focuses on the Chat window
|
// Alt c focuses on the Chat window
|
||||||
$(this).blur();
|
$(this).trigger('blur');
|
||||||
self.show();
|
self.show();
|
||||||
$('#chatinput').focus();
|
$('#chatinput').trigger('focus');
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#chatinput').keypress((evt) => {
|
$('#chatinput').on('keypress', (evt) => {
|
||||||
// if the user typed enter, fire the send
|
// if the user typed enter, fire the send
|
||||||
if (evt.key === 'Enter' && !evt.shiftKey) {
|
if (evt.key === 'Enter' && !evt.shiftKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
@ -257,7 +260,7 @@ exports.chat = (() => {
|
||||||
// initial messages are loaded in pad.js' _afterHandshake
|
// initial messages are loaded in pad.js' _afterHandshake
|
||||||
|
|
||||||
$('#chatcounter').text(0);
|
$('#chatcounter').text(0);
|
||||||
$('#chatloadmessagesbutton').click(() => {
|
$('#chatloadmessagesbutton').on('click', () => {
|
||||||
const start = Math.max(this.historyPointer - 20, 0);
|
const start = Math.max(this.historyPointer - 20, 0);
|
||||||
const end = this.historyPointer;
|
const end = this.historyPointer;
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
|
||||||
if (browser.firefox) {
|
if (browser.firefox) {
|
||||||
// Prevent "escape" from taking effect and canceling a comet connection;
|
// Prevent "escape" from taking effect and canceling a comet connection;
|
||||||
// doesn't work if focus is on an iframe.
|
// doesn't work if focus is on an iframe.
|
||||||
$(window).bind('keydown', (evt) => {
|
$(window).on('keydown', (evt) => {
|
||||||
if (evt.which === 27) {
|
if (evt.which === 27) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ const randomPadName = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$('#go2Name').submit(() => {
|
$('#go2Name').on('submit', () => {
|
||||||
const padname = $('#padname').val();
|
const padname = $('#padname').val();
|
||||||
if (padname.length > 0) {
|
if (padname.length > 0) {
|
||||||
window.location = `p/${encodeURIComponent(padname.trim())}`;
|
window.location = `p/${encodeURIComponent(padname.trim())}`;
|
||||||
|
@ -51,7 +51,7 @@ $(() => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#button').click(() => {
|
$('#button').on('click', () => {
|
||||||
window.location = `p/${randomPadName()}`;
|
window.location = `p/${randomPadName()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -412,10 +412,12 @@ const pad = {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
padeditor.ace.focus();
|
padeditor.ace.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
const optionsStickyChat = $('#options-stickychat');
|
||||||
|
optionsStickyChat.on('click', () => { chat.stickToScreen(); });
|
||||||
// if we have a cookie for always showing chat then show it
|
// if we have a cookie for always showing chat then show it
|
||||||
if (padcookie.getPref('chatAlwaysVisible')) {
|
if (padcookie.getPref('chatAlwaysVisible')) {
|
||||||
chat.stickToScreen(true); // stick it to the screen
|
chat.stickToScreen(true); // stick it to the screen
|
||||||
$('#options-stickychat').prop('checked', true); // set the checkbox to on
|
optionsStickyChat.prop('checked', true); // set the checkbox to on
|
||||||
}
|
}
|
||||||
// if we have a cookie for always showing chat then show it
|
// if we have a cookie for always showing chat then show it
|
||||||
if (padcookie.getPref('chatAndUsers')) {
|
if (padcookie.getPref('chatAndUsers')) {
|
||||||
|
@ -437,8 +439,8 @@ const pad = {
|
||||||
// Prevent sticky chat or chat and users to be checked for mobiles
|
// Prevent sticky chat or chat and users to be checked for mobiles
|
||||||
const checkChatAndUsersVisibility = (x) => {
|
const checkChatAndUsersVisibility = (x) => {
|
||||||
if (x.matches) { // If media query matches
|
if (x.matches) { // If media query matches
|
||||||
$('#options-chatandusers:checked').click();
|
$('#options-chatandusers:checked').trigger('click');
|
||||||
$('#options-stickychat:checked').click();
|
$('#options-stickychat:checked').trigger('click');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const mobileMatch = window.matchMedia('(max-width: 800px)');
|
const mobileMatch = window.matchMedia('(max-width: 800px)');
|
||||||
|
@ -711,7 +713,7 @@ const pad = {
|
||||||
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
|
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
|
||||||
$('form#reconnectform input.missedChanges')
|
$('form#reconnectform input.missedChanges')
|
||||||
.val(JSON.stringify(pad.collabClient.getMissedChanges()));
|
.val(JSON.stringify(pad.collabClient.getMissedChanges()));
|
||||||
$('form#reconnectform').submit();
|
$('form#reconnectform').trigger('submit');
|
||||||
},
|
},
|
||||||
callWhenNotCommitting: (f) => {
|
callWhenNotCommitting: (f) => {
|
||||||
pad.collabClient.callWhenNotCommitting(f);
|
pad.collabClient.callWhenNotCommitting(f);
|
||||||
|
|
|
@ -96,7 +96,7 @@ const whenConnectionIsRestablishedWithServer = (callback, pad) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const forceReconnection = ($modal) => {
|
const forceReconnection = ($modal) => {
|
||||||
$modal.find('#forcereconnect').click();
|
$modal.find('#forcereconnect').trigger('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCountDownTimerMessage = ($modal, minutes, seconds) => {
|
const updateCountDownTimerMessage = ($modal, minutes, seconds) => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ const padconnectionstatus = (() => {
|
||||||
|
|
||||||
const self = {
|
const self = {
|
||||||
init: () => {
|
init: () => {
|
||||||
$('button#forcereconnect').click(() => {
|
$('button#forcereconnect').on('click', () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -65,13 +65,13 @@ class ToolbarItem {
|
||||||
|
|
||||||
bind(callback) {
|
bind(callback) {
|
||||||
if (this.isButton()) {
|
if (this.isButton()) {
|
||||||
this.$el.click((event) => {
|
this.$el.on('click', (event) => {
|
||||||
$(':focus').blur();
|
$(':focus').trigger('blur');
|
||||||
callback(this.getCommand(), this);
|
callback(this.getCommand(), this);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
} else if (this.isSelect()) {
|
} else if (this.isSelect()) {
|
||||||
this.$el.find('select').change(() => {
|
this.$el.find('select').on('change', () => {
|
||||||
callback(this.getCommand(), this);
|
callback(this.getCommand(), this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ exports.padeditbar = new class {
|
||||||
$('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE
|
$('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE
|
||||||
this.enable();
|
this.enable();
|
||||||
$('#editbar [data-key]').each((i, elt) => {
|
$('#editbar [data-key]').each((i, elt) => {
|
||||||
$(elt).unbind('click');
|
$(elt).off('click');
|
||||||
new ToolbarItem($(elt)).bind((command, item) => {
|
new ToolbarItem($(elt)).bind((command, item) => {
|
||||||
this.triggerCommand(command, item);
|
this.triggerCommand(command, item);
|
||||||
});
|
});
|
||||||
|
@ -144,11 +144,11 @@ exports.padeditbar = new class {
|
||||||
this._bodyKeyEvent(evt);
|
this._bodyKeyEvent(evt);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.show-more-icon-btn').click(() => {
|
$('.show-more-icon-btn').on('click', () => {
|
||||||
$('.toolbar').toggleClass('full-icons');
|
$('.toolbar').toggleClass('full-icons');
|
||||||
});
|
});
|
||||||
this.checkAllIconsAreDisplayedInToolbar();
|
this.checkAllIconsAreDisplayedInToolbar();
|
||||||
$(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100));
|
$(window).on('resize', _.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100));
|
||||||
|
|
||||||
this._registerDefaultCommands();
|
this._registerDefaultCommands();
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@ exports.padeditbar = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
// When editor is scrolled, we add a class to style the editbar differently
|
// When editor is scrolled, we add a class to style the editbar differently
|
||||||
$('iframe[name="ace_outer"]').contents().scroll((ev) => {
|
$('iframe[name="ace_outer"]').contents().on('scroll', (ev) => {
|
||||||
$('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2);
|
$('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -305,12 +305,12 @@ exports.padeditbar = new class {
|
||||||
// Close any dropdowns we have open..
|
// Close any dropdowns we have open..
|
||||||
this.toggleDropDown('none');
|
this.toggleDropDown('none');
|
||||||
// Shift focus away from any drop downs
|
// Shift focus away from any drop downs
|
||||||
$(':focus').blur(); // required to do not try to remove!
|
$(':focus').trigger('blur'); // required to do not try to remove!
|
||||||
// Check we're on a pad and not on the timeslider
|
// Check we're on a pad and not on the timeslider
|
||||||
// Or some other window I haven't thought about!
|
// Or some other window I haven't thought about!
|
||||||
if (typeof pad === 'undefined') {
|
if (typeof pad === 'undefined') {
|
||||||
// Timeslider probably..
|
// Timeslider probably..
|
||||||
$('#editorcontainerbox').focus(); // Focus back onto the pad
|
$('#editorcontainerbox').trigger('focus'); // Focus back onto the pad
|
||||||
} else {
|
} else {
|
||||||
padeditor.ace.focus(); // Sends focus back to pad
|
padeditor.ace.focus(); // Sends focus back to pad
|
||||||
// The above focus doesn't always work in FF, you have to hit enter afterwards
|
// The above focus doesn't always work in FF, you have to hit enter afterwards
|
||||||
|
@ -320,8 +320,8 @@ exports.padeditbar = new class {
|
||||||
// Focus on the editbar :)
|
// Focus on the editbar :)
|
||||||
const firstEditbarElement = parent.parent.$('#editbar button').first();
|
const firstEditbarElement = parent.parent.$('#editbar button').first();
|
||||||
|
|
||||||
$(evt.currentTarget).blur();
|
$(evt.currentTarget).trigger('blur');
|
||||||
firstEditbarElement.focus();
|
firstEditbarElement.trigger('focus');
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,7 +341,7 @@ exports.padeditbar = new class {
|
||||||
this._editbarPosition--;
|
this._editbarPosition--;
|
||||||
// Allow focus to shift back to end of row and start of row
|
// Allow focus to shift back to end of row and start of row
|
||||||
if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1;
|
if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1;
|
||||||
$(focusItems[this._editbarPosition]).focus();
|
$(focusItems[this._editbarPosition]).trigger('focus');
|
||||||
}
|
}
|
||||||
|
|
||||||
// On right arrow move to next button in editbar
|
// On right arrow move to next button in editbar
|
||||||
|
@ -352,7 +352,7 @@ exports.padeditbar = new class {
|
||||||
this._editbarPosition++;
|
this._editbarPosition++;
|
||||||
// Allow focus to shift back to end of row and start of row
|
// Allow focus to shift back to end of row and start of row
|
||||||
if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0;
|
if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0;
|
||||||
$(focusItems[this._editbarPosition]).focus();
|
$(focusItems[this._editbarPosition]).trigger('focus');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,7 +366,7 @@ exports.padeditbar = new class {
|
||||||
|
|
||||||
this.registerCommand('settings', () => {
|
this.registerCommand('settings', () => {
|
||||||
this.toggleDropDown('settings');
|
this.toggleDropDown('settings');
|
||||||
$('#options-stickychat').focus();
|
$('#options-stickychat').trigger('focus');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerCommand('import_export', () => {
|
this.registerCommand('import_export', () => {
|
||||||
|
@ -374,22 +374,22 @@ exports.padeditbar = new class {
|
||||||
// If Import file input exists then focus on it..
|
// If Import file input exists then focus on it..
|
||||||
if ($('#importfileinput').length !== 0) {
|
if ($('#importfileinput').length !== 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$('#importfileinput').focus();
|
$('#importfileinput').trigger('focus');
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
$('.exportlink').first().focus();
|
$('.exportlink').first().trigger('focus');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerCommand('showusers', () => {
|
this.registerCommand('showusers', () => {
|
||||||
this.toggleDropDown('users');
|
this.toggleDropDown('users');
|
||||||
$('#myusernameedit').focus();
|
$('#myusernameedit').trigger('focus');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerCommand('embed', () => {
|
this.registerCommand('embed', () => {
|
||||||
this.setEmbedLinks();
|
this.setEmbedLinks();
|
||||||
this.toggleDropDown('embed');
|
this.toggleDropDown('embed');
|
||||||
$('#linkinput').focus().select();
|
$('#linkinput').trigger('focus').trigger('select');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerCommand('savedRevision', () => {
|
this.registerCommand('savedRevision', () => {
|
||||||
|
|
|
@ -76,7 +76,7 @@ const padeditor = (() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// font family change
|
// font family change
|
||||||
$('#viewfontmenu').change(() => {
|
$('#viewfontmenu').on('change', () => {
|
||||||
pad.changeViewOption('padFontFamily', $('#viewfontmenu').val());
|
pad.changeViewOption('padFontFamily', $('#viewfontmenu').val());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ const padeditor = (() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$('#languagemenu').val(html10n.getLanguage());
|
$('#languagemenu').val(html10n.getLanguage());
|
||||||
$('#languagemenu').change(() => {
|
$('#languagemenu').on('change', () => {
|
||||||
Cookies.set('language', $('#languagemenu').val());
|
Cookies.set('language', $('#languagemenu').val());
|
||||||
window.html10n.localize([$('#languagemenu').val(), 'en']);
|
window.html10n.localize([$('#languagemenu').val(), 'en']);
|
||||||
if ($('select').niceSelect) {
|
if ($('select').niceSelect) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ const padimpexp = (() => {
|
||||||
const fileInputUpdated = () => {
|
const fileInputUpdated = () => {
|
||||||
$('#importsubmitinput').addClass('throbbold');
|
$('#importsubmitinput').addClass('throbbold');
|
||||||
$('#importformfilediv').addClass('importformenabled');
|
$('#importformfilediv').addClass('importformenabled');
|
||||||
$('#importsubmitinput').removeAttr('disabled');
|
$('#importsubmitinput').prop('disabled', false);
|
||||||
$('#importmessagefail').fadeOut('fast');
|
$('#importmessagefail').fadeOut('fast');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,8 +69,8 @@ const padimpexp = (() => {
|
||||||
$('#import_export').removeClass('popup-show');
|
$('#import_export').removeClass('popup-show');
|
||||||
if (directDatabaseAccess) window.location.reload();
|
if (directDatabaseAccess) window.location.reload();
|
||||||
}
|
}
|
||||||
$('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton'));
|
$('#importsubmitinput').prop('disabled', false).val(html10n.get('pad.impexp.importbutton'));
|
||||||
window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0);
|
window.setTimeout(() => $('#importfileinput').prop('disabled', false), 0);
|
||||||
$('#importstatusball').hide();
|
$('#importstatusball').hide();
|
||||||
addImportFrames();
|
addImportFrames();
|
||||||
})();
|
})();
|
||||||
|
@ -162,9 +162,9 @@ const padimpexp = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
addImportFrames();
|
addImportFrames();
|
||||||
$('#importfileinput').change(fileInputUpdated);
|
$('#importfileinput').on('change', fileInputUpdated);
|
||||||
$('#importform').unbind('submit').submit(fileInputSubmit);
|
$('#importform').off('submit').on('submit', fileInputSubmit);
|
||||||
$('.disabledexport').click(cantExport);
|
$('.disabledexport').on('click', cantExport);
|
||||||
},
|
},
|
||||||
disable: () => {
|
disable: () => {
|
||||||
$('#impexp-disabled-clickcatcher').show();
|
$('#impexp-disabled-clickcatcher').show();
|
||||||
|
|
|
@ -325,23 +325,23 @@ const paduserlist = (() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUpEditable = (jqueryNode, valueGetter, valueSetter) => {
|
const setUpEditable = (jqueryNode, valueGetter, valueSetter) => {
|
||||||
jqueryNode.bind('focus', (evt) => {
|
jqueryNode.on('focus', (evt) => {
|
||||||
const oldValue = valueGetter();
|
const oldValue = valueGetter();
|
||||||
if (jqueryNode.val() !== oldValue) {
|
if (jqueryNode.val() !== oldValue) {
|
||||||
jqueryNode.val(oldValue);
|
jqueryNode.val(oldValue);
|
||||||
}
|
}
|
||||||
jqueryNode.addClass('editactive').removeClass('editempty');
|
jqueryNode.addClass('editactive').removeClass('editempty');
|
||||||
});
|
});
|
||||||
jqueryNode.bind('blur', (evt) => {
|
jqueryNode.on('blur', (evt) => {
|
||||||
const newValue = jqueryNode.removeClass('editactive').val();
|
const newValue = jqueryNode.removeClass('editactive').val();
|
||||||
valueSetter(newValue);
|
valueSetter(newValue);
|
||||||
});
|
});
|
||||||
padutils.bindEnterAndEscape(jqueryNode, () => {
|
padutils.bindEnterAndEscape(jqueryNode, () => {
|
||||||
jqueryNode.blur();
|
jqueryNode.trigger('blur');
|
||||||
}, () => {
|
}, () => {
|
||||||
jqueryNode.val(valueGetter()).blur();
|
jqueryNode.val(valueGetter()).trigger('blur');
|
||||||
});
|
});
|
||||||
jqueryNode.removeAttr('disabled').addClass('editable');
|
jqueryNode.prop('disabled', false).addClass('editable');
|
||||||
};
|
};
|
||||||
|
|
||||||
let pad = undefined;
|
let pad = undefined;
|
||||||
|
@ -369,15 +369,15 @@ const paduserlist = (() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// color picker
|
// color picker
|
||||||
$('#myswatchbox').click(showColorPicker);
|
$('#myswatchbox').on('click', showColorPicker);
|
||||||
$('#mycolorpicker .pickerswatchouter').click(function () {
|
$('#mycolorpicker .pickerswatchouter').on('click', function () {
|
||||||
$('#mycolorpicker .pickerswatchouter').removeClass('picked');
|
$('#mycolorpicker .pickerswatchouter').removeClass('picked');
|
||||||
$(this).addClass('picked');
|
$(this).addClass('picked');
|
||||||
});
|
});
|
||||||
$('#mycolorpickersave').click(() => {
|
$('#mycolorpickersave').on('click', () => {
|
||||||
closeColorPicker(true);
|
closeColorPicker(true);
|
||||||
});
|
});
|
||||||
$('#mycolorpickercancel').click(() => {
|
$('#mycolorpickercancel').on('click', () => {
|
||||||
closeColorPicker(false);
|
closeColorPicker(false);
|
||||||
});
|
});
|
||||||
//
|
//
|
||||||
|
@ -587,7 +587,7 @@ const showColorPicker = () => {
|
||||||
|
|
||||||
li.appendTo(colorsList);
|
li.appendTo(colorsList);
|
||||||
|
|
||||||
li.bind('click', (event) => {
|
li.on('click', (event) => {
|
||||||
$('#colorpickerswatches li').removeClass('picked');
|
$('#colorpickerswatches li').removeClass('picked');
|
||||||
$(event.target).addClass('picked');
|
$(event.target).addClass('picked');
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ const padutils = {
|
||||||
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
||||||
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
||||||
if (onEnter) {
|
if (onEnter) {
|
||||||
node.keypress((evt) => {
|
node.on('keypress', (evt) => {
|
||||||
if (evt.which === 13) {
|
if (evt.which === 13) {
|
||||||
onEnter(evt);
|
onEnter(evt);
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ const padutils = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onEscape) {
|
if (onEscape) {
|
||||||
node.keydown((evt) => {
|
node.on('keydown', (evt) => {
|
||||||
if (evt.which === 27) {
|
if (evt.which === 27) {
|
||||||
onEscape(evt);
|
onEscape(evt);
|
||||||
}
|
}
|
||||||
|
@ -299,7 +299,7 @@ const padutils = {
|
||||||
}
|
}
|
||||||
field.removeClass('editempty');
|
field.removeClass('editempty');
|
||||||
});
|
});
|
||||||
field.blur(() => {
|
field.on('blur', () => {
|
||||||
if (!field.val()) {
|
if (!field.val()) {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
@ -313,11 +313,11 @@ const padutils = {
|
||||||
if (value) {
|
if (value) {
|
||||||
$(node).attr('checked', 'checked');
|
$(node).attr('checked', 'checked');
|
||||||
} else {
|
} else {
|
||||||
$(node).removeAttr('checked');
|
$(node).prop('checked', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bindCheckboxChange: (node, func) => {
|
bindCheckboxChange: (node, func) => {
|
||||||
$(node).change(func);
|
$(node).on('change', func);
|
||||||
},
|
},
|
||||||
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
|
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||||
if (c === '.') return '-';
|
if (c === '.') return '-';
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
const plugins = require('./plugins');
|
const plugins = require('./plugins');
|
||||||
const hooks = require('./hooks');
|
const hooks = require('./hooks');
|
||||||
const request = require('request');
|
|
||||||
const runCmd = require('../../../node/utils/run_cmd');
|
const runCmd = require('../../../node/utils/run_cmd');
|
||||||
const settings = require('../../../node/utils/Settings');
|
const settings = require('../../../node/utils/Settings');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
const logger = log4js.getLogger('plugins');
|
const logger = log4js.getLogger('plugins');
|
||||||
|
|
||||||
|
@ -71,28 +71,20 @@ let cacheTimestamp = 0;
|
||||||
exports.getAvailablePlugins = (maxCacheAge) => {
|
exports.getAvailablePlugins = (maxCacheAge) => {
|
||||||
const nowTimestamp = Math.round(Date.now() / 1000);
|
const nowTimestamp = Math.round(Date.now() / 1000);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
// check cache age before making any request
|
// check cache age before making any request
|
||||||
if (exports.availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) {
|
if (exports.availablePlugins && maxCacheAge && (nowTimestamp - cacheTimestamp) <= maxCacheAge) {
|
||||||
return resolve(exports.availablePlugins);
|
return resolve(exports.availablePlugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
request('https://static.etherpad.org/plugins.json', (er, response, plugins) => {
|
await axios.get('https://static.etherpad.org/plugins.json')
|
||||||
if (er) return reject(er);
|
.then(pluginsLoaded => {
|
||||||
|
exports.availablePlugins = pluginsLoaded.data;
|
||||||
try {
|
|
||||||
plugins = JSON.parse(plugins);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`error parsing plugins.json: ${err.stack || err}`);
|
|
||||||
plugins = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.availablePlugins = plugins;
|
|
||||||
cacheTimestamp = nowTimestamp;
|
cacheTimestamp = nowTimestamp;
|
||||||
resolve(plugins);
|
resolve(exports.availablePlugins);
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
|
exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
|
||||||
|
|
|
@ -46,7 +46,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
||||||
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
|
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
|
||||||
};
|
};
|
||||||
|
|
||||||
$('.skin-variant').change(() => {
|
$('.skin-variant').on('change', () => {
|
||||||
updateSkinVariantsClasses();
|
updateSkinVariantsClasses();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ const init = () => {
|
||||||
// get all the export links
|
// get all the export links
|
||||||
exportLinks = $('#export > .exportlink');
|
exportLinks = $('#export > .exportlink');
|
||||||
|
|
||||||
$('button#forcereconnect').click(() => {
|
$('button#forcereconnect').on('click', () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ const handleClientVars = (message) => {
|
||||||
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
|
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
|
||||||
|
|
||||||
// font family change
|
// font family change
|
||||||
$('#viewfontmenu').change(function () {
|
$('#viewfontmenu').on('change', function () {
|
||||||
$('#innerdocbody').css('font-family', $(this).val() || '');
|
$('#innerdocbody').css('font-family', $(this).val() || '');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
12
src/static/js/vendors/farbtastic.js
vendored
12
src/static/js/vendors/farbtastic.js
vendored
|
@ -33,7 +33,7 @@ $._farbtastic = function (container, options) {
|
||||||
fb.linkTo = function (callback) {
|
fb.linkTo = function (callback) {
|
||||||
// Unbind previous nodes
|
// Unbind previous nodes
|
||||||
if (typeof fb.callback == 'object') {
|
if (typeof fb.callback == 'object') {
|
||||||
$(fb.callback).unbind('keyup', fb.updateValue);
|
$(fb.callback).off('keyup').on('keyup', fb.updateValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset color
|
// Reset color
|
||||||
|
@ -45,7 +45,7 @@ $._farbtastic = function (container, options) {
|
||||||
}
|
}
|
||||||
else if (typeof callback == 'object' || typeof callback == 'string') {
|
else if (typeof callback == 'object' || typeof callback == 'string') {
|
||||||
fb.callback = $(callback);
|
fb.callback = $(callback);
|
||||||
fb.callback.bind('keyup', fb.updateValue);
|
fb.callback.on('keyup', fb.updateValue);
|
||||||
if (fb.callback[0].value) {
|
if (fb.callback[0].value) {
|
||||||
fb.setColor(fb.callback[0].value);
|
fb.setColor(fb.callback[0].value);
|
||||||
}
|
}
|
||||||
|
@ -388,7 +388,7 @@ $._farbtastic = function (container, options) {
|
||||||
fb.mousedown = function (event) {
|
fb.mousedown = function (event) {
|
||||||
// Capture mouse
|
// Capture mouse
|
||||||
if (!$._farbtastic.dragging) {
|
if (!$._farbtastic.dragging) {
|
||||||
$(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
|
$(document).on('mousemove', fb.mousemove).on('mouseup', fb.mouseup);
|
||||||
$._farbtastic.dragging = true;
|
$._farbtastic.dragging = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,8 +429,8 @@ $._farbtastic = function (container, options) {
|
||||||
*/
|
*/
|
||||||
fb.mouseup = function () {
|
fb.mouseup = function () {
|
||||||
// Uncapture mouse
|
// Uncapture mouse
|
||||||
$(document).unbind('mousemove', fb.mousemove);
|
$(document).off('mousemove', fb.mousemove);
|
||||||
$(document).unbind('mouseup', fb.mouseup);
|
$(document).off('mouseup', fb.mouseup);
|
||||||
$._farbtastic.dragging = false;
|
$._farbtastic.dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,7 +519,7 @@ $._farbtastic = function (container, options) {
|
||||||
fb.initWidget();
|
fb.initWidget();
|
||||||
|
|
||||||
// Install mousedown handler (the others are set on the document on-demand)
|
// Install mousedown handler (the others are set on the document on-demand)
|
||||||
$('canvas.farbtastic-overlay', container).mousedown(fb.mousedown);
|
$('canvas.farbtastic-overlay', container).on('mousedown',fb.mousedown);
|
||||||
|
|
||||||
// Set linked elements/callback
|
// Set linked elements/callback
|
||||||
if (options.callback) {
|
if (options.callback) {
|
||||||
|
|
5315
src/static/js/vendors/jquery.js
vendored
5315
src/static/js/vendors/jquery.js
vendored
File diff suppressed because it is too large
Load diff
2
src/static/js/vendors/nice-select.js
vendored
2
src/static/js/vendors/nice-select.js
vendored
|
@ -123,7 +123,7 @@
|
||||||
$dropdown.find('.list').css('max-height', $maxListHeight + 'px');
|
$dropdown.find('.list').css('max-height', $maxListHeight + 'px');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$dropdown.focus();
|
$dropdown.trigger('focus');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
window.customStart = () => {
|
window.customStart = () => {
|
||||||
$('#pad_title').show();
|
$('#pad_title').show();
|
||||||
$('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); });
|
$('.buttonicon').on('mousedown', function () { $(this).parent().addClass('pressed'); });
|
||||||
$('.buttonicon').mouseup(function () { $(this).parent().removeClass('pressed'); });
|
$('.buttonicon').on('mouseup', function () { $(this).parent().removeClass('pressed'); });
|
||||||
};
|
};
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
<% e.begin_block("mySettings"); %>
|
<% e.begin_block("mySettings"); %>
|
||||||
<h2 data-l10n-id="pad.settings.myView"></h2>
|
<h2 data-l10n-id="pad.settings.myView"></h2>
|
||||||
<p class="hide-for-mobile">
|
<p class="hide-for-mobile">
|
||||||
<input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();">
|
<input type="checkbox" id="options-stickychat">
|
||||||
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
|
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
|
||||||
</p>
|
</p>
|
||||||
<p class="hide-for-mobile">
|
<p class="hide-for-mobile">
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
*/
|
*/
|
||||||
const common = require('./common');
|
const common = require('./common');
|
||||||
const host = `http://${settings.ip}:${settings.port}`;
|
const host = `http://${settings.ip}:${settings.port}`;
|
||||||
const request = require('request');
|
|
||||||
const froth = require('mocha-froth');
|
const froth = require('mocha-froth');
|
||||||
const settings = require('../container/loadSettings').loadSettings();
|
const settings = require('../container/loadSettings').loadSettings();
|
||||||
|
const axios = require('axios');
|
||||||
const apiKey = common.apiKey;
|
const apiKey = common.apiKey;
|
||||||
const apiVersion = 1;
|
const apiVersion = 1;
|
||||||
const testPadId = `TEST_fuzz${makeid()}`;
|
const testPadId = `TEST_fuzz${makeid()}`;
|
||||||
|
@ -23,22 +22,18 @@ console.log('Tests will start in 5 seconds, click the URL now!');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
for (let i = 1; i < 1000000; i++) { // 1M runs
|
for (let i = 1; i < 1000000; i++) { // 1M runs
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
runTest(i);
|
await runTest(i);
|
||||||
}, i * 100); // 100 ms
|
}, i * 100); // 100 ms
|
||||||
}
|
}
|
||||||
}, 5000); // wait 5 seconds
|
}, 5000); // wait 5 seconds
|
||||||
|
|
||||||
function runTest(number) {
|
async function runTest(number) {
|
||||||
request(`${host + endPoint('createPad')}&padID=${testPadId}`, (err, res, body) => {
|
await axios.get(`${host + endPoint('createPad')}&padID=${testPadId}`)
|
||||||
const req = request.post(`${host}/p/${testPadId}/import`, (err, res, body) => {
|
.then(() => {
|
||||||
if (err) {
|
const req = axios.post(`${host}/p/${testPadId}/import`)
|
||||||
throw new Error('FAILURE', err);
|
.then(() => {
|
||||||
} else {
|
|
||||||
console.log('Success');
|
console.log('Success');
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let fN = '/test.txt';
|
let fN = '/test.txt';
|
||||||
let cT = 'text/plain';
|
let cT = 'text/plain';
|
||||||
|
|
||||||
|
@ -55,6 +50,10 @@ function runTest(number) {
|
||||||
contentType: cT,
|
contentType: cT,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
throw new Error('FAILURE', err);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeid() {
|
function makeid() {
|
||||||
|
|
555
src/tests/backend/specs/SecretRotator.js
Normal file
555
src/tests/backend/specs/SecretRotator.js
Normal file
|
@ -0,0 +1,555 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const SecretRotator = require('../../../node/security/SecretRotator');
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
const common = require('../common');
|
||||||
|
const crypto = require('../../../node/security/crypto');
|
||||||
|
const db = require('../../../node/db/DB');
|
||||||
|
|
||||||
|
const logger = common.logger;
|
||||||
|
|
||||||
|
// Greatest common divisor.
|
||||||
|
const gcd = (...args) => (
|
||||||
|
args.length === 1 ? args[0]
|
||||||
|
: args.length === 2 ? ((args[1]) ? gcd(args[1], args[0] % args[1]) : Math.abs(args[0]))
|
||||||
|
: gcd(args[0], gcd(...args.slice(1))));
|
||||||
|
// Least common multiple.
|
||||||
|
const lcm = (...args) => (
|
||||||
|
args.length === 1 ? args[0]
|
||||||
|
: args.length === 2 ? Math.abs(args[0] * args[1]) / gcd(...args)
|
||||||
|
: lcm(args[0], lcm(...args.slice(1))));
|
||||||
|
|
||||||
|
class FakeClock {
|
||||||
|
constructor() {
|
||||||
|
logger.debug('new fake clock');
|
||||||
|
this._now = 0;
|
||||||
|
this._nextId = 1;
|
||||||
|
this._idle = Promise.resolve();
|
||||||
|
this.timeouts = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
_next() { return Math.min(...[...this.timeouts.values()].map((x) => x.when)); }
|
||||||
|
async setNow(t) {
|
||||||
|
logger.debug(`setting fake time to ${t}`);
|
||||||
|
assert(t >= this._now);
|
||||||
|
assert(t < Infinity);
|
||||||
|
let n;
|
||||||
|
while ((n = this._next()) <= t) {
|
||||||
|
this._now = Math.max(this._now, Math.min(n, t));
|
||||||
|
logger.debug(`fake time set to ${this._now}; firing timeouts...`);
|
||||||
|
await this._fire();
|
||||||
|
}
|
||||||
|
this._now = t;
|
||||||
|
logger.debug(`fake time set to ${this._now}`);
|
||||||
|
}
|
||||||
|
async advance(t) { await this.setNow(this._now + t); }
|
||||||
|
async advanceToNext() {
|
||||||
|
const n = this._next();
|
||||||
|
if (n < this._now) await this._fire();
|
||||||
|
else if (n < Infinity) await this.setNow(n);
|
||||||
|
}
|
||||||
|
async _fire() {
|
||||||
|
// This method MUST NOT execute any of the setTimeout callbacks synchronously, otherwise
|
||||||
|
// fc.setTimeout(fn, 0) would execute fn before fc.setTimeout() returns. Fortunately, the
|
||||||
|
// ECMAScript standard guarantees that a function passed to Promise.prototype.then() will run
|
||||||
|
// asynchronously.
|
||||||
|
this._idle = this._idle.then(() => Promise.all(
|
||||||
|
[...this.timeouts.values()]
|
||||||
|
.filter(({when}) => when <= this._now)
|
||||||
|
.sort((a, b) => a.when - b.when)
|
||||||
|
.map(async ({id, fn}) => {
|
||||||
|
this.clearTimeout(id);
|
||||||
|
// With the standard setTimeout(), the callback function's return value is ignored.
|
||||||
|
// Here we await the return value so that test code can block until timeout work is
|
||||||
|
// done.
|
||||||
|
await fn();
|
||||||
|
})));
|
||||||
|
await this._idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
get now() { return this._now; }
|
||||||
|
setTimeout(fn, wait = 0) {
|
||||||
|
const when = this._now + wait;
|
||||||
|
const id = this._nextId++;
|
||||||
|
this.timeouts.set(id, {id, fn, when});
|
||||||
|
this._fire();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
clearTimeout(id) { this.timeouts.delete(id); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// In JavaScript, the % operator is remainder, not modulus.
|
||||||
|
const mod = (a, n) => ((a % n) + n) % n;
|
||||||
|
|
||||||
|
describe(__filename, function () {
|
||||||
|
let dbPrefix;
|
||||||
|
let sr;
|
||||||
|
let interval = 1e3;
|
||||||
|
const lifetime = 1e4;
|
||||||
|
const intervalStart = (t) => t - mod(t, interval);
|
||||||
|
const hkdf = async (secret, salt, tN) => Buffer.from(
|
||||||
|
await crypto.hkdf('sha256', secret, salt, `${tN}`, 32)).toString('hex');
|
||||||
|
|
||||||
|
const newRotator = (s = null) => new SecretRotator(dbPrefix, interval, lifetime, s);
|
||||||
|
|
||||||
|
const setFakeClock = (sr, fc = null) => {
|
||||||
|
if (fc == null) fc = new FakeClock();
|
||||||
|
sr._t = {
|
||||||
|
now: () => fc.now,
|
||||||
|
setTimeout: fc.setTimeout.bind(fc),
|
||||||
|
clearTimeout: fc.clearTimeout.bind(fc),
|
||||||
|
};
|
||||||
|
return fc;
|
||||||
|
};
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await common.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
dbPrefix = `test-SecretRotator-${common.randomString()}`;
|
||||||
|
interval = 1e3;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
if (sr != null) sr.stop();
|
||||||
|
sr = null;
|
||||||
|
await Promise.all(
|
||||||
|
(await db.findKeys(`${dbPrefix}:*`, null)).map(async (dbKey) => await db.remove(dbKey)));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', function () {
|
||||||
|
it('creates empty secrets array', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
assert.deepEqual(sr.secrets, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const invalidChar of '*:%') {
|
||||||
|
it(`rejects database prefixes containing ${invalidChar}`, async function () {
|
||||||
|
dbPrefix += invalidChar;
|
||||||
|
assert.throws(newRotator, /invalid char/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('start', function () {
|
||||||
|
it('does not replace secrets array', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr);
|
||||||
|
const {secrets} = sr;
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(sr.secrets, secrets);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives secrets', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(sr.secrets.length, 3); // Current (active), previous, and next.
|
||||||
|
for (const s of sr.secrets) {
|
||||||
|
assert.equal(typeof s, 'string');
|
||||||
|
assert(s);
|
||||||
|
}
|
||||||
|
assert.equal(new Set(sr.secrets).size, sr.secrets.length); // The secrets should all differ.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes params', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const dbKeys = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
assert.equal(dbKeys.length, 1);
|
||||||
|
const [id] = dbKeys;
|
||||||
|
assert(id.startsWith(`${dbPrefix}:`));
|
||||||
|
assert.notEqual(id.slice(dbPrefix.length + 1), '');
|
||||||
|
const p = await db.get(id);
|
||||||
|
const {secret, salt} = p.algParams;
|
||||||
|
assert.deepEqual(p, {
|
||||||
|
algId: 1,
|
||||||
|
algParams: {
|
||||||
|
digest: 'sha256',
|
||||||
|
keyLen: 32,
|
||||||
|
salt,
|
||||||
|
secret,
|
||||||
|
},
|
||||||
|
start: fc.now,
|
||||||
|
end: fc.now + (2 * interval),
|
||||||
|
interval,
|
||||||
|
lifetime,
|
||||||
|
});
|
||||||
|
assert.equal(typeof salt, 'string');
|
||||||
|
assert.match(salt, /^[0-9a-f]{64}$/);
|
||||||
|
assert.equal(typeof secret, 'string');
|
||||||
|
assert.match(secret, /^[0-9a-f]{64}$/);
|
||||||
|
assert.deepEqual(sr.secrets, await Promise.all(
|
||||||
|
[0, -interval, interval].map(async (tN) => await hkdf(secret, salt, tN))));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses matching publication if unexpired', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const {secrets} = sr;
|
||||||
|
const dbKeys = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
sr.stop();
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert.deepEqual(sr.secrets, secrets);
|
||||||
|
assert.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes expired publications', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const [oldId] = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
assert(oldId != null);
|
||||||
|
sr.stop();
|
||||||
|
const p = await db.get(oldId);
|
||||||
|
await fc.setNow(p.end + p.lifetime + p.interval);
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
const ids = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
assert.equal(ids.length, 1);
|
||||||
|
const [newId] = ids;
|
||||||
|
assert.notEqual(newId, oldId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps expired publications until interval past expiration', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const [, , future] = sr.secrets;
|
||||||
|
sr.stop();
|
||||||
|
const [origId] = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
const p = await db.get(origId);
|
||||||
|
await fc.advance(p.end + p.lifetime + p.interval - 1);
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert(sr.secrets.slice(1).includes(future));
|
||||||
|
// It should have created a new publication, not extended the life of the old publication.
|
||||||
|
assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2);
|
||||||
|
assert.deepEqual(await db.get(origId), p);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('idempotent', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(fc.timeouts.size, 1);
|
||||||
|
const secrets = [...sr.secrets];
|
||||||
|
const dbKeys = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(fc.timeouts.size, 1);
|
||||||
|
assert.deepEqual(sr.secrets, secrets);
|
||||||
|
assert.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`schedules update at next interval (= ${interval})`, function () {
|
||||||
|
const testCases = [
|
||||||
|
{now: 0, want: interval},
|
||||||
|
{now: 1, want: interval},
|
||||||
|
{now: interval - 1, want: interval},
|
||||||
|
{now: interval, want: 2 * interval},
|
||||||
|
{now: interval + 1, want: 2 * interval},
|
||||||
|
];
|
||||||
|
for (const {now, want} of testCases) {
|
||||||
|
it(`${now} -> ${want}`, async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await fc.setNow(now);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(fc.timeouts.size, 1);
|
||||||
|
const [{when}] = fc.timeouts.values();
|
||||||
|
assert.equal(when, want);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('multiple active params with different intervals', async function () {
|
||||||
|
const intervals = [400, 600, 1000];
|
||||||
|
const lcmi = lcm(...intervals);
|
||||||
|
const wants = new Set();
|
||||||
|
for (const i of intervals) for (let t = i; t <= lcmi; t += i) wants.add(t);
|
||||||
|
const fcs = new FakeClock();
|
||||||
|
const srs = intervals.map((i) => {
|
||||||
|
interval = i;
|
||||||
|
const sr = newRotator();
|
||||||
|
setFakeClock(sr, fcs);
|
||||||
|
return sr;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
for (const sr of srs) await sr.start(); // Don't use Promise.all() otherwise they race.
|
||||||
|
interval = intervals[intervals.length - 1];
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr); // Independent clock to test a single instance's behavior.
|
||||||
|
await sr.start();
|
||||||
|
for (const want of [...wants].sort((a, b) => a - b)) {
|
||||||
|
logger.debug(`next timeout should be at ${want}`);
|
||||||
|
await fc.advanceToNext();
|
||||||
|
await fcs.setNow(fc.now); // Keep all of the publications alive.
|
||||||
|
assert.equal(fc.now, want);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (const sr of srs) sr.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop', function () {
|
||||||
|
it('clears timeout', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
assert.notEqual(fc.timeouts.size, 0);
|
||||||
|
sr.stop();
|
||||||
|
assert.equal(fc.timeouts.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('safe to call multiple times', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
sr.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('legacy secret', function () {
|
||||||
|
it('ends at now if there are no previously published secrets', async function () {
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
// Use a time that isn't a multiple of interval in case there is a modular arithmetic bug that
|
||||||
|
// would otherwise go undetected.
|
||||||
|
await fc.setNow(1);
|
||||||
|
assert(mod(fc.now, interval) !== 0);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(sr.secrets.length, 4); // 1 for the legacy secret, 3 for past, current, future
|
||||||
|
assert(sr.secrets.slice(1).includes('legacy')); // Should not be the current secret.
|
||||||
|
const ids = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
const params = (await Promise.all(ids.map(async (id) => await db.get(id))))
|
||||||
|
.sort((a, b) => a.algId - b.algId);
|
||||||
|
assert.deepEqual(params, [
|
||||||
|
{
|
||||||
|
algId: 0,
|
||||||
|
algParams: 'legacy',
|
||||||
|
// The start time must equal the end time so that legacy secrets do not affect the end
|
||||||
|
// times of legacy secrets published by other instances.
|
||||||
|
start: fc.now,
|
||||||
|
end: fc.now,
|
||||||
|
lifetime,
|
||||||
|
interval: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
algId: 1,
|
||||||
|
algParams: params[1].algParams,
|
||||||
|
start: fc.now,
|
||||||
|
end: intervalStart(fc.now) + (2 * interval),
|
||||||
|
interval,
|
||||||
|
lifetime,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ends at the start of the oldest previously published secret', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await fc.setNow(1);
|
||||||
|
assert(mod(fc.now, interval) !== 0);
|
||||||
|
const wantTime = fc.now;
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(sr.secrets.length, 3);
|
||||||
|
const [s1, s0, s2] = sr.secrets; // s1=current, s0=previous, s2=next
|
||||||
|
sr.stop();
|
||||||
|
// Use a time that is not a multiple of interval off of epoch or wantTime just in case there
|
||||||
|
// is a modular arithmetic bug that would otherwise go undetected.
|
||||||
|
await fc.advance(interval + 1);
|
||||||
|
assert(mod(fc.now, interval) !== 0);
|
||||||
|
assert(mod(fc.now - wantTime, interval) !== 0);
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal(sr.secrets.length, 5); // s0 through s3 and the legacy secret.
|
||||||
|
assert.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3], 'legacy']);
|
||||||
|
const ids = await db.findKeys(`${dbPrefix}:*`, null);
|
||||||
|
const params = (await Promise.all(ids.map(async (id) => await db.get(id))))
|
||||||
|
.sort((a, b) => a.algId - b.algId);
|
||||||
|
assert.deepEqual(params, [
|
||||||
|
{
|
||||||
|
algId: 0,
|
||||||
|
algParams: 'legacy',
|
||||||
|
start: wantTime,
|
||||||
|
end: wantTime,
|
||||||
|
interval: null,
|
||||||
|
lifetime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
algId: 1,
|
||||||
|
algParams: params[1].algParams,
|
||||||
|
start: wantTime,
|
||||||
|
end: intervalStart(fc.now) + (2 * interval),
|
||||||
|
interval,
|
||||||
|
lifetime,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple instances with different legacy secrets', async function () {
|
||||||
|
sr = newRotator('legacy1');
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
sr = newRotator('legacy2');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert(sr.secrets.slice(1).includes('legacy1'));
|
||||||
|
assert(sr.secrets.slice(1).includes('legacy2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple instances with the same legacy secret', async function () {
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert.deepEqual(sr.secrets, [...new Set(sr.secrets)]);
|
||||||
|
// There shouldn't be multiple publications for the same legacy secret.
|
||||||
|
assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('legacy secret is included for interval after expiration', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
await fc.advance(lifetime + interval - 1);
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert(sr.secrets.slice(1).includes('legacy'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('legacy secret is not included if the oldest secret is old enough', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
await fc.advance(lifetime + interval);
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert(!sr.secrets.includes('legacy'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dead secrets still affect legacy secret end time', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const secrets = new Set(sr.secrets);
|
||||||
|
sr.stop();
|
||||||
|
await fc.advance(lifetime + (3 * interval));
|
||||||
|
sr = newRotator('legacy');
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert(!sr.secrets.includes('legacy'));
|
||||||
|
assert(!sr.secrets.some((s) => secrets.has(s)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rotation', function () {
|
||||||
|
it('no rotation before start of interval', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
assert.equal(fc.now, 0);
|
||||||
|
await sr.start();
|
||||||
|
const secrets = [...sr.secrets];
|
||||||
|
await fc.advance(interval - 1);
|
||||||
|
assert.deepEqual(sr.secrets, secrets);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not replace secrets array', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const [current] = sr.secrets;
|
||||||
|
const secrets = sr.secrets;
|
||||||
|
await fc.advance(interval);
|
||||||
|
assert.notEqual(sr.secrets[0], current);
|
||||||
|
assert.equal(sr.secrets, secrets);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('future secret becomes current, new future is generated', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const secrets = new Set(sr.secrets);
|
||||||
|
assert.equal(secrets.size, 3);
|
||||||
|
const [s1, s0, s2] = sr.secrets;
|
||||||
|
await fc.advance(interval);
|
||||||
|
assert.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3]]);
|
||||||
|
assert(!secrets.has(sr.secrets[3]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expired publications are deleted', async function () {
|
||||||
|
const origInterval = interval;
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
sr.stop();
|
||||||
|
++interval; // Force new params so that the old params can expire.
|
||||||
|
sr = newRotator();
|
||||||
|
setFakeClock(sr, fc);
|
||||||
|
await sr.start();
|
||||||
|
assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2);
|
||||||
|
await fc.advance(lifetime + (3 * origInterval));
|
||||||
|
assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('old secrets are eventually removed', async function () {
|
||||||
|
sr = newRotator();
|
||||||
|
const fc = setFakeClock(sr);
|
||||||
|
await sr.start();
|
||||||
|
const [, s0] = sr.secrets;
|
||||||
|
await fc.advance(lifetime + interval - 1);
|
||||||
|
assert(sr.secrets.slice(1).includes(s0));
|
||||||
|
await fc.advance(1);
|
||||||
|
assert(!sr.secrets.includes(s0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clock skew', function () {
|
||||||
|
it('out of sync works if in adjacent interval', async function () {
|
||||||
|
const srs = [newRotator(), newRotator()];
|
||||||
|
const fcs = srs.map((sr) => setFakeClock(sr));
|
||||||
|
for (const sr of srs) await sr.start(); // Don't use Promise.all() otherwise they race.
|
||||||
|
assert.deepEqual(srs[0].secrets, srs[1].secrets);
|
||||||
|
// Advance fcs[0] to the end of the interval after fcs[1].
|
||||||
|
await fcs[0].advance((2 * interval) - 1);
|
||||||
|
assert(srs[0].secrets.includes(srs[1].secrets[0]));
|
||||||
|
assert(srs[1].secrets.includes(srs[0].secrets[0]));
|
||||||
|
// Advance both by an interval.
|
||||||
|
await Promise.all([fcs[1].advance(interval), fcs[0].advance(interval)]);
|
||||||
|
assert(srs[0].secrets.includes(srs[1].secrets[0]));
|
||||||
|
assert(srs[1].secrets.includes(srs[0].secrets[0]));
|
||||||
|
// Advance fcs[1] to the end of the interval after fcs[0].
|
||||||
|
await Promise.all([fcs[1].advance((3 * interval) - 1), fcs[0].advance(1)]);
|
||||||
|
assert(srs[0].secrets.includes(srs[1].secrets[0]));
|
||||||
|
assert(srs[1].secrets.includes(srs[0].secrets[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('start up out of sync', async function () {
|
||||||
|
const srs = [newRotator(), newRotator()];
|
||||||
|
const fcs = srs.map((sr) => setFakeClock(sr));
|
||||||
|
await fcs[0].advance((2 * interval) - 1);
|
||||||
|
await srs[0].start(); // Must start before srs[1] so that srs[1] starts in srs[0]'s past.
|
||||||
|
await srs[1].start();
|
||||||
|
assert(srs[0].secrets.includes(srs[1].secrets[0]));
|
||||||
|
assert(srs[1].secrets.includes(srs[0].secrets[0]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const common = require('../../common');
|
const common = require('../../common');
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
|
||||||
let agent;
|
let agent;
|
||||||
const apiKey = common.apiKey;
|
const apiKey = common.apiKey;
|
||||||
|
@ -15,14 +16,14 @@ describe(__filename, function () {
|
||||||
before(async function () { agent = await common.init(); });
|
before(async function () { agent = await common.init(); });
|
||||||
|
|
||||||
describe('API Versioning', function () {
|
describe('API Versioning', function () {
|
||||||
it('errors if can not connect', function (done) {
|
it('errors if can not connect', async function () {
|
||||||
agent.get('/api/')
|
await agent.get('/api/')
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
apiVersion = res.body.currentVersion;
|
apiVersion = res.body.currentVersion;
|
||||||
if (!res.body.currentVersion) throw new Error('No version set in API');
|
if (!res.body.currentVersion) throw new Error('No version set in API');
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,20 +39,18 @@ describe(__filename, function () {
|
||||||
-> getChatHistory(padID)
|
-> getChatHistory(padID)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe('createPad', function () {
|
describe('Chat functionality', function () {
|
||||||
it('creates a new Pad', function (done) {
|
it('creates a new Pad', async function () {
|
||||||
agent.get(`${endPoint('createPad')}&padID=${padID}`)
|
await agent.get(`${endPoint('createPad')}&padID=${padID}`)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
|
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
|
||||||
})
|
})
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createAuthor', function () {
|
it('Creates an author with a name set', async function () {
|
||||||
it('Creates an author with a name set', function (done) {
|
await agent.get(endPoint('createAuthor'))
|
||||||
agent.get(endPoint('createAuthor'))
|
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
if (res.body.code !== 0 || !res.body.data.authorID) {
|
if (res.body.code !== 0 || !res.body.data.authorID) {
|
||||||
throw new Error('Unable to create author');
|
throw new Error('Unable to create author');
|
||||||
|
@ -59,47 +58,51 @@ describe(__filename, function () {
|
||||||
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
|
authorID = res.body.data.authorID; // we will be this author for the rest of the tests
|
||||||
})
|
})
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('appendChatMessage', function () {
|
it('Gets the head of chat before the first chat msg', async function () {
|
||||||
it('Adds a chat message to the pad', function (done) {
|
await agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
|
||||||
agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
|
.expect((res) => {
|
||||||
|
if (res.body.data.chatHead !== -1) throw new Error('Chat Head Length is wrong');
|
||||||
|
if (res.body.code !== 0) throw new Error('Unable to get chat head');
|
||||||
|
})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Adds a chat message to the pad', async function () {
|
||||||
|
await agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
|
||||||
`&authorID=${authorID}&time=${timestamp}`)
|
`&authorID=${authorID}&time=${timestamp}`)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
if (res.body.code !== 0) throw new Error('Unable to create chat message');
|
if (res.body.code !== 0) throw new Error('Unable to create chat message');
|
||||||
})
|
})
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Gets the head of chat', async function () {
|
||||||
describe('getChatHead', function () {
|
await agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
|
||||||
it('Gets the head of chat', function (done) {
|
|
||||||
agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
|
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong');
|
if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong');
|
||||||
|
|
||||||
if (res.body.code !== 0) throw new Error('Unable to get chat head');
|
if (res.body.code !== 0) throw new Error('Unable to get chat head');
|
||||||
})
|
})
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getChatHistory', function () {
|
it('Gets Chat History of a Pad', async function () {
|
||||||
it('Gets Chat History of a Pad', function (done) {
|
await agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
|
||||||
agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
|
|
||||||
.expect((res) => {
|
|
||||||
if (res.body.data.messages.length !== 1) {
|
|
||||||
throw new Error('Chat History Length is wrong');
|
|
||||||
}
|
|
||||||
if (res.body.code !== 0) throw new Error('Unable to get chat history');
|
|
||||||
})
|
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
assert.equal(res.body.code, 0, 'Unable to get chat history');
|
||||||
|
assert.equal(res.body.data.messages.length, 1, 'Chat History Length is wrong');
|
||||||
|
assert.equal(res.body.data.messages[0].text, 'blalblalbha', 'Chat text does not match');
|
||||||
|
assert.equal(res.body.data.messages[0].userId, authorID, 'Message author does not match');
|
||||||
|
assert.equal(res.body.data.messages[0].time, timestamp.toString(), 'Message time does not match');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,16 +17,16 @@ describe(__filename, function () {
|
||||||
before(async function () { agent = await common.init(); });
|
before(async function () { agent = await common.init(); });
|
||||||
|
|
||||||
describe('Connectivity for instance-level API tests', function () {
|
describe('Connectivity for instance-level API tests', function () {
|
||||||
it('can connect', function (done) {
|
it('can connect', async function () {
|
||||||
agent.get('/api/')
|
await agent.get('/api/')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getStats', function () {
|
describe('getStats', function () {
|
||||||
it('Gets the stats of a running instance', function (done) {
|
it('Gets the stats of a running instance', async function () {
|
||||||
agent.get(endPoint('getStats'))
|
await agent.get(endPoint('getStats'))
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
if (res.body.code !== 0) throw new Error('getStats() failed');
|
if (res.body.code !== 0) throw new Error('getStats() failed');
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ describe(__filename, function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200, done);
|
.expect(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ let apiVersion = 1;
|
||||||
const testPadId = makeid();
|
const testPadId = makeid();
|
||||||
const newPadId = makeid();
|
const newPadId = makeid();
|
||||||
const copiedPadId = makeid();
|
const copiedPadId = makeid();
|
||||||
|
const anotherPadId = makeid();
|
||||||
let lastEdited = '';
|
let lastEdited = '';
|
||||||
const text = generateLongText();
|
const text = generateLongText();
|
||||||
|
|
||||||
|
@ -502,6 +503,31 @@ describe(__filename, function () {
|
||||||
.expect('Content-Type', /json/);
|
.expect('Content-Type', /json/);
|
||||||
assert.equal(res.body.data.revisions, revCount);
|
assert.equal(res.body.data.revisions, revCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a new Pad with empty text', async function () {
|
||||||
|
await agent.get(`${endPoint('createPad')}&padID=${anotherPadId}&text=`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
assert.equal(res.body.code, 0, 'Unable to create new Pad');
|
||||||
|
});
|
||||||
|
await agent.get(`${endPoint('getText')}&padID=${anotherPadId}`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
assert.equal(res.body.code, 0, 'Unable to get pad text');
|
||||||
|
assert.equal(res.body.data.text, '\n', 'Pad text is not empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes with empty text', async function () {
|
||||||
|
await agent.get(`${endPoint('deletePad')}&padID=${anotherPadId}`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
assert.equal(res.body.code, 0, 'Unable to delete empty Pad');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('copyPadWithoutHistory', function () {
|
describe('copyPadWithoutHistory', function () {
|
||||||
|
|
11
src/tests/backend/specs/crypto.js
Normal file
11
src/tests/backend/specs/crypto.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
const {Buffer} = require('buffer');
|
||||||
|
const crypto = require('../../../node/security/crypto');
|
||||||
|
const nodeCrypto = require('crypto');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null;
|
||||||
|
|
||||||
|
const ab2hex = (ab) => Buffer.from(ab).toString('hex');
|
90
src/tests/backend/specs/lowerCasePadIds.js
Normal file
90
src/tests/backend/specs/lowerCasePadIds.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
const common = require('../common');
|
||||||
|
const padManager = require('../../../node/db/PadManager');
|
||||||
|
const settings = require('../../../node/utils/Settings');
|
||||||
|
|
||||||
|
describe(__filename, function () {
|
||||||
|
let agent;
|
||||||
|
const cleanUpPads = async () => {
|
||||||
|
const {padIDs} = await padManager.listAllPads();
|
||||||
|
await Promise.all(padIDs.map(async (padId) => {
|
||||||
|
if (await padManager.doesPadExist(padId)) {
|
||||||
|
const pad = await padManager.getPad(padId);
|
||||||
|
await pad.remove();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let backup;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
backup = settings.lowerCasePadIds;
|
||||||
|
agent = await common.init();
|
||||||
|
});
|
||||||
|
beforeEach(async function () {
|
||||||
|
await cleanUpPads();
|
||||||
|
});
|
||||||
|
afterEach(async function () {
|
||||||
|
await cleanUpPads();
|
||||||
|
});
|
||||||
|
after(async function () {
|
||||||
|
settings.lowerCasePadIds = backup;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('not activated', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
settings.lowerCasePadIds = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('do nothing', async function () {
|
||||||
|
await agent.get('/p/UPPERCASEpad')
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('activated', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
settings.lowerCasePadIds = true;
|
||||||
|
});
|
||||||
|
it('lowercase pad ids', async function () {
|
||||||
|
await agent.get('/p/UPPERCASEpad')
|
||||||
|
.expect(302)
|
||||||
|
.expect('location', 'uppercasepad');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps old pads accessible', async function () {
|
||||||
|
Object.assign(settings, {
|
||||||
|
lowerCasePadIds: false,
|
||||||
|
});
|
||||||
|
await padManager.getPad('ALREADYexistingPad', 'oldpad');
|
||||||
|
await padManager.getPad('alreadyexistingpad', 'newpad');
|
||||||
|
Object.assign(settings, {
|
||||||
|
lowerCasePadIds: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldPad = await agent.get('/p/ALREADYexistingPad').expect(200);
|
||||||
|
const oldPadSocket = await common.connect(oldPad);
|
||||||
|
const oldPadHandshake = await common.handshake(oldPadSocket, 'ALREADYexistingPad');
|
||||||
|
assert.equal(oldPadHandshake.data.padId, 'ALREADYexistingPad');
|
||||||
|
assert.equal(oldPadHandshake.data.collab_client_vars.initialAttributedText.text, 'oldpad\n');
|
||||||
|
|
||||||
|
const newPad = await agent.get('/p/alreadyexistingpad').expect(200);
|
||||||
|
const newPadSocket = await common.connect(newPad);
|
||||||
|
const newPadHandshake = await common.handshake(newPadSocket, 'alreadyexistingpad');
|
||||||
|
assert.equal(newPadHandshake.data.padId, 'alreadyexistingpad');
|
||||||
|
assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'newpad\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallow creation of different case pad-name via socket connection', async function () {
|
||||||
|
await padManager.getPad('maliciousattempt', 'attempt');
|
||||||
|
|
||||||
|
const newPad = await agent.get('/p/maliciousattempt').expect(200);
|
||||||
|
const newPadSocket = await common.connect(newPad);
|
||||||
|
const newPadHandshake = await common.handshake(newPadSocket, 'MaliciousAttempt');
|
||||||
|
|
||||||
|
assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'attempt\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -181,7 +181,9 @@ const helper = {};
|
||||||
helper.padOuter$.fx.off = true;
|
helper.padOuter$.fx.off = true;
|
||||||
helper.padInner$.fx.off = true;
|
helper.padInner$.fx.off = true;
|
||||||
|
|
||||||
return opts.id;
|
// Don't return opts.id -- the server might have redirected the browser to a transformed version
|
||||||
|
// of the requested pad ID.
|
||||||
|
return helper.padChrome$.window.clientVars.padId;
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.newAdmin = async (page) => {
|
helper.newAdmin = async (page) => {
|
||||||
|
|
|
@ -37,7 +37,7 @@ helper.edit = async (message, line) => {
|
||||||
await helper.withFastCommit(async (incorp) => {
|
await helper.withFastCommit(async (incorp) => {
|
||||||
helper.linesDiv()[line].sendkeys(message);
|
helper.linesDiv()[line].sendkeys(message);
|
||||||
incorp();
|
incorp();
|
||||||
await helper.waitForPromise(() => editsNum + 1 === helper.commits.length);
|
await helper.waitForPromise(() => editsNum + 1 === helper.commits.length, 10000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ helper.sendChatMessage = async (message) => {
|
||||||
*/
|
*/
|
||||||
helper.showSettings = async () => {
|
helper.showSettings = async () => {
|
||||||
if (helper.isSettingsShown()) return;
|
if (helper.isSettingsShown()) return;
|
||||||
helper.settingsButton().click();
|
helper.settingsButton().trigger('click');
|
||||||
await helper.waitForPromise(() => helper.isSettingsShown(), 2000);
|
await helper.waitForPromise(() => helper.isSettingsShown(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ helper.showSettings = async () => {
|
||||||
*/
|
*/
|
||||||
helper.hideSettings = async () => {
|
helper.hideSettings = async () => {
|
||||||
if (!helper.isSettingsShown()) return;
|
if (!helper.isSettingsShown()) return;
|
||||||
helper.settingsButton().click();
|
helper.settingsButton().trigger('click');
|
||||||
await helper.waitForPromise(() => !helper.isSettingsShown(), 2000);
|
await helper.waitForPromise(() => !helper.isSettingsShown(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ helper.hideSettings = async () => {
|
||||||
helper.enableStickyChatviaSettings = async () => {
|
helper.enableStickyChatviaSettings = async () => {
|
||||||
const stickyChat = helper.padChrome$('#options-stickychat');
|
const stickyChat = helper.padChrome$('#options-stickychat');
|
||||||
if (!helper.isSettingsShown() || stickyChat.is(':checked')) return;
|
if (!helper.isSettingsShown() || stickyChat.is(':checked')) return;
|
||||||
stickyChat.click();
|
stickyChat.trigger('click');
|
||||||
await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ helper.enableStickyChatviaSettings = async () => {
|
||||||
helper.disableStickyChatviaSettings = async () => {
|
helper.disableStickyChatviaSettings = async () => {
|
||||||
const stickyChat = helper.padChrome$('#options-stickychat');
|
const stickyChat = helper.padChrome$('#options-stickychat');
|
||||||
if (!helper.isSettingsShown() || !stickyChat.is(':checked')) return;
|
if (!helper.isSettingsShown() || !stickyChat.is(':checked')) return;
|
||||||
stickyChat.click();
|
stickyChat.trigger('click');
|
||||||
await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ helper.disableStickyChatviaSettings = async () => {
|
||||||
helper.enableStickyChatviaIcon = async () => {
|
helper.enableStickyChatviaIcon = async () => {
|
||||||
const stickyChat = helper.padChrome$('#titlesticky');
|
const stickyChat = helper.padChrome$('#titlesticky');
|
||||||
if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
|
if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
|
||||||
stickyChat.click();
|
stickyChat.trigger('click');
|
||||||
await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ helper.enableStickyChatviaIcon = async () => {
|
||||||
*/
|
*/
|
||||||
helper.disableStickyChatviaIcon = async () => {
|
helper.disableStickyChatviaIcon = async () => {
|
||||||
if (!helper.isChatboxShown() || !helper.isChatboxSticky()) return;
|
if (!helper.isChatboxShown() || !helper.isChatboxSticky()) return;
|
||||||
helper.titlecross().click();
|
helper.titlecross().trigger('click');
|
||||||
await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -175,9 +175,8 @@ helper.disableStickyChatviaIcon = async () => {
|
||||||
*/
|
*/
|
||||||
helper.gotoTimeslider = async (revision) => {
|
helper.gotoTimeslider = async (revision) => {
|
||||||
revision = Number.isInteger(revision) ? `#${revision}` : '';
|
revision = Number.isInteger(revision) ? `#${revision}` : '';
|
||||||
const iframe = $('#iframe-container iframe');
|
helper.padChrome$.window.location.href =
|
||||||
iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`);
|
`${helper.padChrome$.window.location.pathname}/timeslider${revision}`;
|
||||||
|
|
||||||
await helper.waitForPromise(() => helper.timesliderTimerTime() &&
|
await helper.waitForPromise(() => helper.timesliderTimerTime() &&
|
||||||
!Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000);
|
!Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ helper.contentWindow = () => $('#iframe-container iframe')[0].contentWindow;
|
||||||
helper.showChat = async () => {
|
helper.showChat = async () => {
|
||||||
const chaticon = helper.chatIcon();
|
const chaticon = helper.chatIcon();
|
||||||
if (!chaticon.hasClass('visible')) return;
|
if (!chaticon.hasClass('visible')) return;
|
||||||
chaticon.click();
|
chaticon.trigger('click');
|
||||||
await helper.waitForPromise(() => !chaticon.hasClass('visible'), 2000);
|
await helper.waitForPromise(() => !chaticon.hasClass('visible'), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ helper.showChat = async () => {
|
||||||
*/
|
*/
|
||||||
helper.hideChat = async () => {
|
helper.hideChat = async () => {
|
||||||
if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
|
if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
|
||||||
helper.titlecross().click();
|
helper.titlecross().trigger('click');
|
||||||
await helper.waitForPromise(() => !helper.isChatboxShown(), 2000);
|
await helper.waitForPromise(() => !helper.isChatboxShown(), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ helper.settingsButton =
|
||||||
helper.toggleUserList = async () => {
|
helper.toggleUserList = async () => {
|
||||||
const isVisible = helper.userListShown();
|
const isVisible = helper.userListShown();
|
||||||
const button = helper.padChrome$("button[data-l10n-id='pad.toolbar.showusers.title']");
|
const button = helper.padChrome$("button[data-l10n-id='pad.toolbar.showusers.title']");
|
||||||
button.click();
|
button.trigger('click');
|
||||||
await helper.waitForPromise(() => !isVisible);
|
await helper.waitForPromise(() => !isVisible);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,9 +104,9 @@ helper.userListShown = () => helper.padChrome$('div#users').hasClass('popup-show
|
||||||
*/
|
*/
|
||||||
helper.setUserName = async (name) => {
|
helper.setUserName = async (name) => {
|
||||||
const userElement = helper.usernameField();
|
const userElement = helper.usernameField();
|
||||||
userElement.click();
|
userElement.trigger('click');
|
||||||
userElement.val(name);
|
userElement.val(name);
|
||||||
userElement.blur();
|
userElement.trigger('blur');
|
||||||
await helper.waitForPromise(() => !helper.usernameField().hasClass('editactive'));
|
await helper.waitForPromise(() => !helper.usernameField().hasClass('editactive'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe('Admin > Settings', function () {
|
||||||
observer.observe(
|
observer.observe(
|
||||||
helper.admin$('#response')[0], {attributes: true, childList: false, subtree: false});
|
helper.admin$('#response')[0], {attributes: true, childList: false, subtree: false});
|
||||||
});
|
});
|
||||||
helper.admin$('#saveSettings').click();
|
helper.admin$('#saveSettings').trigger('click');
|
||||||
await p;
|
await p;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,8 +80,8 @@ describe('Admin > Settings', function () {
|
||||||
await helper.waitForPromise(async () => {
|
await helper.waitForPromise(async () => {
|
||||||
oldStartTime = await getStartTime();
|
oldStartTime = await getStartTime();
|
||||||
return oldStartTime != null && oldStartTime > 0;
|
return oldStartTime != null && oldStartTime > 0;
|
||||||
}, 1000, 500);
|
}, 2100, 500);
|
||||||
helper.admin$('#restartEtherpad').click();
|
helper.admin$('#restartEtherpad').trigger('click');
|
||||||
await helper.waitForPromise(async () => {
|
await helper.waitForPromise(async () => {
|
||||||
const startTime = await getStartTime();
|
const startTime = await getStartTime();
|
||||||
return startTime != null && startTime > oldStartTime;
|
return startTime != null && startTime > oldStartTime;
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('Plugins page', function () {
|
||||||
|
|
||||||
await timeout(500); // HACK! Please submit better fix..
|
await timeout(500); // HACK! Please submit better fix..
|
||||||
const $doUpdateButton = helper.admin$('.ep_align .do-update');
|
const $doUpdateButton = helper.admin$('.ep_align .do-update');
|
||||||
$doUpdateButton.click();
|
$doUpdateButton.trigger('click');
|
||||||
|
|
||||||
// ensure its showing as Updating
|
// ensure its showing as Updating
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
|
@ -79,7 +79,7 @@ describe('Plugins page', function () {
|
||||||
// skip if we already have ep_headings2 installed..
|
// skip if we already have ep_headings2 installed..
|
||||||
if (helper.admin$('.ep_headings2 .do-install').is(':visible') === false) this.skip();
|
if (helper.admin$('.ep_headings2 .do-install').is(':visible') === false) this.skip();
|
||||||
|
|
||||||
helper.admin$('.ep_headings2 .do-install').click();
|
helper.admin$('.ep_headings2 .do-install').trigger('click');
|
||||||
// ensure install has attempted to be started
|
// ensure install has attempted to be started
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
() => helper.admin$('.ep_headings2 .do-install').length !== 0, 120000);
|
() => helper.admin$('.ep_headings2 .do-install').length !== 0, 120000);
|
||||||
|
@ -96,7 +96,7 @@ describe('Plugins page', function () {
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
() => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000);
|
() => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000);
|
||||||
|
|
||||||
helper.admin$('.ep_headings2 .do-uninstall').click();
|
helper.admin$('.ep_headings2 .do-uninstall').trigger('click');
|
||||||
|
|
||||||
// ensure its showing uninstalling
|
// ensure its showing uninstalling
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('author of pad edition', function () {
|
||||||
$lineWithUnorderedList.sendkeys('{selectall}');
|
$lineWithUnorderedList.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist');
|
const $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertUnorderedListButton.click();
|
$insertUnorderedListButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => (
|
await helper.waitForPromise(() => (
|
||||||
getLine(LINE_WITH_UNORDERED_LIST).find('ul li').length === 1 &&
|
getLine(LINE_WITH_UNORDERED_LIST).find('ul li').length === 1 &&
|
||||||
|
@ -36,7 +36,7 @@ describe('author of pad edition', function () {
|
||||||
$lineWithOrderedList.sendkeys('{selectall}');
|
$lineWithOrderedList.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist');
|
const $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist');
|
||||||
$insertOrderedListButton.click();
|
$insertOrderedListButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => (
|
await helper.waitForPromise(() => (
|
||||||
getLine(LINE_WITH_ORDERED_LIST).find('ol li').length === 1 &&
|
getLine(LINE_WITH_ORDERED_LIST).find('ol li').length === 1 &&
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe('bold button', function () {
|
||||||
|
|
||||||
// get the bold button and click it
|
// get the bold button and click it
|
||||||
const $boldButton = chrome$('.buttonicon-bold');
|
const $boldButton = chrome$('.buttonicon-bold');
|
||||||
$boldButton.click();
|
$boldButton.trigger('click');
|
||||||
|
|
||||||
const $newFirstTextElement = inner$('div').first();
|
const $newFirstTextElement = inner$('div').first();
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,10 @@ describe('change user color', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
let $userButton = chrome$('.buttonicon-showusers');
|
let $userButton = chrome$('.buttonicon-showusers');
|
||||||
$userButton.click();
|
$userButton.trigger('click');
|
||||||
|
|
||||||
let $userSwatch = chrome$('#myswatch');
|
let $userSwatch = chrome$('#myswatch');
|
||||||
$userSwatch.click();
|
$userSwatch.trigger('click');
|
||||||
|
|
||||||
const fb = chrome$.farbtastic('#colorpicker');
|
const fb = chrome$.farbtastic('#colorpicker');
|
||||||
const $colorPickerSave = chrome$('#mycolorpickersave');
|
const $colorPickerSave = chrome$('#mycolorpickersave');
|
||||||
|
@ -34,7 +34,7 @@ describe('change user color', function () {
|
||||||
// The swatch updates as the test color is picked.
|
// The swatch updates as the test color is picked.
|
||||||
fb.setColor(testColorHash);
|
fb.setColor(testColorHash);
|
||||||
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
|
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
|
||||||
$colorPickerSave.click();
|
$colorPickerSave.trigger('click');
|
||||||
expect($userSwatch.css('background-color')).to.be(testColorRGB);
|
expect($userSwatch.css('background-color')).to.be(testColorRGB);
|
||||||
|
|
||||||
// give it a second to save the color on the server side
|
// give it a second to save the color on the server side
|
||||||
|
@ -47,10 +47,10 @@ describe('change user color', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
$userButton = chrome$('.buttonicon-showusers');
|
$userButton = chrome$('.buttonicon-showusers');
|
||||||
$userButton.click();
|
$userButton.trigger('click');
|
||||||
|
|
||||||
$userSwatch = chrome$('#myswatch');
|
$userSwatch = chrome$('#myswatch');
|
||||||
$userSwatch.click();
|
$userSwatch.trigger('click');
|
||||||
|
|
||||||
$colorPickerPreview = chrome$('#mycolorpickerpreview');
|
$colorPickerPreview = chrome$('#mycolorpickerpreview');
|
||||||
|
|
||||||
|
@ -64,15 +64,15 @@ describe('change user color', function () {
|
||||||
|
|
||||||
const $colorOption = helper.padChrome$('#options-colorscheck');
|
const $colorOption = helper.padChrome$('#options-colorscheck');
|
||||||
if (!$colorOption.is(':checked')) {
|
if (!$colorOption.is(':checked')) {
|
||||||
$colorOption.click();
|
$colorOption.trigger('click');
|
||||||
}
|
}
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $userButton = chrome$('.buttonicon-showusers');
|
const $userButton = chrome$('.buttonicon-showusers');
|
||||||
$userButton.click();
|
$userButton.trigger('click');
|
||||||
|
|
||||||
const $userSwatch = chrome$('#myswatch');
|
const $userSwatch = chrome$('#myswatch');
|
||||||
$userSwatch.click();
|
$userSwatch.trigger('click');
|
||||||
|
|
||||||
const fb = chrome$.farbtastic('#colorpicker');
|
const fb = chrome$.farbtastic('#colorpicker');
|
||||||
const $colorPickerSave = chrome$('#mycolorpickersave');
|
const $colorPickerSave = chrome$('#mycolorpickersave');
|
||||||
|
@ -82,11 +82,11 @@ describe('change user color', function () {
|
||||||
const testColorRGB = 'rgb(171, 205, 239)';
|
const testColorRGB = 'rgb(171, 205, 239)';
|
||||||
|
|
||||||
fb.setColor(testColorHash);
|
fb.setColor(testColorHash);
|
||||||
$colorPickerSave.click();
|
$colorPickerSave.trigger('click');
|
||||||
|
|
||||||
// click on the chat button to make chat visible
|
// click on the chat button to make chat visible
|
||||||
const $chatButton = chrome$('#chaticon');
|
const $chatButton = chrome$('#chaticon');
|
||||||
$chatButton.click();
|
$chatButton.trigger('click');
|
||||||
const $chatInput = chrome$('#chatinput');
|
const $chatInput = chrome$('#chatinput');
|
||||||
$chatInput.sendkeys('O hi'); // simulate a keypress of typing user
|
$chatInput.sendkeys('O hi'); // simulate a keypress of typing user
|
||||||
$chatInput.sendkeys('{enter}');
|
$chatInput.sendkeys('{enter}');
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe('chat-load-messages', function () {
|
||||||
it('adds a lot of messages', async function () {
|
it('adds a lot of messages', async function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
const chatButton = chrome$('#chaticon');
|
const chatButton = chrome$('#chaticon');
|
||||||
chatButton.click();
|
chatButton.trigger('click');
|
||||||
const chatInput = chrome$('#chatinput');
|
const chatInput = chrome$('#chatinput');
|
||||||
const chatText = chrome$('#chattext');
|
const chatText = chrome$('#chattext');
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ describe('chat-load-messages', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
helper.waitFor(() => {
|
helper.waitFor(() => {
|
||||||
const chatButton = chrome$('#chaticon');
|
const chatButton = chrome$('#chaticon');
|
||||||
chatButton.click();
|
chatButton.trigger('click');
|
||||||
chatText = chrome$('#chattext');
|
chatText = chrome$('#chattext');
|
||||||
return chatText.children('p').length === expectedCount;
|
return chatText.children('p').length === expectedCount;
|
||||||
}).always(() => {
|
}).always(() => {
|
||||||
|
@ -47,11 +47,11 @@ describe('chat-load-messages', function () {
|
||||||
const expectedCount = 122;
|
const expectedCount = 122;
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
const chatButton = chrome$('#chaticon');
|
const chatButton = chrome$('#chaticon');
|
||||||
chatButton.click();
|
chatButton.trigger('click');
|
||||||
const chatText = chrome$('#chattext');
|
const chatText = chrome$('#chattext');
|
||||||
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
|
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
|
||||||
|
|
||||||
loadMsgBtn.click();
|
loadMsgBtn.trigger('click');
|
||||||
helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => {
|
helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => {
|
||||||
expect(chatText.children('p').length).to.be(expectedCount);
|
expect(chatText.children('p').length).to.be(expectedCount);
|
||||||
done();
|
done();
|
||||||
|
@ -63,11 +63,11 @@ describe('chat-load-messages', function () {
|
||||||
const expectedDisplay = 'none';
|
const expectedDisplay = 'none';
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
const chatButton = chrome$('#chaticon');
|
const chatButton = chrome$('#chaticon');
|
||||||
chatButton.click();
|
chatButton.trigger('click');
|
||||||
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
|
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
|
||||||
const loadMsgBall = chrome$('#chatloadmessagesball');
|
const loadMsgBall = chrome$('#chatloadmessagesball');
|
||||||
|
|
||||||
loadMsgBtn.click();
|
loadMsgBtn.trigger('click');
|
||||||
helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay &&
|
helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay &&
|
||||||
loadMsgBall.css('display') === expectedDisplay).always(() => {
|
loadMsgBall.css('display') === expectedDisplay).always(() => {
|
||||||
expect(loadMsgBtn.css('display')).to.be(expectedDisplay);
|
expect(loadMsgBtn.css('display')).to.be(expectedDisplay);
|
||||||
|
|
|
@ -32,11 +32,11 @@ describe('clear authorship colors button', function () {
|
||||||
() => inner$('div span').first().attr('class').indexOf('author') !== -1);
|
() => inner$('div span').first().attr('class').indexOf('author') !== -1);
|
||||||
|
|
||||||
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
|
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
|
||||||
inner$('div').first().focus();
|
inner$('div').first().trigger('focus');
|
||||||
|
|
||||||
// get the clear authorship colors button and click it
|
// get the clear authorship colors button and click it
|
||||||
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
||||||
$clearauthorshipcolorsButton.click();
|
$clearauthorshipcolorsButton.trigger('click');
|
||||||
|
|
||||||
// does the first div include an author class?
|
// does the first div include an author class?
|
||||||
const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
||||||
|
@ -70,11 +70,11 @@ describe('clear authorship colors button', function () {
|
||||||
() => inner$('div span').first().attr('class').indexOf('author') !== -1);
|
() => inner$('div span').first().attr('class').indexOf('author') !== -1);
|
||||||
|
|
||||||
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
|
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
|
||||||
inner$('div').first().focus();
|
inner$('div').first().trigger('focus');
|
||||||
|
|
||||||
// get the clear authorship colors button and click it
|
// get the clear authorship colors button and click it
|
||||||
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
||||||
$clearauthorshipcolorsButton.click();
|
$clearauthorshipcolorsButton.trigger('click');
|
||||||
|
|
||||||
// does the first div include an author class?
|
// does the first div include an author class?
|
||||||
let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
||||||
|
@ -93,7 +93,7 @@ describe('clear authorship colors button', function () {
|
||||||
const $undoButton = chrome$('.buttonicon-undo');
|
const $undoButton = chrome$('.buttonicon-undo');
|
||||||
|
|
||||||
// click the button
|
// click the button
|
||||||
$undoButton.click(); // shouldn't do anything
|
$undoButton.trigger('click'); // shouldn't do anything
|
||||||
hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
|
||||||
expect(hasAuthorClass).to.be(false);
|
expect(hasAuthorClass).to.be(false);
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('drag and drop', function () {
|
||||||
before(async function () {
|
before(async function () {
|
||||||
const originalHTML = helper.padInner$('body').html();
|
const originalHTML = helper.padInner$('body').html();
|
||||||
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
||||||
$undoButton.click();
|
$undoButton.trigger('click');
|
||||||
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ describe('drag and drop', function () {
|
||||||
before(async function () {
|
before(async function () {
|
||||||
const originalHTML = helper.padInner$('body').html();
|
const originalHTML = helper.padInner$('body').html();
|
||||||
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
||||||
$undoButton.click();
|
$undoButton.trigger('click');
|
||||||
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ describe('embed links', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
// open share dropdown
|
// open share dropdown
|
||||||
chrome$('.buttonicon-embed').click();
|
chrome$('.buttonicon-embed').trigger('click');
|
||||||
|
|
||||||
// get the link of the share field + the actual pad url and compare them
|
// get the link of the share field + the actual pad url and compare them
|
||||||
const shareLink = chrome$('#linkinput').val();
|
const shareLink = chrome$('#linkinput').val();
|
||||||
|
@ -73,7 +73,7 @@ describe('embed links', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
// open share dropdown
|
// open share dropdown
|
||||||
chrome$('.buttonicon-embed').click();
|
chrome$('.buttonicon-embed').trigger('click');
|
||||||
|
|
||||||
// get the link of the share field + the actual pad url and compare them
|
// get the link of the share field + the actual pad url and compare them
|
||||||
const embedCode = chrome$('#embedinput').val();
|
const embedCode = chrome$('#embedinput').val();
|
||||||
|
@ -93,8 +93,8 @@ describe('embed links', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
// open share dropdown
|
// open share dropdown
|
||||||
chrome$('.buttonicon-embed').click();
|
chrome$('.buttonicon-embed').trigger('click');
|
||||||
chrome$('#readonlyinput').click();
|
chrome$('#readonlyinput').trigger('click');
|
||||||
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
|
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
|
||||||
|
|
||||||
// get the link of the share field + the actual pad url and compare them
|
// get the link of the share field + the actual pad url and compare them
|
||||||
|
@ -109,9 +109,9 @@ describe('embed links', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
// open share dropdown
|
// open share dropdown
|
||||||
chrome$('.buttonicon-embed').click();
|
chrome$('.buttonicon-embed').trigger('click');
|
||||||
// check read only checkbox, a bit hacky
|
// check read only checkbox, a bit hacky
|
||||||
chrome$('#readonlyinput').click();
|
chrome$('#readonlyinput').trigger('click');
|
||||||
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
|
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('font select', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $settingsButton = chrome$('.buttonicon-settings');
|
const $settingsButton = chrome$('.buttonicon-settings');
|
||||||
$settingsButton.click();
|
$settingsButton.trigger('click');
|
||||||
|
|
||||||
// get the font menu and RobotoMono option
|
// get the font menu and RobotoMono option
|
||||||
const $viewfontmenu = chrome$('#viewfontmenu');
|
const $viewfontmenu = chrome$('#viewfontmenu');
|
||||||
|
@ -21,7 +21,7 @@ describe('font select', function () {
|
||||||
// $RobotoMonooption.attr('selected','selected');
|
// $RobotoMonooption.attr('selected','selected');
|
||||||
// commenting out above will break safari test
|
// commenting out above will break safari test
|
||||||
$viewfontmenu.val('RobotoMono');
|
$viewfontmenu.val('RobotoMono');
|
||||||
$viewfontmenu.change();
|
$viewfontmenu.trigger('change');
|
||||||
|
|
||||||
// check if font changed to RobotoMono
|
// check if font changed to RobotoMono
|
||||||
const fontFamily = inner$('body').css('font-family').toLowerCase();
|
const fontFamily = inner$('body').css('font-family').toLowerCase();
|
||||||
|
|
|
@ -47,13 +47,13 @@ describe('the test helper', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
let $userButton = chrome$('.buttonicon-showusers');
|
let $userButton = chrome$('.buttonicon-showusers');
|
||||||
$userButton.click();
|
$userButton.trigger('click');
|
||||||
|
|
||||||
let $usernameInput = chrome$('#myusernameedit');
|
let $usernameInput = chrome$('#myusernameedit');
|
||||||
$usernameInput.click();
|
$usernameInput.trigger('click');
|
||||||
|
|
||||||
$usernameInput.val('John McLear');
|
$usernameInput.val('John McLear');
|
||||||
$usernameInput.blur();
|
$usernameInput.trigger('blur');
|
||||||
|
|
||||||
// Before refreshing, make sure the name is there
|
// Before refreshing, make sure the name is there
|
||||||
expect($usernameInput.val()).to.be('John McLear');
|
expect($usernameInput.val()).to.be('John McLear');
|
||||||
|
@ -85,7 +85,7 @@ describe('the test helper', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
$userButton = chrome$('.buttonicon-showusers');
|
$userButton = chrome$('.buttonicon-showusers');
|
||||||
$userButton.click();
|
$userButton.trigger('click');
|
||||||
|
|
||||||
// confirm that the session was actually cleared
|
// confirm that the session was actually cleared
|
||||||
$usernameInput = chrome$('#myusernameedit');
|
$usernameInput = chrome$('#myusernameedit');
|
||||||
|
|
|
@ -527,7 +527,7 @@ describe('importexport.js', function () {
|
||||||
const isVisible = () => popup.hasClass('popup-show');
|
const isVisible = () => popup.hasClass('popup-show');
|
||||||
if (isVisible()) return;
|
if (isVisible()) return;
|
||||||
const button = helper.padChrome$('button[data-l10n-id="pad.toolbar.import_export.title"]');
|
const button = helper.padChrome$('button[data-l10n-id="pad.toolbar.import_export.title"]');
|
||||||
button.click();
|
button.trigger('click');
|
||||||
await helper.waitForPromise(isVisible);
|
await helper.waitForPromise(isVisible);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -558,7 +558,7 @@ describe('importexport.js', function () {
|
||||||
dt.items.add(new File([contents], `file.${ext}`, {type: 'text/plain'}));
|
dt.items.add(new File([contents], `file.${ext}`, {type: 'text/plain'}));
|
||||||
const form = helper.padChrome$('#importform');
|
const form = helper.padChrome$('#importform');
|
||||||
form.find('input[type=file]')[0].files = dt.files;
|
form.find('input[type=file]')[0].files = dt.files;
|
||||||
form.find('#importsubmitinput').submit();
|
form.find('#importsubmitinput').trigger('submit');
|
||||||
try {
|
try {
|
||||||
await helper.waitForPromise(() => {
|
await helper.waitForPromise(() => {
|
||||||
const got = helper.linesDiv();
|
const got = helper.linesDiv();
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe('indentation button', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
const $indentButton = chrome$('.buttonicon-indent');
|
const $indentButton = chrome$('.buttonicon-indent');
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1);
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ describe('indentation button', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
const $indentButton = chrome$('.buttonicon-indent');
|
const $indentButton = chrome$('.buttonicon-indent');
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
// type a bit, make a line break and type again
|
// type a bit, make a line break and type again
|
||||||
const $firstTextElement = inner$('div span').first();
|
const $firstTextElement = inner$('div span').first();
|
||||||
|
@ -147,19 +147,19 @@ describe('indentation button', function () {
|
||||||
helper.selectLines($firstLine, $secondLine);
|
helper.selectLines($firstLine, $secondLine);
|
||||||
|
|
||||||
const $indentButton = chrome$('.buttonicon-indent');
|
const $indentButton = chrome$('.buttonicon-indent');
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1);
|
||||||
|
|
||||||
// apply bold
|
// apply bold
|
||||||
const $boldButton = chrome$('.buttonicon-bold');
|
const $boldButton = chrome$('.buttonicon-bold');
|
||||||
$boldButton.click();
|
$boldButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('b').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('b').length === 1);
|
||||||
|
|
||||||
// outdent first 2 lines
|
// outdent first 2 lines
|
||||||
const $outdentButton = chrome$('.buttonicon-outdent');
|
const $outdentButton = chrome$('.buttonicon-outdent');
|
||||||
$outdentButton.click();
|
$outdentButton.trigger('click');
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 0);
|
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 0);
|
||||||
|
|
||||||
// check if '*' is displayed
|
// check if '*' is displayed
|
||||||
|
@ -179,7 +179,7 @@ describe('indentation button', function () {
|
||||||
|
|
||||||
// get the indentation button and click it
|
// get the indentation button and click it
|
||||||
const $indentButton = helper.$getPadChrome().find('.buttonicon-indent');
|
const $indentButton = helper.$getPadChrome().find('.buttonicon-indent');
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
let newFirstTextElement = $inner.find('div').first();
|
let newFirstTextElement = $inner.find('div').first();
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ describe('indentation button', function () {
|
||||||
expect(isLI).to.be(true);
|
expect(isLI).to.be(true);
|
||||||
|
|
||||||
// indent again
|
// indent again
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
newFirstTextElement = $inner.find('div').first();
|
newFirstTextElement = $inner.find('div').first();
|
||||||
|
|
||||||
|
@ -215,8 +215,8 @@ describe('indentation button', function () {
|
||||||
|
|
||||||
// get the unindentation button and click it twice
|
// get the unindentation button and click it twice
|
||||||
const $outdentButton = helper.$getPadChrome().find('.buttonicon-outdent');
|
const $outdentButton = helper.$getPadChrome().find('.buttonicon-outdent');
|
||||||
$outdentButton.click();
|
$outdentButton.trigger('click');
|
||||||
$outdentButton.click();
|
$outdentButton.trigger('click');
|
||||||
|
|
||||||
newFirstTextElement = $inner.find('div').first();
|
newFirstTextElement = $inner.find('div').first();
|
||||||
|
|
||||||
|
@ -242,8 +242,8 @@ describe('indentation button', function () {
|
||||||
helper.selectText(firstTextElement[0], $inner);
|
helper.selectText(firstTextElement[0], $inner);
|
||||||
|
|
||||||
// indent twice
|
// indent twice
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
$indentButton.click();
|
$indentButton.trigger('click');
|
||||||
|
|
||||||
// get the first text element out of the inner iframe
|
// get the first text element out of the inner iframe
|
||||||
firstTextElement = $inner.find('div').first();
|
firstTextElement = $inner.find('div').first();
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe('italic some text', function () {
|
||||||
|
|
||||||
// get the bold button and click it
|
// get the bold button and click it
|
||||||
const $boldButton = chrome$('.buttonicon-italic');
|
const $boldButton = chrome$('.buttonicon-italic');
|
||||||
$boldButton.click();
|
$boldButton.trigger('click');
|
||||||
|
|
||||||
// ace creates a new dom element when you press a button, just get the first text element again
|
// ace creates a new dom element when you press a button, just get the first text element again
|
||||||
const $newFirstTextElement = inner$('div').first();
|
const $newFirstTextElement = inner$('div').first();
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe('Language select and change', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $settingsButton = chrome$('.buttonicon-settings');
|
const $settingsButton = chrome$('.buttonicon-settings');
|
||||||
$settingsButton.click();
|
$settingsButton.trigger('click');
|
||||||
|
|
||||||
// click the language button
|
// click the language button
|
||||||
const $language = chrome$('#languagemenu');
|
const $language = chrome$('#languagemenu');
|
||||||
|
@ -24,7 +24,7 @@ describe('Language select and change', function () {
|
||||||
|
|
||||||
// select german
|
// select german
|
||||||
$languageoption.attr('selected', 'selected');
|
$languageoption.attr('selected', 'selected');
|
||||||
$language.change();
|
$language.trigger('change');
|
||||||
|
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)');
|
() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)');
|
||||||
|
@ -45,13 +45,13 @@ describe('Language select and change', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $settingsButton = chrome$('.buttonicon-settings');
|
const $settingsButton = chrome$('.buttonicon-settings');
|
||||||
$settingsButton.click();
|
$settingsButton.trigger('click');
|
||||||
|
|
||||||
// click the language button
|
// click the language button
|
||||||
const $language = chrome$('#languagemenu');
|
const $language = chrome$('#languagemenu');
|
||||||
// select english
|
// select english
|
||||||
$language.val('en');
|
$language.val('en');
|
||||||
$language.change();
|
$language.trigger('change');
|
||||||
|
|
||||||
// get the value of the bold button
|
// get the value of the bold button
|
||||||
let $boldButton = chrome$('.buttonicon-bold').parent();
|
let $boldButton = chrome$('.buttonicon-bold').parent();
|
||||||
|
@ -79,7 +79,7 @@ describe('Language select and change', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $settingsButton = chrome$('.buttonicon-settings');
|
const $settingsButton = chrome$('.buttonicon-settings');
|
||||||
$settingsButton.click();
|
$settingsButton.trigger('click');
|
||||||
|
|
||||||
// click the language button
|
// click the language button
|
||||||
const $language = chrome$('#languagemenu');
|
const $language = chrome$('#languagemenu');
|
||||||
|
@ -88,7 +88,7 @@ describe('Language select and change', function () {
|
||||||
// select arabic
|
// select arabic
|
||||||
// $languageoption.attr('selected','selected'); // Breaks the test..
|
// $languageoption.attr('selected','selected'); // Breaks the test..
|
||||||
$language.val('ar');
|
$language.val('ar');
|
||||||
$languageoption.change();
|
$languageoption.trigger('change');
|
||||||
|
|
||||||
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'ltr');
|
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'ltr');
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ describe('Language select and change', function () {
|
||||||
|
|
||||||
// click on the settings button to make settings visible
|
// click on the settings button to make settings visible
|
||||||
const $settingsButton = chrome$('.buttonicon-settings');
|
const $settingsButton = chrome$('.buttonicon-settings');
|
||||||
$settingsButton.click();
|
$settingsButton.trigger('click');
|
||||||
|
|
||||||
// click the language button
|
// click the language button
|
||||||
const $language = chrome$('#languagemenu');
|
const $language = chrome$('#languagemenu');
|
||||||
|
@ -111,7 +111,7 @@ describe('Language select and change', function () {
|
||||||
// select arabic
|
// select arabic
|
||||||
$languageoption.attr('selected', 'selected');
|
$languageoption.attr('selected', 'selected');
|
||||||
$language.val('en');
|
$language.val('en');
|
||||||
$languageoption.change();
|
$languageoption.trigger('change');
|
||||||
|
|
||||||
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'rtl');
|
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'rtl');
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('author of pad edition', function () {
|
||||||
|
|
||||||
// get the clear authorship colors button and click it
|
// get the clear authorship colors button and click it
|
||||||
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
|
||||||
$clearauthorshipcolorsButton.click();
|
$clearauthorshipcolorsButton.trigger('click');
|
||||||
|
|
||||||
// does the first divs span include an author class?
|
// does the first divs span include an author class?
|
||||||
const hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1;
|
const hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1;
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('ordered_list.js', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('ol li').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('ol li').length === 1);
|
||||||
});
|
});
|
||||||
|
@ -115,10 +115,10 @@ describe('ordered_list.js', function () {
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||||
const $firstLine = inner$('div').first();
|
const $firstLine = inner$('div').first();
|
||||||
$firstLine.sendkeys('{selectall}');
|
$firstLine.sendkeys('{selectall}');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
const $secondLine = inner$('div').first().next();
|
const $secondLine = inner$('div').first().next();
|
||||||
$secondLine.sendkeys('{selectall}');
|
$secondLine.sendkeys('{selectall}');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
expect($secondLine.find('ol').attr('start') === 2);
|
expect($secondLine.find('ol').attr('start') === 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ describe('ordered_list.js', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
// type a bit, make a line break and type again
|
// type a bit, make a line break and type again
|
||||||
const $firstTextElement = inner$('div span').first();
|
const $firstTextElement = inner$('div span').first();
|
||||||
|
@ -182,7 +182,7 @@ describe('ordered_list.js', function () {
|
||||||
$firstTextElement.sendkeys('{selectall}');
|
$firstTextElement.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
const e = new inner$.Event(helper.evtType);
|
const e = new inner$.Event(helper.evtType);
|
||||||
e.keyCode = 9; // tab
|
e.keyCode = 9; // tab
|
||||||
|
@ -217,15 +217,15 @@ describe('ordered_list.js', function () {
|
||||||
$firstTextElement.sendkeys('{selectall}');
|
$firstTextElement.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
const $indentButton = chrome$('.buttonicon-indent');
|
const $indentButton = chrome$('.buttonicon-indent');
|
||||||
$indentButton.click(); // make it indented twice
|
$indentButton.trigger('click'); // make it indented twice
|
||||||
|
|
||||||
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
|
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
|
||||||
|
|
||||||
const $outdentButton = chrome$('.buttonicon-outdent');
|
const $outdentButton = chrome$('.buttonicon-outdent');
|
||||||
$outdentButton.click(); // make it deindented to 1
|
$outdentButton.trigger('click'); // make it deindented to 1
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,16 +71,16 @@ describe('Pad modal', function () {
|
||||||
|
|
||||||
const clickOnPadInner = () => {
|
const clickOnPadInner = () => {
|
||||||
const $editor = helper.padInner$('#innerdocbody');
|
const $editor = helper.padInner$('#innerdocbody');
|
||||||
$editor.click();
|
$editor.trigger('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickOnPadOuter = () => {
|
const clickOnPadOuter = () => {
|
||||||
const $lineNumbersColumn = helper.padOuter$('#sidedivinner');
|
const $lineNumbersColumn = helper.padOuter$('#sidedivinner');
|
||||||
$lineNumbersColumn.click();
|
$lineNumbersColumn.trigger('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSettingsAndWaitForModalToBeVisible = async () => {
|
const openSettingsAndWaitForModalToBeVisible = async () => {
|
||||||
helper.padChrome$('.buttonicon-settings').click();
|
helper.padChrome$('.buttonicon-settings').trigger('click');
|
||||||
|
|
||||||
// wait for modal to be displayed
|
// wait for modal to be displayed
|
||||||
const modalSelector = '#settings';
|
const modalSelector = '#settings';
|
||||||
|
|
|
@ -22,8 +22,8 @@ describe('undo button then redo button', function () {
|
||||||
const $undoButton = chrome$('.buttonicon-undo');
|
const $undoButton = chrome$('.buttonicon-undo');
|
||||||
const $redoButton = chrome$('.buttonicon-redo');
|
const $redoButton = chrome$('.buttonicon-redo');
|
||||||
// click the buttons
|
// click the buttons
|
||||||
$undoButton.click(); // removes foo
|
$undoButton.trigger('click'); // removes foo
|
||||||
$redoButton.click(); // resends foo
|
$redoButton.trigger('click'); // resends foo
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div span').first().text() === newString);
|
await helper.waitForPromise(() => inner$('div span').first().text() === newString);
|
||||||
const finalValue = inner$('div').first().text();
|
const finalValue = inner$('div').first().text();
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe('select formatting buttons when selection has style applied', function
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
selectLine(line);
|
selectLine(line);
|
||||||
const $formattingButton = chrome$(`.buttonicon-${style}`);
|
const $formattingButton = chrome$(`.buttonicon-${style}`);
|
||||||
$formattingButton.click();
|
$formattingButton.trigger('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isButtonSelected = function (style) {
|
const isButtonSelected = function (style) {
|
||||||
|
@ -37,7 +37,7 @@ describe('select formatting buttons when selection has style applied', function
|
||||||
const undo = async function () {
|
const undo = async function () {
|
||||||
const originalHTML = helper.padInner$('body').html();
|
const originalHTML = helper.padInner$('body').html();
|
||||||
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
const $undoButton = helper.padChrome$('.buttonicon-undo');
|
||||||
$undoButton.click();
|
$undoButton.trigger('click');
|
||||||
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe('strikethrough button', function () {
|
||||||
|
|
||||||
// get the strikethrough button and click it
|
// get the strikethrough button and click it
|
||||||
const $strikethroughButton = chrome$('.buttonicon-strikethrough');
|
const $strikethroughButton = chrome$('.buttonicon-strikethrough');
|
||||||
$strikethroughButton.click();
|
$strikethroughButton.trigger('click');
|
||||||
|
|
||||||
// ace creates a new dom element when you press a button, just get the first text element again
|
// ace creates a new dom element when you press a button, just get the first text element again
|
||||||
const $newFirstTextElement = inner$('div').first();
|
const $newFirstTextElement = inner$('div').first();
|
||||||
|
|
|
@ -22,7 +22,7 @@ xdescribe('timeslider button takes you to the timeslider of a pad', function ()
|
||||||
await helper.waitForPromise(() => modifiedValue !== originalValue);
|
await helper.waitForPromise(() => modifiedValue !== originalValue);
|
||||||
|
|
||||||
const $timesliderButton = chrome$('#timesliderlink');
|
const $timesliderButton = chrome$('#timesliderlink');
|
||||||
$timesliderButton.click(); // So click the timeslider link
|
$timesliderButton.trigger('click'); // So click the timeslider link
|
||||||
|
|
||||||
await helper.waitForPromise(() => {
|
await helper.waitForPromise(() => {
|
||||||
const iFrameURL = chrome$.window.location.href;
|
const iFrameURL = chrome$.window.location.href;
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('timeslider follow', function () {
|
||||||
|
|
||||||
// set to follow contents as it arrives
|
// set to follow contents as it arrives
|
||||||
helper.contentWindow().$('#options-followContents').prop('checked', true);
|
helper.contentWindow().$('#options-followContents').prop('checked', true);
|
||||||
helper.contentWindow().$('#playpause_button_icon').click();
|
helper.contentWindow().$('#playpause_button_icon').trigger('click');
|
||||||
|
|
||||||
let newTop;
|
let newTop;
|
||||||
await helper.waitForPromise(() => {
|
await helper.waitForPromise(() => {
|
||||||
|
@ -64,27 +64,27 @@ describe('timeslider follow', function () {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// line 40 changed
|
// line 40 changed
|
||||||
helper.contentWindow().$('#leftstep').click();
|
helper.contentWindow().$('#leftstep').trigger('click');
|
||||||
await helper.waitForPromise(() => hasFollowedToLine(40));
|
await helper.waitForPromise(() => hasFollowedToLine(40));
|
||||||
|
|
||||||
// line 1 is the first line that changed
|
// line 1 is the first line that changed
|
||||||
helper.contentWindow().$('#leftstep').click();
|
helper.contentWindow().$('#leftstep').trigger('click');
|
||||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||||
|
|
||||||
// line 1 changed
|
// line 1 changed
|
||||||
helper.contentWindow().$('#leftstep').click();
|
helper.contentWindow().$('#leftstep').trigger('click');
|
||||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||||
|
|
||||||
// line 1 changed
|
// line 1 changed
|
||||||
helper.contentWindow().$('#rightstep').click();
|
helper.contentWindow().$('#rightstep').trigger('click');
|
||||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||||
|
|
||||||
// line 1 is the first line that changed
|
// line 1 is the first line that changed
|
||||||
helper.contentWindow().$('#rightstep').click();
|
helper.contentWindow().$('#rightstep').trigger('click');
|
||||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||||
|
|
||||||
// line 40 changed
|
// line 40 changed
|
||||||
helper.contentWindow().$('#rightstep').click();
|
helper.contentWindow().$('#rightstep').trigger('click');
|
||||||
helper.waitForPromise(() => hasFollowedToLine(40));
|
helper.waitForPromise(() => hasFollowedToLine(40));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('timeslider', function () {
|
||||||
|
|
||||||
// Create a bunch of revisions.
|
// Create a bunch of revisions.
|
||||||
for (let i = 0; i < 99; i++) await helper.edit('a');
|
for (let i = 0; i < 99; i++) await helper.edit('a');
|
||||||
chrome$('.buttonicon-savedRevision').click();
|
chrome$('.buttonicon-savedRevision').trigger('click');
|
||||||
await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0);
|
await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0);
|
||||||
// Give some time to send the SAVE_REVISION message to the server before navigating away.
|
// Give some time to send the SAVE_REVISION message to the server before navigating away.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe('undo button', function () {
|
||||||
// get clear authorship button as a variable
|
// get clear authorship button as a variable
|
||||||
const $undoButton = chrome$('.buttonicon-undo');
|
const $undoButton = chrome$('.buttonicon-undo');
|
||||||
// click the button
|
// click the button
|
||||||
$undoButton.click();
|
$undoButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div span').first().text() === originalValue);
|
await helper.waitForPromise(() => inner$('div span').first().text() === originalValue);
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe('unordered_list.js', function () {
|
||||||
const originalText = inner$('div').first().text();
|
const originalText = inner$('div').first().text();
|
||||||
|
|
||||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertunorderedlistButton.click();
|
$insertunorderedlistButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => {
|
await helper.waitForPromise(() => {
|
||||||
const newText = inner$('div').first().text();
|
const newText = inner$('div').first().text();
|
||||||
|
@ -21,7 +21,7 @@ describe('unordered_list.js', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove indentation by bullet and ensure text string remains the same
|
// remove indentation by bullet and ensure text string remains the same
|
||||||
chrome$('.buttonicon-outdent').click();
|
chrome$('.buttonicon-outdent').trigger('click');
|
||||||
await helper.waitForPromise(() => inner$('div').first().text() === originalText);
|
await helper.waitForPromise(() => inner$('div').first().text() === originalText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +38,7 @@ describe('unordered_list.js', function () {
|
||||||
const originalText = inner$('div').first().text();
|
const originalText = inner$('div').first().text();
|
||||||
|
|
||||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertunorderedlistButton.click();
|
$insertunorderedlistButton.trigger('click');
|
||||||
|
|
||||||
await helper.waitForPromise(() => {
|
await helper.waitForPromise(() => {
|
||||||
const newText = inner$('div').first().text();
|
const newText = inner$('div').first().text();
|
||||||
|
@ -46,7 +46,7 @@ describe('unordered_list.js', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove indentation by bullet and ensure text string remains the same
|
// remove indentation by bullet and ensure text string remains the same
|
||||||
$insertunorderedlistButton.click();
|
$insertunorderedlistButton.trigger('click');
|
||||||
await helper.waitForPromise(() => inner$('div').find('ul').length !== 1);
|
await helper.waitForPromise(() => inner$('div').find('ul').length !== 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -63,7 +63,7 @@ describe('unordered_list.js', function () {
|
||||||
const chrome$ = helper.padChrome$;
|
const chrome$ = helper.padChrome$;
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
// type a bit, make a line break and type again
|
// type a bit, make a line break and type again
|
||||||
const $firstTextElement = inner$('div span').first();
|
const $firstTextElement = inner$('div span').first();
|
||||||
|
@ -98,7 +98,7 @@ describe('unordered_list.js', function () {
|
||||||
$firstTextElement.sendkeys('{selectall}');
|
$firstTextElement.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertorderedlistButton.click();
|
$insertorderedlistButton.trigger('click');
|
||||||
|
|
||||||
const e = new inner$.Event(helper.evtType);
|
const e = new inner$.Event(helper.evtType);
|
||||||
e.keyCode = 9; // tab
|
e.keyCode = 9; // tab
|
||||||
|
@ -131,14 +131,14 @@ describe('unordered_list.js', function () {
|
||||||
$firstTextElement.sendkeys('{selectall}');
|
$firstTextElement.sendkeys('{selectall}');
|
||||||
|
|
||||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||||
$insertunorderedlistButton.click();
|
$insertunorderedlistButton.trigger('click');
|
||||||
|
|
||||||
const $indentButton = chrome$('.buttonicon-indent');
|
const $indentButton = chrome$('.buttonicon-indent');
|
||||||
$indentButton.click(); // make it indented twice
|
$indentButton.trigger('click'); // make it indented twice
|
||||||
|
|
||||||
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
|
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
|
||||||
const $outdentButton = chrome$('.buttonicon-outdent');
|
const $outdentButton = chrome$('.buttonicon-outdent');
|
||||||
$outdentButton.click(); // make it deindented to 1
|
$outdentButton.trigger('click'); // make it deindented to 1
|
||||||
|
|
||||||
await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1);
|
await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,7 +32,7 @@ describe('Automatic pad reload on Force Reconnect message', function () {
|
||||||
context('and user clicks on Cancel', function () {
|
context('and user clicks on Cancel', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
|
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
|
||||||
$errorMessageModal.find('#cancelreconnect').click();
|
$errorMessageModal.find('#cancelreconnect').trigger('click');
|
||||||
await helper.waitForPromise(
|
await helper.waitForPromise(
|
||||||
() => helper.padChrome$('#connectivity .userdup').is(':visible') === true);
|
() => helper.padChrome$('#connectivity .userdup').is(':visible') === true);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue