From 74ceaf23549240bded0a89ae038258bcff6d27a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20=C2=B7=20ASEnough?= <49665315+alexliluz@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:29:31 +0800 Subject: [PATCH 01/34] Fix Debian cross-linking with xx-cargo (#7524) * Fix Debian cross-linking with xx-cargo * Fix SC2155 in Debian cross builds --- docker/Dockerfile.debian | 14 ++++++++++++-- docker/Dockerfile.j2 | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 7ebb07bd..9e2e6016 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -96,8 +96,13 @@ ARG DB=sqlite,mysql,postgresql # dummy project, except the target folder # This folder contains the compiled dependencies RUN source /env-cargo && \ - # Workaround for xx related build issues + # Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ find . -not -path "./target*" -delete @@ -113,8 +118,13 @@ RUN source /env-cargo && \ # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage - # Workaround for xx related build issues + # Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index 5e33d512..d8b9c8c6 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -28,8 +28,13 @@ # [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # {% macro xx_cargo_config() -%} -# Workaround for xx related build issues +# Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" {%- endmacro %} FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault From 55f883a5669a5b1c0227bc8341e7a2899da20660 Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 5 Aug 2026 19:29:41 +0000 Subject: [PATCH 02/34] Fix playwright test (#7548) * Config server setting suppressOnboardingInterstitials * Backport fix playwright tests --------- Co-authored-by: Timshel --- .env.template | 8 + playwright/.env.template | 16 +- playwright/README.md | 30 +- playwright/compose/keycloak/setup.sh | 4 +- playwright/compose/playwright/Dockerfile | 2 +- playwright/compose/warden/Dockerfile | 1 + playwright/compose/warden/build.sh | 11 + playwright/docker-compose.yml | 15 +- playwright/global-setup.ts | 2 +- playwright/global-utils.ts | 15 +- playwright/package-lock.json | 1162 +++++++++-------- playwright/package.json | 14 +- playwright/playwright.config.ts | 14 +- playwright/test.env | 10 +- playwright/tests/collection.spec.ts | 16 +- playwright/tests/cyphers.spec.ts | 56 + playwright/tests/login.smtp.spec.ts | 31 +- playwright/tests/login.spec.ts | 4 +- playwright/tests/organization.smtp.spec.ts | 44 +- playwright/tests/secrets.spec.ts | 110 ++ playwright/tests/send.spec.ts | 16 +- playwright/tests/setups/2fa.ts | 15 +- playwright/tests/setups/admin.ts | 21 + playwright/tests/setups/db-teardown.ts | 2 +- playwright/tests/setups/orgs.ts | 25 +- playwright/tests/setups/sso-teardown.ts | 2 +- playwright/tests/setups/sso.ts | 25 +- playwright/tests/setups/user.ts | 34 +- playwright/tests/sso_login.smtp.spec.ts | 55 +- playwright/tests/sso_login.spec.ts | 10 +- .../tests/sso_organization.smtp.spec.ts | 16 +- playwright/tests/sso_organization.spec.ts | 27 +- src/api/core/mod.rs | 2 +- src/config.rs | 5 + 34 files changed, 1042 insertions(+), 778 deletions(-) create mode 100644 playwright/tests/cyphers.spec.ts create mode 100644 playwright/tests/secrets.spec.ts create mode 100644 playwright/tests/setups/admin.ts diff --git a/.env.template b/.env.template index fd7c2fd2..9fc29989 100644 --- a/.env.template +++ b/.env.template @@ -316,6 +316,14 @@ ## unauthenticated access to potentially sensitive data. # SHOW_PASSWORD_HINT=false +######################### +### Client settings ### +######################### + +## Control whether clients onboarding interstitials are suppressed +## (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) +# CLIENT_SUPPRESS_ONBOARDING=false + ######################### ### Advanced settings ### ######################### diff --git a/playwright/.env.template b/playwright/.env.template index a6696aab..4ead281d 100644 --- a/playwright/.env.template +++ b/playwright/.env.template @@ -21,11 +21,19 @@ TEST_USER3=test3 TEST_USER3_PASSWORD=${TEST_USER3} TEST_USER3_MAIL=${TEST_USER3}@yopmail.com +TEST_USER4=test4 +TEST_USER4_PASSWORD=${TEST_USER4} +TEST_USER4_MAIL=${TEST_USER4}@yopmail.com + +TEST_USER5=test5 +TEST_USER5_PASSWORD=${TEST_USER5} +TEST_USER5_MAIL=${TEST_USER5}@yopmail.com + ################### # Keycloak Config # ################### -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KC_HTTP_HOST=127.0.0.1 KC_HTTP_PORT=8080 @@ -39,8 +47,10 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} ###################### ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=8000 -DOMAIN=http://localhost:${ROCKET_PORT} +ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} +DOMAIN=https://127.0.0.1:${ROCKET_PORT} LOG_LEVEL=info,oidcwarden::sso=debug +SSO_DEBUG_TOKENS=true I_REALLY_WANT_VOLATILE_STORAGE=true SSO_ENABLED=true diff --git a/playwright/README.md b/playwright/README.md index a27e6105..000725d7 100644 --- a/playwright/README.md +++ b/playwright/README.md @@ -1,8 +1,8 @@ # Integration tests This allows running integration tests using [Playwright](https://playwright.dev/). - -It uses its own `test.env` with different ports to not collide with a running dev instance. +\ +It usse its own [test.env](/test/scenarios/test.env) with different ports to not collide with a running dev instance. ## Install @@ -11,11 +11,11 @@ Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in conta ### Running Playwright outside docker -It is possible to run `Playwright` outside of the container, this removes the need to rebuild the image for each change. -You will additionally need `nodejs` then run: +It's possible to run `Playwright` outside of the container, this remove the need to rebuild the image for each change. +You'll additionally need `nodejs` then run: ```bash -npm ci --ignore-scripts +npm ci --ignore-scripts --allow-git=none --allow-remote=none npx playwright install-deps npx playwright install firefox ``` @@ -65,7 +65,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests): ```bash -PW_KEEP_SERVICE_RUNNNING=true npx playwright test +PW_KEEP_SERVICE_RUNNING=true npx playwright test ``` ### Running specific tests @@ -77,7 +77,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login ``` -To run only a specifc test (It might fail if it has dependency): +To run only a specific test (It might fail if it has dependency): ```bash DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation" @@ -92,7 +92,7 @@ This does not start the server, you will need to start it manually. ```bash DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden -npx playwright codegen "http://127.0.0.1:8003" +npx playwright codegen "https://127.0.0.1:8000" --ignore-https-errors ``` ## Override web-vault @@ -112,12 +112,11 @@ You can check the result running: DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden ``` -Then check `http://127.0.0.1:8003/admin/diagnostics` with `admin`. +Then check `https://127.0.0.1:8003/admin/diagnostics` with `admin`. # OpenID Connect test setup -Additionally this `docker-compose` template allows to run locally Vaultwarden, -[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. +Additionally this `docker-compose` template allow to run locally `Vaultwarden`, [Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. ## Setup @@ -131,18 +130,17 @@ Then start the stack (the `profile` is required to run `Vaultwarden`) : ```bash > docker compose --profile vaultwarden --env-file .env up .... -keycloakSetup_1 | Logging into http://127.0.0.1:8080 as user admin of realm master +keycloakSetup_1 | Logging into https://127.0.0.1:8080 as user admin of realm master keycloakSetup_1 | Created new realm with id 'test' keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e oidc_keycloakSetup_1 exited with code 0 ``` -Wait until `oidc_keycloakSetup_1 exited with code 0` which indicates the correct setup of the Keycloak realm, client and user -(It is normal for this container to stop once the configuration is done). +Wait until `oidc_keycloakSetup_1 exited with code 0` which indicate the correct setup of the Keycloak realm, client and user (It's normal for this container to stop once the configuration is done). Then you can access : -- `Vaultwarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`. +- `Vaultwarden` on https://0.0.0.0:8000 with the default user `test@yopmail.com/test`. - `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin` - `Maildev` on http://0.0.0.0:1080 @@ -171,7 +169,7 @@ docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild V All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template). The content of the file will be loaded as environment variables in all containers. -- `keycloak` [configuration](https://www.keycloak.org/server/all-config) includes `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). +- `keycloak` [configuration](https://www.keycloak.org/server/all-config) include `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). - All `Vaultwarden` configuration can be set (EX: `SMTP_*`) ## Cleanup diff --git a/playwright/compose/keycloak/setup.sh b/playwright/compose/keycloak/setup.sh index a27caaff..f1d8a303 100755 --- a/playwright/compose/keycloak/setup.sh +++ b/playwright/compose/keycloak/setup.sh @@ -17,7 +17,7 @@ done set -e -kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli +kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600" kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i @@ -39,6 +39,6 @@ kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLi # THEN in another terminal: # docker exec -it keycloakSetup-dev /bin/bash # export PATH=$PATH:/opt/keycloak/bin -# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli +# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli # ENJOY # Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html diff --git a/playwright/compose/playwright/Dockerfile b/playwright/compose/playwright/Dockerfile index 4dae1ae4..6b48c7dc 100644 --- a/playwright/compose/playwright/Dockerfile +++ b/playwright/compose/playwright/Dockerfile @@ -28,7 +28,7 @@ RUN mkdir /playwright WORKDIR /playwright COPY package.json package-lock.json . -RUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox +RUN npm ci --ignore-scripts --allow-git=none --allow-remote=none && npx playwright install-deps && npx playwright install firefox COPY docker-compose.yml test.env ./ COPY compose ./compose diff --git a/playwright/compose/warden/Dockerfile b/playwright/compose/warden/Dockerfile index e472d207..9a369dab 100644 --- a/playwright/compose/warden/Dockerfile +++ b/playwright/compose/warden/Dockerfile @@ -35,6 +35,7 @@ WORKDIR / COPY --from=prebuilt /start.sh . COPY --from=prebuilt /vaultwarden . +COPY --from=build /data ./data COPY --from=build /web-vault ./web-vault ENTRYPOINT ["/start.sh"] diff --git a/playwright/compose/warden/build.sh b/playwright/compose/warden/build.sh index 37e9a25e..ee8b47fe 100755 --- a/playwright/compose/warden/build.sh +++ b/playwright/compose/warden/build.sh @@ -22,3 +22,14 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then mv build /web-vault fi + +# Lower the KDF iterations default for faster tests. +sed -i 's/(6e5,2e6,6e5)/(1e5,2e6,1e5)/' /web-vault/app/main.*.js + +# Generate a self signed cert +mkdir -p /data/ssl; cd /data/ssl + +openssl req -x509 -out localhost.crt -keyout localhost.key \ + -newkey rsa:2048 -nodes -sha256 \ + -subj '/CN=localhost' -extensions EXT -config <( \ + printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") diff --git a/playwright/docker-compose.yml b/playwright/docker-compose.yml index f4402326..5dd04ff4 100644 --- a/playwright/docker-compose.yml +++ b/playwright/docker-compose.yml @@ -24,12 +24,15 @@ services: environment: - ADMIN_TOKEN - DATABASE_URL + - CLIENT_SUPPRESS_ONBOARDING + - EMAIL_2FA_AUTO_FALLBACK - I_REALLY_WANT_VOLATILE_STORAGE - LOG_LEVEL - LOGIN_RATELIMIT_MAX_BURST - SMTP_HOST - SMTP_FROM - SMTP_DEBUG + - SSO_AUTH_ONLY_NOT_SESSION - SSO_DEBUG_TOKENS - SSO_ENABLED - SSO_FRONTEND @@ -70,7 +73,7 @@ services: Mysql: profiles: ["playwright"] container_name: playwright_mysql - image: mysql:8.4.1 + image: mysql:9.7.0 env_file: test.env healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] @@ -82,7 +85,7 @@ services: Postgres: profiles: ["playwright"] container_name: playwright_postgres - image: postgres:16.3 + image: postgres:18.4 env_file: test.env healthcheck: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] @@ -94,7 +97,7 @@ services: Maildev: profiles: ["vaultwarden", "maildev"] container_name: maildev - image: timshel/maildev:3.0.4 + image: timshel/maildev:3.2.19 ports: - ${SMTP_PORT}:1025 - 1080:1080 @@ -102,7 +105,7 @@ services: Keycloak: profiles: ["keycloak", "vaultwarden"] container_name: keycloak-${ENV:-dev} - image: quay.io/keycloak/keycloak:26.3.4 + image: quay.io/keycloak/keycloak:26.6.2 network_mode: "host" command: - start-dev @@ -112,12 +115,12 @@ services: profiles: ["keycloak", "vaultwarden"] container_name: keycloakSetup-${ENV:-dev} image: keycloak_setup-${ENV:-dev} + network_mode: "host" build: context: compose/keycloak dockerfile: Dockerfile args: - KEYCLOAK_VERSION: 26.3.4 - network_mode: "host" + KEYCLOAK_VERSION: 26.6.2 depends_on: - Keycloak restart: "no" diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts index 89405f12..9959d247 100644 --- a/playwright/global-setup.ts +++ b/playwright/global-setup.ts @@ -1,4 +1,4 @@ -import { firefox, type FullConfig } from '@playwright/test'; +import { type FullConfig } from '@playwright/test'; import { execSync } from 'node:child_process'; import fs from 'fs'; diff --git a/playwright/global-utils.ts b/playwright/global-utils.ts index 224bb4b8..937de651 100644 --- a/playwright/global-utils.ts +++ b/playwright/global-utils.ts @@ -207,7 +207,7 @@ export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, } export async function stopVault(force: boolean = false) { - if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) { + if( force === false && process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`); } else { console.log(`Vaultwarden stopping`); @@ -231,6 +231,7 @@ export async function checkNotification(page: Page, hasText: string) { } export async function cleanLanding(page: Page) { + await page.context().clearCookies(); await page.goto('/', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('button').nth(0)).toBeVisible(); @@ -248,15 +249,3 @@ export async function logout(test: Test, page: Page, user: { name: string }) { await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible(); }); } - -export async function ignoreExtension(page: Page) { - await page.waitForLoadState('domcontentloaded'); - - try { - await page.getByRole('button', { name: 'Add it later' }).click({timeout: 5_000}); - await page.getByRole('link', { name: 'Skip to web app' }).click(); - } catch (error) { - console.log('Extension setup not visible. Continuing'); - } - -} diff --git a/playwright/package-lock.json b/playwright/package-lock.json index 2f4cd0c1..57f5bcaf 100644 --- a/playwright/package-lock.json +++ b/playwright/package-lock.json @@ -9,41 +9,56 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "mysql2": "3.15.3", - "otpauth": "9.4.1", - "pg": "8.16.3" + "mysql2": "3.22.3", + "otpauth": "9.5.1", + "pg": "8.21.0" }, "devDependencies": { - "@playwright/test": "1.56.1", - "dotenv": "17.2.3", - "dotenv-expand": "12.0.3", - "maildev": "npm:@timshel_npm/maildev@3.2.5" + "@playwright/test": "1.60.0", + "dotenv": "17.4.2", + "dotenv-expand": "13.0.0", + "maildev": "npm:@timshel_npm/maildev@3.2.19" } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", - "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.1" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz", - "integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.2" + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -52,10 +67,22 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -68,13 +95,13 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -87,17 +114,17 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", "dev": true, "funding": [ { @@ -110,21 +137,21 @@ } ], "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -137,16 +164,16 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz", - "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", "dev": true, "funding": [ { @@ -158,14 +185,19 @@ "url": "https://opencollective.com/csstools" } ], - "engines": { - "node": ">=18" + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -178,27 +210,44 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "dependencies": { - "playwright": "1.56.1" + "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -246,12 +295,11 @@ } }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", - "dev": true, + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/trusted-types": { @@ -261,6 +309,38 @@ "dev": true, "optional": true }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "dev": true, + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/@zone-eu/mailsplit/node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -289,15 +369,6 @@ "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==", "dev": true }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -312,15 +383,6 @@ "node": ">= 6.0.0" } }, - "node_modules/base32.js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", - "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -340,29 +402,33 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -376,6 +442,22 @@ } } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -421,9 +503,9 @@ } }, "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "engines": { "node": ">=20" @@ -460,15 +542,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -499,9 +582,9 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "dependencies": { "object-assign": "^4", @@ -509,46 +592,36 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/cssstyle": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", - "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", - "dev": true, - "dependencies": { - "@asamuzakjp/css-color": "^4.0.3", - "@csstools/css-syntax-patches-for-csstree": "^1.0.14", - "css-tree": "^3.1.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { @@ -634,9 +707,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "dev": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -657,9 +730,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "engines": { "node": ">=12" @@ -669,12 +742,12 @@ } }, "node_modules/dotenv-expand": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", - "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-13.0.0.tgz", + "integrity": "sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==", "dev": true, "dependencies": { - "dotenv": "^16.4.5" + "dotenv": "^17.4.2" }, "engines": { "node": ">=12" @@ -683,18 +756,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -734,20 +795,21 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", + "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", "dev": true, "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.20.1" }, "engines": { "node": ">=10.2.0" @@ -776,9 +838,9 @@ } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -828,27 +890,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -907,18 +948,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -949,9 +991,9 @@ } }, "node_modules/express/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -972,9 +1014,9 @@ "dev": true }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "dependencies": { "debug": "^4.4.0", @@ -985,13 +1027,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -1122,9 +1168,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -1143,15 +1189,15 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-to-text": { @@ -1190,102 +1236,25 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "node": ">= 0.8" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1337,34 +1306,35 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" }, "node_modules/jsdom": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", - "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", - "dev": true, - "dependencies": { - "@asamuzakjp/dom-selector": "^6.7.2", - "cssstyle": "^5.3.1", - "data-urls": "^6.0.0", + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.0.tgz", + "integrity": "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "rrweb-cssom": "^0.8.0", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -1391,17 +1361,33 @@ "dev": true }, "node_modules/libmime": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", - "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz", + "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==", "dev": true, "dependencies": { "encoding-japanese": "2.2.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.2", "libbase64": "1.3.0", "libqp": "2.1.1" } }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/libqp": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", @@ -1423,18 +1409,18 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", "dev": true, "engines": { "node": "20 || >=22" } }, "node_modules/lru.min": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", - "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "engines": { "bun": ">=1.0.0", "deno": ">=1.30.0", @@ -1447,25 +1433,25 @@ }, "node_modules/maildev": { "name": "@timshel_npm/maildev", - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.5.tgz", - "integrity": "sha512-suWQu2s2kmO+MXtNJYW9peklznhd+aorIUb4tSNrfaKoEJjDa3vLXTvWf+3cb67o4Yv4Z6nPeKdMTCDZVn/Nyw==", + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.19.tgz", + "integrity": "sha512-A/f07Fe7hCFy/2cUo0xg2r349RvOAHi4TuqOlZXxPuWShWbiSbvYcWr5wDz7G8aV5RyggNP0m+yB/lwmYx5OQg==", "dev": true, "dependencies": { "@types/mailparser": "3.4.6", "addressparser": "1.0.1", "async": "3.2.6", - "commander": "14.0.1", + "commander": "14.0.3", "compression": "1.8.1", - "cors": "2.8.5", - "dompurify": "3.3.0", - "express": "5.1.0", - "jsdom": "27.0.1", - "mailparser": "3.7.5", + "cors": "2.8.6", + "dompurify": "3.4.1", + "express": "5.2.1", + "jsdom": "29.1.0", + "mailparser": "3.9.8", "mime": "4.1.0", - "nodemailer": "7.0.9", - "smtp-server": "3.15.0", - "socket.io": "4.8.1", + "nodemailer": "8.0.7", + "smtp-server": "3.18.4", + "socket.io": "4.8.3", "wildstring": "1.0.9" }, "bin": { @@ -1476,27 +1462,27 @@ } }, "node_modules/mailparser": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.5.tgz", - "integrity": "sha512-o59RgZC+4SyCOn4xRH1mtRiZ1PbEmi6si6Ufnd3tbX/V9zmZN1qcqu8xbXY62H6CwIclOT3ppm5u/wV2nujn4g==", + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.8.tgz", + "integrity": "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w==", "dev": true, "dependencies": { + "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", - "iconv-lite": "0.7.0", - "libmime": "5.3.7", + "iconv-lite": "0.7.2", + "libmime": "5.3.8", "linkify-it": "5.0.0", - "mailsplit": "5.4.6", - "nodemailer": "7.0.9", + "nodemailer": "8.0.5", "punycode.js": "2.3.1", - "tlds": "1.260.0" + "tlds": "1.261.0" } }, "node_modules/mailparser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -1509,16 +1495,13 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mailsplit": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz", - "integrity": "sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==", - "deprecated": "This package has been renamed to @zone-eu/mailsplit. Please update your dependencies.", + "node_modules/mailparser/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "dev": true, - "dependencies": { - "libbase64": "1.3.0", - "libmime": "5.3.7", - "libqp": "2.1.1" + "engines": { + "node": ">=6.0.0" } }, "node_modules/math-intrinsics": { @@ -1531,9 +1514,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true }, "node_modules/media-typer": { @@ -1582,15 +1565,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1600,28 +1587,30 @@ "dev": true }, "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.3.tgz", + "integrity": "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==", "dependencies": { - "aws-ssl-profiles": "^1.1.1", + "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" }, "engines": { "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" } }, "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -1634,22 +1623,14 @@ } }, "node_modules/named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", "dependencies": { - "lru-cache": "^7.14.1" + "lru.min": "^1.1.0" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/named-placeholders/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" + "node": ">=8.0.0" } }, "node_modules/negotiator": { @@ -1662,9 +1643,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz", + "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1722,35 +1703,35 @@ } }, "node_modules/otpauth": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.4.1.tgz", - "integrity": "sha512-+iVvys36CFsyXEqfNftQm1II7SW23W1wx9RwNk0Cd97lbvorqAhBDksb/0bYry087QMxjiuBS0wokdoZ0iUeAw==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.1.tgz", + "integrity": "sha512-fJmDAHc8wImfqqqOXIlBvT1dEKrZK0Cmb2VEgScpNTolCz0PHh6ExUZGv4sLtOsWNaHCQlD+rRqaPgnoxFoZjQ==", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.2.0" }, "funding": { "url": "https://github.com/hectorm/otpauth?sponsor=1" } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -1779,12 +1760,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/peberminta": { @@ -1797,13 +1779,13 @@ } }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -1811,7 +1793,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -1823,15 +1805,15 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -1842,17 +1824,17 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -1878,12 +1860,12 @@ } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -1896,9 +1878,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -1974,9 +1956,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "dependencies": { "side-channel": "^1.1.0" @@ -1998,18 +1980,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/require-from-string": { @@ -2038,9 +2036,9 @@ } }, "node_modules/router/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2060,12 +2058,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2116,31 +2108,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/send/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2160,15 +2156,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" - }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "dependencies": { "encodeurl": "^2.0.0", @@ -2178,6 +2169,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -2206,13 +2201,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2259,30 +2254,38 @@ } }, "node_modules/smtp-server": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/smtp-server/-/smtp-server-3.15.0.tgz", - "integrity": "sha512-yv945vk0/xcukSKAoIhGz6GOlcXoCyGQH2w9IlLrTKk3SJiOBH9bcO6tD0ILTZYJsMqRa6OTRZAyqeuLXkv59Q==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/smtp-server/-/smtp-server-3.18.4.tgz", + "integrity": "sha512-9EnXPG4Tv+2P/TSEUdFTduYn9IxtxNRsOq/ryVj8ZlT+6MU2um9gn2Td2hHlgH1n+saagMWtici3hn5J5PhU+g==", "dev": true, "dependencies": { - "base32.js": "0.1.0", "ipv6-normalize": "1.0.1", - "nodemailer": "7.0.9", + "nodemailer": "8.0.5", "punycode.js": "2.3.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.18.0" + } + }, + "node_modules/smtp-server/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", + "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" @@ -2292,19 +2295,19 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", + "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", "dev": true, "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.20.1" } }, "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2324,44 +2327,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2395,9 +2377,9 @@ } }, "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2464,12 +2446,18 @@ "node": ">= 10.x" } }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", "engines": { - "node": ">= 0.6" + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, "node_modules/statuses": { @@ -2488,30 +2476,30 @@ "dev": true }, "node_modules/tlds": { - "version": "1.260.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", - "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", "dev": true, "bin": { "tlds": "bin.js" } }, "node_modules/tldts": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", - "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", "dev": true, "dependencies": { - "tldts-core": "^7.0.17" + "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", - "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", "dev": true }, "node_modules/toidentifier": { @@ -2524,9 +2512,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "dependencies": { "tldts": "^7.0.5" @@ -2548,17 +2536,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/uc.micro": { @@ -2567,11 +2572,19 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" }, "node_modules/unpipe": { "version": "1.0.0", @@ -2604,46 +2617,35 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "engines": { "node": ">=20" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "dependencies": { + "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/wildstring": { @@ -2659,9 +2661,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/playwright/package.json b/playwright/package.json index f47ec5dc..a7a35734 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -8,14 +8,14 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "1.56.1", - "dotenv": "17.2.3", - "dotenv-expand": "12.0.3", - "maildev": "npm:@timshel_npm/maildev@3.2.5" + "@playwright/test": "1.60.0", + "dotenv": "17.4.2", + "dotenv-expand": "13.0.0", + "maildev": "npm:@timshel_npm/maildev@3.2.19" }, "dependencies": { - "mysql2": "3.15.3", - "otpauth": "9.4.1", - "pg": "8.16.3" + "mysql2": "3.22.3", + "otpauth": "9.5.1", + "pg": "8.21.0" } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index de721aa3..ba5885d9 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -25,10 +25,12 @@ export default defineConfig({ /* Long global timeout for complex tests * But short action/nav/expect timeouts to fail on specific step (raise locally if not enough). */ - timeout: 120 * 1000, - actionTimeout: 20 * 1000, - navigationTimeout: 20 * 1000, - expect: { timeout: 20 * 1000 }, + timeout: 240 * 1000, + actionTimeout: 40 * 1000, + navigationTimeout: 40 * 1000, + expect: { timeout: 40 * 1000 }, + + "permissions": ["clipboard-read"], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { @@ -37,6 +39,10 @@ export default defineConfig({ browserName: 'firefox', locale: 'en-GB', timezoneId: 'Europe/London', + ignoreHTTPSErrors: true, + launchOptions: { + args: ['--ignore-certificate-errors'] + }, /* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */ trace: 'on', diff --git a/playwright/test.env b/playwright/test.env index df182ebe..2260f860 100644 --- a/playwright/test.env +++ b/playwright/test.env @@ -10,7 +10,7 @@ DOCKER_BUILDKIT=1 ##################### # Playwright Config # ##################### -PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false} +PW_KEEP_SERVICE_RUNNING=${PW_KEEP_SERVICE_RUNNING:-false} PW_SMTP_FROM=vaultwarden@playwright.test ##################### @@ -38,8 +38,8 @@ TEST_USER3_MAIL=${TEST_USER3}@example.com ################### # Keycloak Config # ################### -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KC_HTTP_HOST=127.0.0.1 KC_HTTP_PORT=8081 @@ -52,10 +52,12 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} # Vaultwarden Config # ###################### ROCKET_PORT=8003 -DOMAIN=http://localhost:${ROCKET_PORT} +ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} +DOMAIN=https://127.0.0.1:${ROCKET_PORT} LOG_LEVEL=info,oidcwarden::sso=debug LOGIN_RATELIMIT_MAX_BURST=100 ADMIN_TOKEN=admin +CLIENT_SUPPRESS_ONBOARDING=true SMTP_SECURITY=off SMTP_PORT=${MAILDEV_SMTP_PORT} diff --git a/playwright/tests/collection.spec.ts b/playwright/tests/collection.spec.ts index 786a4644..867386a5 100644 --- a/playwright/tests/collection.spec.ts +++ b/playwright/tests/collection.spec.ts @@ -1,6 +1,8 @@ import { test, expect, type TestInfo } from '@playwright/test'; import * as utils from "../global-utils"; + +import * as orgs from './setups/orgs'; import { createAccount } from './setups/user'; let users = utils.loadEnv(); @@ -16,20 +18,12 @@ test.afterAll('Teardown', async ({}) => { test('Create', async ({ page }) => { await createAccount(test, page, users.user1); - await test.step('Create Org', async () => { - await page.getByRole('link', { name: 'New organisation' }).click(); - await page.getByLabel('Organisation name (required)').fill('Test'); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); - - await utils.checkNotification(page, 'Organisation created'); - }); + await orgs.create(test, page, 'New organisation'); await test.step('Create Collection', async () => { - await page.getByRole('link', { name: 'Collections' }).click(); - await page.getByRole('button', { name: 'New' }).click(); + await page.getByRole('button', { name: 'New', exact: true }).click(); await page.getByRole('menuitem', { name: 'Collection' }).click(); - await page.getByLabel('Name (required)').fill('RandomCollec'); + await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('RandomCollec'); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Created collection RandomCollec'); await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible(); diff --git a/playwright/tests/cyphers.spec.ts b/playwright/tests/cyphers.spec.ts new file mode 100644 index 00000000..679874de --- /dev/null +++ b/playwright/tests/cyphers.spec.ts @@ -0,0 +1,56 @@ +import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; +import { activateTOTP, disableTOTP } from './setups/2fa'; + +let users = utils.loadEnv(); +let totp; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVault(browser, testInfo, {}); +}); + +test.afterAll('Teardown', async ({}) => { + utils.stopVault(); +}); + +test('Change Key settings', async ({ page }) => { + await createAccount(test, page, users.user1); + + await test.step('Change SHA-256 Iterations', async () => { + await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); + await page.getByRole('link', { name: 'Security' }).click(); + await page.getByRole('link', { name: 'Keys' }).click(); + + await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('700000'); + + await page.getByRole('button', { name: 'Update encryption settings' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Update settings' }).click(); + await page.getByRole('heading', { name: 'Log in' }).click(); + }); + + await logUser(test, page, users.user1); + + await test.step('Switch to Argon2', async () => { + await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); + await page.getByRole('link', { name: 'Security' }).click(); + await page.getByRole('link', { name: 'Keys' }).click(); + + await page.locator('.ng-arrow-wrapper').click(); + await page.getByText('Argon2id').click(); + + await page.getByRole('spinbutton', { name: 'KDF memory (MB) * (required)'}).fill('16'); + await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('2'); + await page.getByRole('spinbutton', { name: 'KDF parallelism * (required)'}).fill('1'); + + await page.getByRole('button', { name: 'Update encryption settings' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Update settings' }).click(); + await page.getByRole('heading', { name: 'Log in' }).click(); + }); + + await logUser(test, page, users.user1); +}); diff --git a/playwright/tests/login.smtp.spec.ts b/playwright/tests/login.smtp.spec.ts index 87474b79..c5c4d9ba 100644 --- a/playwright/tests/login.smtp.spec.ts +++ b/playwright/tests/login.smtp.spec.ts @@ -41,13 +41,10 @@ test('Account creation', async ({ page }) => { test('Login', async ({ context, page }) => { const mailBuffer = mailserver.buffer(users.user1.email); - await logUser(test, page, users.user1, mailBuffer); + await logUser(test, page, users.user1, { mailBuffer }); await test.step('verify email', async () => { - await page.getByText('Verify your account\'s email').click(); - await expect(page.getByText('Verify your account\'s email')).toBeVisible(); - await page.getByRole('button', { name: 'Send email' }).click(); - + await page.getByRole('button', { name: "Send email" }).click(); await utils.checkNotification(page, 'Check your email inbox for a verification link'); const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email"); @@ -78,26 +75,10 @@ test('Activate 2fa', async ({ page }) => { test('2fa', async ({ page }) => { const emails = mailserver.buffer(users.user1.email); - await test.step('login', async () => { - await page.goto('/'); - - await page.getByLabel(/Email address/).fill(users.user1.email); - await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); - - await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); - const code = await retrieveEmailCode(test, page, emails); - await page.getByLabel(/Verification code/).fill(code); - await page.getByRole('button', { name: 'Continue' }).click(); - - await page.getByRole('button', { name: 'Add it later' }).click(); - await page.getByRole('link', { name: 'Skip to web app' }).click(); - - await expect(page).toHaveTitle(/Vaults/); - }) - - await disableEmail(test, page, users.user1); + await logUser(test, page, users.user1, { + mailBuffer: emails, + mail2fa: true, + }); emails.close(); }); diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts index aaac4708..194976ea 100644 --- a/playwright/tests/login.spec.ts +++ b/playwright/tests/login.spec.ts @@ -37,8 +37,8 @@ test('Authenticator 2fa', async ({ page }) => { await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); await page.getByLabel(/Verification code/).fill(totp.generate({timestamp})); diff --git a/playwright/tests/organization.smtp.spec.ts b/playwright/tests/organization.smtp.spec.ts index 2be5fec1..6d0eb859 100644 --- a/playwright/tests/organization.smtp.spec.ts +++ b/playwright/tests/organization.smtp.spec.ts @@ -4,6 +4,7 @@ import { MailDev } from 'maildev'; import * as utils from '../global-utils'; import * as orgs from './setups/orgs'; import { createAccount, logUser } from './setups/user'; +import { activateTOTP } from './setups/2fa'; let users = utils.loadEnv(); @@ -20,6 +21,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { await utils.startVault(browser, testInfo, { SMTP_HOST: process.env.MAILDEV_HOST, SMTP_FROM: process.env.PW_SMTP_FROM, + EMAIL_2FA_AUTO_FALLBACK: "true", }); mail1Buffer = mailServer.buffer(users.user1.email); @@ -45,7 +47,7 @@ test('Invite users', async ({ page }) => { await orgs.policies(test, page, 'Test'); await page.getByRole('button', { name: 'Account recovery' }).click(); await page.getByRole('checkbox', { name: 'Turn on' }).check(); - await page.getByRole('checkbox', { name: 'Require new members' }).check(); + await page.getByRole('checkbox', { name: 'Automatically enroll new' }).check(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Edited policy Account recovery'); }); @@ -66,18 +68,16 @@ test('invited with new account', async ({ page }) => { await page.goto(link); await expect(page).toHaveTitle(/Create account | Vaultwarden Web/); - //await page.getByLabel('Name').fill(users.user2.name); - await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password); - await page.getByLabel('Confirm master password (').fill(users.user2.password); + // await page.getByLabel('Name').fill(users.user2.name); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByRole('button', { name: 'Create account' }).click(); await utils.checkNotification(page, 'Your new account has been created'); - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); - // Redirected to the vault await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); // await utils.checkNotification(page, 'You have been logged in!'); + await utils.checkNotification(page, 'Successfully accepted your invitation'); }); await test.step('Check mails', async () => { @@ -100,21 +100,19 @@ test('invited with existing account', async ({ page }) => { await page.getByRole('button', { name: 'Continue' }).click(); // Unlock page - await page.getByLabel('Master password').fill(users.user3.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); - - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user3.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); + await utils.checkNotification(page, 'Successfully accepted your invitation'); await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox'); await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted')); }); test('Confirm invited user', async ({ page }) => { - await logUser(test, page, users.user1, mail1Buffer); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); await orgs.members(test, page, 'Test'); await orgs.confirm(test, page, 'Test', users.user2.email); @@ -123,25 +121,26 @@ test('Confirm invited user', async ({ page }) => { }); test('Organization is visible', async ({ page }) => { - await logUser(test, page, users.user2, mail2Buffer); + await logUser(test, page, users.user2, { mailBuffer: mail2Buffer }); await page.getByRole('button', { name: 'vault: Test', exact: true }).click(); await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); }); test('Recover user password', async ({ page }) => { - await logUser(test, page, users.user1, mail1Buffer); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); let newPassword = "TotoNewPassword"; await orgs.members(test, page, 'Test'); - await test.step(`Rrcover ${users.user2.email}`, async () => { + await test.step(`Recover ${users.user2.email}`, async () => { await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click(); await page.getByRole('menuitem', { name: 'Recover account' }).click(); - await page.getByRole('textbox', { name: 'New master password (required)', exact: true }).fill(newPassword); - await page.getByRole('textbox', { name: 'Confirm new master password (' }).fill(newPassword); - await page.getByRole('button', { name: 'Save' }).click(); - await utils.checkNotification(page, 'Password reset success'); + await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword); + await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Account recovery success'); + await mail2Buffer.expect((m) => m.subject.includes('Master Password Has Been Changed')); }); let user2 = { @@ -149,5 +148,8 @@ test('Recover user password', async ({ page }) => { name: users.user2.name, password: newPassword, }; - await logUser(test, page, user2, mail2Buffer); + await logUser(test, page, user2, { + mailBuffer: mail2Buffer, + notNewDevice: true, + }); }); diff --git a/playwright/tests/secrets.spec.ts b/playwright/tests/secrets.spec.ts new file mode 100644 index 00000000..e229d400 --- /dev/null +++ b/playwright/tests/secrets.spec.ts @@ -0,0 +1,110 @@ +import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; + +let users = utils.loadEnv(); +let totp; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVault(browser, testInfo, {}); + + const context = await browser.newContext(); + const page = await context.newPage(); + await createAccount(test, page, users.user1); + await context.close(); +}); + +test.afterAll('Teardown', async ({}) => { + utils.stopVault(); +}); + +test('Password', async ({ context, page }, testInfo: TestInfo) => { + const label = 'Test Password'; + + await logUser(test, page, users.user1); + + await test.step('Create password entry', async () => { + await page.getByRole('button', { name: 'New item' }).click(); + await page.getByRole('textbox', { name: 'Item name * (required)' }).fill(label); + await page.getByRole('textbox', { name: 'Username' }).fill(users.user1.name); + await page.getByRole('textbox', { name: 'Password' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Item added'); + await page.getByRole('button', { name: 'Close' }).click(); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Check', async () => { + await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click(); + await page.getByTestId('copy-username').click(); + await utils.checkNotification(page, 'Username copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.name) + await page.getByTestId('copy-password').click(); + await utils.checkNotification(page, 'Password copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.password) + await page.getByRole('button', { name: 'Close' }).click(); + }); + + await test.step('Delete', async () => { + await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await utils.checkNotification(page, 'Item sent to bin'); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Deleted', async () => { + await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0) + }); +}); + + +test('SSH Key', async ({ context, page }, testInfo: TestInfo) => { + const label = 'Test SSH key'; + + await logUser(test, page, users.user1); + + const privateKey = await test.step('Create key entry', async () => { + await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('menuitem', { name: 'SSH key' }).click(); + await page.getByRole('textbox', { name: 'Item name * (required)' }).fill('Test SSH key'); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Item added'); + + await page.getByRole('button', { name: 'Copy private key' }).click(); + await utils.checkNotification(page, 'Private key copied'); + return await page.evaluate(() => navigator.clipboard.readText()); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Check', async () => { + await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click(); + + await page.getByRole('button', { name: 'Copy private key' }).click(); + await utils.checkNotification(page, 'Private key copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(privateKey) + await page.getByRole('button', { name: 'Close' }).click(); + }); + + await test.step('Delete', async () => { + await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await utils.checkNotification(page, 'Item sent to bin'); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Deleted', async () => { + await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0) + }) +}); diff --git a/playwright/tests/send.spec.ts b/playwright/tests/send.spec.ts index d27c3ffc..ddb8009d 100644 --- a/playwright/tests/send.spec.ts +++ b/playwright/tests/send.spec.ts @@ -21,11 +21,11 @@ test('Send', async ({ browser, page }) => { await page.getByRole('link', { name: 'Send' }).click(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); - await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('button', { name: 'New Send', exact: true }).click(); await page.getByRole('menuitem', { name: 'Text' }).click(); - await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Test'); - await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('test'); + await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Test'); + await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('test'); await page.getByRole('button', { name: 'Save' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); @@ -46,14 +46,14 @@ test('Send', async ({ browser, page }) => { await page.getByRole('link', { name: 'Send' }).click(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); - await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('button', { name: 'New' }).click(); await page.getByRole('menuitem', { name: 'Text' }).click(); - await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Password'); - await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('password'); + await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Password'); + await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('password'); await page.getByRole('combobox', { name: 'Who can view' }).click(); await page.getByText('Anyone with a password set by you').click(); - await page.getByRole('textbox', { name: 'Password (required)' }).fill('password'); + await page.getByRole('textbox', { name: 'Password * (required)', exact: true }).fill('password'); await page.getByRole('button', { name: 'Save' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); @@ -64,7 +64,7 @@ test('Send', async ({ browser, page }) => { await test.step('View with password', async () => { await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' }); await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible(); - await page2.getByRole('textbox', { name: 'Password (required)' }).fill('password'); + await page2.getByRole('textbox', { name: 'Password * (required)' }).fill('password'); await page2.getByRole('button', { name: 'Continue' }).click(); await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible(); await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible(); diff --git a/playwright/tests/setups/2fa.ts b/playwright/tests/setups/2fa.ts index d7936420..d430d053 100644 --- a/playwright/tests/setups/2fa.ts +++ b/playwright/tests/setups/2fa.ts @@ -11,10 +11,11 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string, await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); - const secret = await page.getByLabel('Key').innerText(); + const secret = await page.getByLabel('Key', { exact: true }).innerText(); + let totp = new OTPAuth.TOTP({ secret, period: 30 }); await page.getByLabel(/Verification code/).fill(totp.generate()); @@ -33,8 +34,8 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password (required)').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); @@ -49,7 +50,7 @@ export async function activateEmail(test: Test, page: Page, user: { name: string await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Send email' }).click(); }); @@ -81,8 +82,8 @@ export async function disableEmail(test: Test, page: Page, user: { password: str await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click(); - await page.getByLabel('Master password (required)').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); diff --git a/playwright/tests/setups/admin.ts b/playwright/tests/setups/admin.ts new file mode 100644 index 00000000..354c9ee7 --- /dev/null +++ b/playwright/tests/setups/admin.ts @@ -0,0 +1,21 @@ +import { expect, type Browser, Page } from '@playwright/test'; +import * as utils from '../../global-utils'; + +utils.loadEnv(); + +export async function login(test, page: Page) { + await test.step(`Admin login`, async () => { + await page.goto('/admin'); + await page.getByRole('textbox', { name: 'Enter admin token' }).fill(process.env.ADMIN_TOKEN); + await page.getByRole('button', { name: 'Enter' }).click(); + }); +} + +export async function invite(test, page: Page, email: string) { + await test.step(`Invite user with ${email}`, async () => { + await page.getByRole('link', { name: 'Users' }).click(); + await page.getByRole('textbox', { name: 'Enter email' }).fill(email); + await page.getByRole('button', { name: 'Invite' }).click(); + await expect(page.getByRole('row', { name: email })).toHaveText(/Invited/); + }); +} diff --git a/playwright/tests/setups/db-teardown.ts b/playwright/tests/setups/db-teardown.ts index 5f753a9d..86d40ac5 100644 --- a/playwright/tests/setups/db-teardown.ts +++ b/playwright/tests/setups/db-teardown.ts @@ -5,7 +5,7 @@ const utils = require('../../global-utils'); utils.loadEnv(); test('DB teardown ?', async ({ serviceName }) => { - if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) { + if( process.env.PW_KEEP_SERVICE_RUNNING !== "true" ) { utils.stopComposeService(serviceName); } }); diff --git a/playwright/tests/setups/orgs.ts b/playwright/tests/setups/orgs.ts index 04d81b45..ce12c50e 100644 --- a/playwright/tests/setups/orgs.ts +++ b/playwright/tests/setups/orgs.ts @@ -3,11 +3,14 @@ import { expect, type Browser,Page } from '@playwright/test'; import * as utils from '../../global-utils'; export async function create(test, page: Page, name: string) { - await test.step('Create Org', async () => { - await page.locator('a').filter({ hasText: 'Password Manager' }).first().click(); + await test.step(`Create Org ${name}`, async () => { + let pm_locator = page.locator('a').filter({ hasText: 'Password Manager' }); + if( await pm_locator.count() > 0 ){ + pm_locator.first().click(); + } await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); await page.getByRole('link', { name: 'New organisation' }).click(); - await page.getByLabel('Organisation name (required)').fill(name); + await page.getByRole('textbox', { name: 'Organisation name * (required)', exact: true }).fill(name); await page.getByRole('button', { name: 'Submit' }).click(); await utils.checkNotification(page, 'Organisation created'); @@ -18,7 +21,7 @@ export async function policies(test, page: Page, name: string) { await test.step(`Navigate to ${name} policies`, async () => { await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); - await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click(); + await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); await page.getByRole('link', { name: 'Policies' }).click(); @@ -30,11 +33,11 @@ export async function members(test, page: Page, name: string) { await test.step(`Navigate to ${name} members`, async () => { await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); - await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click(); + await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); - await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + await page.getByRole('link', { name: 'Members' }).click(); await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'All' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Select all' })).toBeVisible(); }); } @@ -42,13 +45,13 @@ export async function invite(test, page: Page, name: string, email: string) { await test.step(`Invite ${email}`, async () => { await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await page.getByRole('button', { name: 'Invite member' }).click(); - await page.getByLabel('Email (required)').fill(email); + await page.getByRole('textbox', { name: 'Email * (required)', exact: true }).fill(email); await page.getByRole('tab', { name: 'Collections' }).click(); await page.getByRole('combobox', { name: 'Permission' }).click(); await page.getByText('Edit items', { exact: true }).click(); - await page.getByLabel('Select collections').click(); - await page.getByText('Default collection').click(); - await page.getByRole('cell', { name: 'Collection', exact: true }).click(); + await page.getByRole('combobox', { name: 'Select collections' }).click(); + await page.getByLabel('Options List').getByText('Default collection').click(); + await page.getByRole('columnheader', { name: 'Collection', exact: true }).click(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'User(s) invited'); }); diff --git a/playwright/tests/setups/sso-teardown.ts b/playwright/tests/setups/sso-teardown.ts index 2899afff..22934b75 100644 --- a/playwright/tests/setups/sso-teardown.ts +++ b/playwright/tests/setups/sso-teardown.ts @@ -6,7 +6,7 @@ const utils = require('../../global-utils'); utils.loadEnv(); test('Keycloak teardown', async () => { - if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) { + if( process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { console.log("Keep Keycloak running"); } else { console.log("Keycloak stopping"); diff --git a/playwright/tests/setups/sso.ts b/playwright/tests/setups/sso.ts index 6317f8b0..0ad0cffb 100644 --- a/playwright/tests/setups/sso.ts +++ b/playwright/tests/setups/sso.ts @@ -15,11 +15,8 @@ export async function logNewUser( options: { mailBuffer?: MailBuffer } = {} ) { await test.step(`Create user ${user.name}`, async () => { - await page.context().clearCookies(); - await test.step('Landing page', async () => { await utils.cleanLanding(page); - await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); @@ -33,26 +30,24 @@ export async function logNewUser( await test.step('Create Vault account', async () => { await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); - await page.getByLabel('Master password (required)', { exact: true }).fill(user.password); - await page.getByLabel('Confirm master password (').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByRole('button', { name: 'Create account' }).click(); }); - await utils.checkNotification(page, 'Account successfully created!'); - await utils.checkNotification(page, 'Invitation accepted'); - - await utils.ignoreExtension(page); - await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); }); + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); + if( options.mailBuffer ){ let mailBuffer = options.mailBuffer; await test.step('Check emails', async () => { - await mailBuffer.expect((m) => m.subject === "Welcome"); await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); + await mailBuffer.expect((m) => m.subject === "Welcome"); }); } }); @@ -69,16 +64,14 @@ export async function logUser( mailBuffer ?: MailBuffer, totp?: OTPAuth.TOTP, mail2fa?: boolean, + notNewDevice?: boolean, } = {} ) { let mailBuffer = options.mailBuffer; await test.step(`Log user ${user.email}`, async () => { - await page.context().clearCookies(); - await test.step('Landing page', async () => { await utils.cleanLanding(page); - await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); @@ -117,14 +110,12 @@ export async function logUser( await page.getByRole('button', { name: 'Unlock' }).click(); }); - await utils.ignoreExtension(page); - await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); }); - if( mailBuffer ){ + if( mailBuffer && !options.notNewDevice ){ await test.step('Check email', async () => { await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); }); diff --git a/playwright/tests/setups/user.ts b/playwright/tests/setups/user.ts index 395196ae..3d3990e9 100644 --- a/playwright/tests/setups/user.ts +++ b/playwright/tests/setups/user.ts @@ -3,6 +3,7 @@ import { expect, type Browser, Page } from '@playwright/test'; import { type MailBuffer } from 'maildev'; import * as utils from '../../global-utils'; +import { retrieveEmailCode } from './2fa'; export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) { await test.step(`Create user ${user.name}`, async () => { @@ -17,12 +18,11 @@ export async function createAccount(test, page: Page, user: { email: string, nam await page.getByRole('button', { name: 'Continue' }).click(); // Vault finish Creation - await page.getByLabel('Master password (required)', { exact: true }).fill(user.password); - await page.getByLabel('Confirm master password (').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByRole('button', { name: 'Create account' }).click(); await utils.checkNotification(page, 'Your new account has been created') - await utils.ignoreExtension(page); // We are now in the default vault page await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); @@ -35,7 +35,16 @@ export async function createAccount(test, page: Page, user: { email: string, nam }); } -export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) { +export async function logUser( + test, + page: Page, + user: { email: string, password: string }, + options: { + mailBuffer ?: MailBuffer, + mail2fa?: boolean, + notNewDevice?: boolean, + } = {} +) { await test.step(`Log user ${user.email}`, async () => { await utils.cleanLanding(page); @@ -43,16 +52,23 @@ export async function logUser(test, page: Page, user: { email: string, password: await page.getByRole('button', { name: 'Continue' }).click(); // Unlock page - await page.getByLabel('Master password').fill(user.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); - await utils.ignoreExtension(page); + if( options.mail2fa ){ + await test.step('2FA check', async () => { + await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); + let code = await retrieveEmailCode(test, page, options.mailBuffer); + await page.getByLabel(/Verification code/).fill(code); + await page.getByRole('button', { name: 'Continue' }).click(); + }); + } // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); - if( mailBuffer ){ - await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox"); + if( options.mailBuffer && !options.notNewDevice ){ + await options.mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox"); } }); } diff --git a/playwright/tests/sso_login.smtp.spec.ts b/playwright/tests/sso_login.smtp.spec.ts index 7a615cd6..1f5c9361 100644 --- a/playwright/tests/sso_login.smtp.spec.ts +++ b/playwright/tests/sso_login.smtp.spec.ts @@ -1,6 +1,7 @@ import { test, expect, type TestInfo } from '@playwright/test'; import { MailDev } from 'maildev'; +import * as admin from "./setups/admin"; import { logNewUser, logUser } from './setups/sso'; import { activateEmail, disableEmail } from './setups/2fa'; import * as utils from "../global-utils"; @@ -19,7 +20,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { await utils.startVault(browser, testInfo, { SSO_ENABLED: true, - SSO_ONLY: false, + SSO_ONLY: true, SMTP_HOST: process.env.MAILDEV_HOST, SMTP_FROM: process.env.PW_SMTP_FROM, }); @@ -32,22 +33,64 @@ test.afterAll('Teardown', async ({}) => { } }); -test('Create and activate 2FA', async ({ page }) => { +test('2FA email', async ({ page }) => { + const mailBuffer = mailserver.buffer(users.user1.email); await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer}); await activateEmail(test, page, users.user1, mailBuffer); + await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true, notNewDevice: true}); + + await disableEmail(test, page, users.user1); + mailBuffer.close(); }); -test('Log and disable', async ({ page }) => { - const mailBuffer = mailserver.buffer(users.user1.email); - await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true}); +test('Admin invite', async ({ page }) => { + const mailBuffer = mailserver.buffer(users.user2.email); - await disableEmail(test, page, users.user1); + await admin.login(test, page); + await admin.invite(test, page, users.user2.email); + + + const link = await test.step('Extract email link', async () => { + const invited = await mailBuffer.expect((m) => m.subject === "Join Vaultwarden"); + await page.setContent(invited.html); + return await page.getByTestId("invite").getAttribute("href"); + }); + + await test.step('Redirect to Keycloak', async () => { + await page.goto(link); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user2.name); + await page.getByLabel('Password', { exact: true }).fill(users.user2.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Create Vault account', async () => { + await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); + await page.getByRole('button', { name: 'Create account' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); + + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); + }); + + await test.step('Check mails', async () => { + await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); + await mailBuffer.expect((m) => m.subject === "Welcome"); + }); mailBuffer.close(); }); diff --git a/playwright/tests/sso_login.spec.ts b/playwright/tests/sso_login.spec.ts index 8a1bb9ab..e93aab14 100644 --- a/playwright/tests/sso_login.spec.ts +++ b/playwright/tests/sso_login.spec.ts @@ -33,8 +33,8 @@ test('Non SSO login', async ({ page }) => { await page.getByRole('button', { name: 'Other' }).click(); // Unlock page - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); @@ -58,6 +58,7 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) = // Landing page await page.goto('/'); + await page.locator("input[type=email].vw-email-sso").fill(users.user1.email); // Check that SSO login is available await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1); @@ -66,7 +67,6 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) = await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0); }); - test('No SSO login', async ({ page }, testInfo: TestInfo) => { await utils.restartVault(page, testInfo, { SSO_ENABLED: false @@ -74,12 +74,14 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => { // Landing page await page.goto('/'); + await page.getByLabel(/Email address/).fill(users.user1.email); // No SSO button (rely on a correct selector checked in previous test) + await page.getByLabel('Master password'); await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0); // Can continue to Master password await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByRole('button', { name: 'Continue' }).click(); - await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1); + await expect(page.getByRole('button', { name: 'Log in' })).toHaveCount(1); }); diff --git a/playwright/tests/sso_organization.smtp.spec.ts b/playwright/tests/sso_organization.smtp.spec.ts index 92813f72..eef4f83d 100644 --- a/playwright/tests/sso_organization.smtp.spec.ts +++ b/playwright/tests/sso_organization.smtp.spec.ts @@ -67,17 +67,16 @@ test('invited with new account', async ({ page }) => { await test.step('Create Vault account', async () => { await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); - await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password); - await page.getByLabel('Confirm master password (').fill(users.user2.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByRole('button', { name: 'Create account' }).click(); - - await utils.checkNotification(page, 'Account successfully created!'); - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); }); await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); + + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); }); await test.step('Check mails', async () => { @@ -95,6 +94,7 @@ test('invited with existing account', async ({ page }) => { await test.step('Redirect to Keycloak', async () => { await page.goto(link); + await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); await test.step('Keycloak login', async () => { @@ -108,13 +108,11 @@ test('invited with existing account', async ({ page }) => { await expect(page).toHaveTitle('Vaultwarden Web'); await page.getByLabel('Master password').fill(users.user3.password); await page.getByRole('button', { name: 'Unlock' }).click(); - - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); }); await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); + await utils.checkNotification(page, 'Successfully accepted your invitation'); }); await test.step('Check mails', async () => { diff --git a/playwright/tests/sso_organization.spec.ts b/playwright/tests/sso_organization.spec.ts index c1238d45..ee7e28f6 100644 --- a/playwright/tests/sso_organization.spec.ts +++ b/playwright/tests/sso_organization.spec.ts @@ -49,7 +49,7 @@ test('Organization is visible', async ({ page }) => { await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); }); -test('Enforce password policy', async ({ page }) => { +test('Activate password policy', async ({ page }) => { await logUser(test, page, users.user1); await orgs.policies(test, page, '/Test'); @@ -61,16 +61,27 @@ test('Enforce password policy', async ({ page }) => { await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Edited policy Master password requirements.'); }); +}); - await utils.logout(test, page, users.user1); +test('Unlock trigger policyy', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); - await test.step(`Unlock trigger policy`, async () => { - await page.locator("input[type=email].vw-email-sso").fill(users.user1.email); - await page.getByRole('button', { name: 'Use single sign-on' }).click(); + await page.locator("input[type=email].vw-email-sso").fill(users.user2.email); + await page.getByRole('button', { name: /Use single sign-on/ }).click(); - await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password); - await page.getByRole('button', { name: 'Unlock' }).click(); + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user2.name); + await page.getByLabel('Password', { exact: true }).fill(users.user2.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); - await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible(); + await test.step('Unlock vault', async () => { + await expect(page).toHaveTitle('Vaultwarden Web'); + await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible(); + await page.getByLabel('Master password').fill(users.user2.password); + await page.getByRole('button', { name: 'Unlock' }).click(); }); + + await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible(); }); diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index a5ae50a4..e6a184dd 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -238,7 +238,7 @@ fn config() -> Json { "disableUserRegistration": CONFIG.is_signup_disabled(), // When enabled, this setting signals to clients that onboarding interstitials // (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) should be suppressed - "suppressOnboardingInterstitials": false + "suppressOnboardingInterstitials": CONFIG.client_suppress_onboarding(), }, "environment": { "vault": domain, diff --git a/src/config.rs b/src/config.rs index c4457478..687e2aaf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -659,6 +659,11 @@ make_config! { events_days_retain: i64, false, option; }, + client { + /// Control whether clients onboarding interstitials are suppressed |> post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals + client_suppress_onboarding: bool, true, def, false; + }, + /// Advanced settings advanced { /// Client IP header |> If not present, the remote IP is used. From b30cc08562cf59645271e0431284a88fdab6e27a Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Thu, 6 Aug 2026 20:22:12 +0200 Subject: [PATCH 03/34] Misc fixes and updates (#7558) * Update GHA and pre-commit Signed-off-by: BlackDex * Update admin diagnostics Added a check if the templates are overridden and return which specific folder, `admin`, `email` or `scss`. This way we could more quickly point users to possible outdated templates which they are using. Also updated the Support String to use some emojis so we should be able to quicker see if there is something wrong. Just checking `true` or `false` could be difficult sometimes, and sometimes what we had as `false` wasn't bad either. Also adjusted the eslint comments so it will work with the latest version of eslint. Signed-off-by: BlackDex * Fix updating collections for a cipher The newer clients expect a `cipherDetails` response on the `collections-admin` endpoints. Without it, the client will cause an error and stops handling the update correctly. This will fix this by returning the cipher json. Fixes #7545 Fixes #7546 Signed-off-by: BlackDex * Cache CSS file in a different way Currently we set a cache ttl of 24 hours, and users need to do a force refresh if there is anything changed to the CSS file. In the past we have had several issue reported which were related to a still cached CSS file. This commit will change the caching and also cache the generated CSS file in memory. Instead of letting the browser cache it for 24 hours we generate an ETag, this is just a hash of the contents. This ETag is returned by the browser during a request, and we can match this, and if so, just return a `304` `Not Modified`. If the ETag is not known, we return the new content. This should make simple refreshes by clients get updated settings or a new version of Vaultwarden which has other CSS entries get updated instantly. If a user does a hard refresh, we will not receive the ETag and the content will be served. The same goes if someone has the `reload_templates` feature enabled, since then we should not cache anyway. If someone adjust settings via the `/admin` interface, the cache will be invalidated and a new CSS will be generated. Signed-off-by: BlackDex * Fix showing events for a specific user Signed-off-by: BlackDex * Update crates and adjust code. - Updated opendal and adjusted code where needed. - Updated yubico_ng and adjusted code where needed. This version now supports using an own HttpClient and it pulls in no reqwest dependency anymore. Now it will use our own client which uses custom hickory DNS and other features. Signed-off-by: BlackDex * Update web-vault to v2026.7.0 Signed-off-by: BlackDex * Fix hadolint warnings Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .github/workflows/hadolint.yml | 4 +- .github/workflows/release.yml | 20 +- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 9 +- Cargo.lock | 338 ++++++++++----------- Cargo.toml | 24 +- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 21 +- docker/Dockerfile.debian | 22 +- docker/Dockerfile.j2 | 10 +- src/api/admin.rs | 31 ++ src/api/core/ciphers.rs | 6 +- src/api/core/two_factor/yubikey.rs | 57 +++- src/api/mod.rs | 2 +- src/api/web.rs | 43 ++- src/config.rs | 6 + src/db/models/event.rs | 10 +- src/error.rs | 2 +- src/static/scripts/admin.js | 3 +- src/static/scripts/admin_diagnostics.js | 36 ++- src/static/scripts/admin_organizations.js | 3 +- src/static/scripts/admin_settings.js | 1 - src/static/scripts/admin_users.js | 3 +- src/static/templates/admin/diagnostics.hbs | 10 + src/storage.rs | 4 +- src/util.rs | 38 +++ 28 files changed, 432 insertions(+), 281 deletions(-) diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 17151922..3111e20b 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -41,12 +41,12 @@ jobs: # Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian) # so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety. - name: Run hadolint on Dockerfile.debian - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 + uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 with: dockerfile: docker/Dockerfile.debian - name: Run hadolint on Dockerfile.alpine - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 + uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 with: dockerfile: docker/Dockerfile.alpine # End Test Dockerfiles with hadolint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0efd21db..9d15dd88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -121,7 +121,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -137,7 +137,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -237,7 +237,7 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -272,7 +272,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -287,7 +287,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -303,7 +303,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -365,7 +365,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -373,7 +373,7 @@ jobs: - name: Attest - ghcr.io - ${{ matrix.base_image }} if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -381,7 +381,7 @@ jobs: - name: Attest - quay.io - ${{ matrix.base_image }} if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index c1f42c56..942a99e9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 7c345e0a..779cd6e3 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 + uses: crate-ci/typos@8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 72810c67..e1de58c3 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35a0140e..f9920696 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,9 +18,10 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 + rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 hooks: - id: typos + always_run: true - repo: local hooks: @@ -38,8 +39,7 @@ repos: entry: cargo test language: system args: [ "--features", "sqlite,mysql,postgresql", "--" ] - types_or: [ rust, file ] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended pass_filenames: false - id: cargo-clippy name: cargo clippy @@ -47,8 +47,7 @@ repos: entry: cargo clippy language: system args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ] - types_or: [ rust, file ] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended pass_filenames: false - id: check-docker-templates name: check docker templates diff --git a/Cargo.lock b/Cargo.lock index 6b20797b..21defe79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -52,9 +52,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -213,7 +213,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "event-listener-strategy", "pin-project-lite", ] @@ -231,7 +231,7 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener 5.4.1", + "event-listener 5.4.2", "futures-lite", "rustix", ] @@ -349,9 +349,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8" +checksum = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -369,7 +369,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 1.4.2", + "http 1.5.0", "sha1 0.10.7", "time", "tokio", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" +checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -407,7 +407,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "percent-encoding", "pin-project-lite", @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.104.0" +version = "1.105.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22" +checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516" dependencies = [ "arc-swap", "aws-credential-types", @@ -436,16 +436,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.106.0" +version = "1.107.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188" +checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c" dependencies = [ "arc-swap", "aws-credential-types", @@ -462,16 +462,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.109.0" +version = "1.110.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" +checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13" dependencies = [ "arc-swap", "aws-credential-types", @@ -489,7 +489,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -509,7 +509,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "percent-encoding", "sha2 0.11.0", "time", @@ -539,7 +539,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "percent-encoding", @@ -596,7 +596,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -617,7 +617,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "pin-project-lite", "tokio", "tracing", @@ -643,7 +643,7 @@ checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -656,7 +656,7 @@ dependencies = [ "bytes", "bytes-utils", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -714,6 +714,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64-simd" version = "0.8.0" @@ -1129,12 +1135,13 @@ dependencies = [ ] [[package]] -name = "crc32c" -version = "0.6.8" +name = "crc-fast" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "rustc_version", + "digest 0.10.7", + "spin 0.10.1", ] [[package]] @@ -1154,13 +1161,14 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "cron" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +checksum = "a5dcd6f69605c2956916ce24e8af637b754964c9a83f4662d3a2361654cdba09" dependencies = [ "chrono", "once_cell", - "winnow 0.6.26", + "phf 0.11.3", + "winnow 0.7.15", ] [[package]] @@ -1380,9 +1388,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "data-url" @@ -1638,13 +1646,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1737,9 +1745,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" @@ -1764,11 +1772,11 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "memchr", ] @@ -1814,11 +1822,10 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1829,7 +1836,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "pin-project-lite", ] @@ -2179,7 +2186,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.2", + "http 1.5.0", "indexmap 2.14.0", "slab", "tokio", @@ -2396,9 +2403,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2422,7 +2429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -2433,7 +2440,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "pin-project-lite", ] @@ -2452,9 +2459,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -2493,7 +2500,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "httparse", "itoa", @@ -2509,10 +2516,10 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", + "http 1.5.0", "hyper 1.11.0", "hyper-util", - "rustls 0.23.42", + "rustls 0.23.43", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -2528,7 +2535,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "hyper 1.11.0", "ipnet", @@ -2720,9 +2727,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" dependencies = [ "serde", ] @@ -2761,9 +2768,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -2789,9 +2796,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", @@ -2865,9 +2872,9 @@ dependencies = [ [[package]] name = "job_scheduler_ng" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217723d58ee473953675d15f11e56898a611aca8ea044d5a34eabeade99ef613" +checksum = "576b4255ab9de8ce7b81060ec54b1b7f8499dfd6c16a66c4cd4cb1ad4eba27e3" dependencies = [ "chrono", "cron", @@ -2943,18 +2950,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.9", ] [[package]] name = "lettre" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" +checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae" dependencies = [ "async-std", "async-trait", - "base64 0.22.1", + "base64 0.23.1", "email-encoding", "email_address", "fastrand", @@ -2967,7 +2974,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-native-certs", "serde", "socket2 0.6.5", @@ -3089,9 +3096,9 @@ dependencies = [ [[package]] name = "mea" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2640d335e7273dacdcf51044026139b2e269c3bb0dfc3f8cb3496b85e3f6a42c" +checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0" dependencies = [ "slab", ] @@ -3176,7 +3183,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "equivalent", - "event-listener 5.4.1", + "event-listener 5.4.2", "futures-util", "parking_lot", "portable-atomic", @@ -3194,11 +3201,11 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.2", + "http 1.5.0", "httparse", "memchr", "mime", - "spin", + "spin 0.9.9", "tokio", "tokio-util", "version_check", @@ -3370,7 +3377,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.2", + "http 1.5.0", "rand 0.8.7", "serde", "serde_json", @@ -3401,9 +3408,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1" +checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3412,24 +3419,22 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309" +checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" dependencies = [ "anyhow", - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures", - "http 1.4.2", - "http-body 1.1.0", + "http 1.5.0", "jiff", "log", "md-5", "mea", "percent-encoding", - "quick-xml 0.39.4", + "quick-xml", "reqsign-core", - "reqwest", "serde", "serde_json", "tokio", @@ -3440,9 +3445,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e89a665fef0e6bd249cf5ea47fc174b7ba892159bee4b9382528b1ca873a2c" +checksum = "826c4e17a30643b888fe983897f9a4b23b07066e1d069727a923cc8fb419a702" dependencies = [ "bytes", "log", @@ -3454,18 +3459,18 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb" +checksum = "58e80cdf192d7eff05feed747894d64f81905ac4eaf132edf7ea270abdd2d663" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", - "crc32c", - "http 1.4.2", + "crc-fast", + "http 1.5.0", "log", "md-5", "opendal-core", - "quick-xml 0.39.4", + "quick-xml", "reqsign-aws-v4", "reqsign-core", "reqsign-file-read-tokio", @@ -3484,7 +3489,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac 0.12.1", - "http 1.4.2", + "http 1.5.0", "itertools", "log", "oauth2", @@ -4007,16 +4012,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -4193,9 +4188,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4226,19 +4221,18 @@ dependencies = [ ] [[package]] -name = "reqsign-aws-v4" -version = "3.0.2" +name = "reqsign-aws-core" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9e1168fab3883ec6afed1c2e20c25b2a09f366cdb662ac3e0878ae0332d63e" +checksum = "e4af084e1f3cbf3e67e0c972765399bce54ecec804cceba46b39a8331f3c1bff" dependencies = [ - "anyhow", "bytes", "form_urlencoded", "hex", - "http 1.4.2", + "http 1.5.0", "log", "percent-encoding", - "quick-xml 0.41.0", + "quick-xml", "reqsign-core", "rust-ini", "serde", @@ -4248,19 +4242,33 @@ dependencies = [ ] [[package]] -name = "reqsign-core" +name = "reqsign-aws-v4" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514a1e0b4aa288652a3fdbda4f0a610f379cdf5374e55a37c9edd03d57ed856b" +checksum = "4ac5b3b7cefa28933792b439186459f77f19f9b6edbeab41b8b187150361a206" +dependencies = [ + "bytes", + "http 1.5.0", + "log", + "quick-xml", + "reqsign-aws-core", + "reqsign-core", + "serde", +] + +[[package]] +name = "reqsign-core" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07dd510b1e1b9b241883e483358147fb2ed2d497a7b39b065ba61eb93deceb0" dependencies = [ "anyhow", - "base64 0.22.1", + "base64 0.23.1", "bytes", - "form_urlencoded", "futures", "hex", "hmac 0.13.0", - "http 1.4.2", + "http 1.5.0", "jiff", "log", "percent-encoding", @@ -4271,9 +4279,9 @@ dependencies = [ [[package]] name = "reqsign-file-read-tokio" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b472a8d1f2e5a4be8ce13bb7bdf4b59e9bee613ce124aca23959ddb42176b39" +checksum = "663d9d55abd0df0830ef0ae43708297cc1371cf4e8ca91f3ac813c309cca8c98" dependencies = [ "anyhow", "reqsign-core", @@ -4291,11 +4299,10 @@ dependencies = [ "cookie", "cookie_store", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.11.0", @@ -4306,7 +4313,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4575,9 +4582,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -4629,7 +4636,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", @@ -4719,9 +4726,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -4921,7 +4928,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -5107,6 +5114,12 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spinning_top" version = "0.3.0" @@ -5330,20 +5343,11 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -5424,13 +5428,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5449,7 +5453,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.42", + "rustls 0.23.43", "tokio", ] @@ -5550,9 +5554,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -5601,7 +5605,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "pin-project-lite", @@ -5702,7 +5706,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.2", + "http 1.5.0", "httparse", "log", "rand 0.8.7", @@ -5818,9 +5822,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e" +checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" [[package]] name = "vaultwarden" @@ -5853,7 +5857,7 @@ dependencies = [ "handlebars", "hickory-resolver", "html5gum", - "http 1.4.2", + "http 1.5.0", "ipnet", "job_scheduler_ng", "jsonwebtoken", @@ -5881,7 +5885,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.42", + "rustls 0.23.43", "semver", "serde", "serde_json", @@ -6392,15 +6396,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.15" @@ -6501,18 +6496,15 @@ dependencies = [ [[package]] name = "yubico_ng" -version = "0.15.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228e2862e3c66f3224102d9a00d9d3646b271a05cc6c4819fea195fa8b5c00e0" +checksum = "563eb0ab41031e758446e3737231541bb4556d6af1e893e94faa09c989f794af" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "form_urlencoded", - "futures", - "hmac 0.12.1", - "rand 0.9.5", - "reqwest", - "sha1 0.10.7", - "threadpool", + "getrandom 0.4.3", + "hmac 0.13.0", + "sha1 0.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index db685864..f65ead29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ libsqlite3-sys = { version = "0.37.0", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.42", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.43", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation @@ -133,13 +133,13 @@ uuid = { version = "1.24.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } chrono-tz = "0.10.4" -time = "0.3.54" +time = "0.3.55" # Job scheduler -job_scheduler_ng = "2.4.0" +job_scheduler_ng = "2.5.0" # Data encoding library Hex/Base32/Base64 -data-encoding = "2.11.0" +data-encoding = "2.11.1" # JWT library jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] } @@ -148,7 +148,7 @@ jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust totp-lite = "2.0.1" # Yubico Library -yubico = { package = "yubico_ng", version = "0.15.0", default-features = false, features = ["online-tokio"] } +yubico_ng = { version = "1.0.0", default-features = false } # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db @@ -161,7 +161,7 @@ webauthn-rs-core = "0.5.5" url = "2.5.8" # Email libraries -lettre = { version = "0.11.22", default-features = false, features = [ +lettre = { version = "0.11.23", default-features = false, features = [ # Misc "tracing", "serde", @@ -231,7 +231,7 @@ pastey = "0.2.3" governor = "0.10.4" # CIDR parsing for the trusted proxies of the client IP header -ipnet = "2.12.0" +ipnet = "2.12.1" # OIDC for SSO openidconnect = { version = "4.0.1", default-features = false } @@ -256,10 +256,10 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.57.0", default-features = false, features = ["services-fs"] } +opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.10.0", optional = true, default-features = false, features = [ +aws-config = { version = "1.10.1", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", @@ -267,9 +267,9 @@ aws-config = { version = "1.10.0", optional = true, default-features = false, fe ] } aws-credential-types = { version = "1.3.0", optional = true } aws-smithy-runtime-api = { version = "1.14.0", optional = true } -http = { version = "1.4.2", optional = true } -reqsign-aws-v4 = { version = "3.0.2", optional = true } -reqsign-core = { version = "3.1.0", optional = true } +http = { version = "1.5.0", optional = true } +reqsign-aws-v4 = { version = "3.1.0", optional = true } +reqsign-core = { version = "3.2.1", optional = true } # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 4a51a6b2..4c5e851b 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2026.6.4" -vault_image_digest: "sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427" +vault_version: "v2026.7.0" +vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index baa4c979..7045138d 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4 -# [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.7.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 +# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 -# [docker.io/vaultwarden/web-vault:v2026.6.4] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c +# [docker.io/vaultwarden/web-vault:v2026.7.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 @@ -70,7 +70,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \ # Output the current contents of the file cat /env-cargo -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -86,7 +86,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ +RUN . /env-cargo && \ cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ find . -not -path "./target*" -delete @@ -97,13 +97,13 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -126,6 +126,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.24 ENV ROCKET_PROFILE="release" \ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 9e2e6016..9ab02568 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4 -# [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.7.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 +# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 -# [docker.io/vaultwarden/web-vault:v2026.6.4] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c +# [docker.io/vaultwarden/web-vault:v2026.7.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts @@ -37,6 +37,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.97.1-slim-trixie AS build +# hadolint ignore=DL3067 COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT @@ -80,7 +81,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \ RUN USER=root cargo new --bin /app WORKDIR /app -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -95,7 +96,7 @@ ARG DB=sqlite,mysql,postgresql # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 # https://github.com/dani-garcia/vaultwarden/discussions/7522 @@ -113,7 +114,7 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ @@ -126,7 +127,7 @@ RUN source /env-cargo && \ export XX_RUSTFLAGS; \ fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -149,6 +150,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim ENV ROCKET_PROFILE="release" \ diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index d8b9c8c6..633d6955 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -57,6 +57,7 @@ FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].arch_image[arch] }} AS # hadolint ignore=DL3006 FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build {% if base == "debian" %} +# hadolint ignore=DL3067 COPY --from=xx / / {% endif %} ARG TARGETARCH @@ -116,7 +117,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \ cat /env-cargo {% endif %} -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -136,7 +137,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ +RUN . /env-cargo && \ {% if base == "debian" %} {{ xx_cargo_config() }} && \ {% elif base == "alpine" %} @@ -151,7 +152,7 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ @@ -161,7 +162,7 @@ RUN source /env-cargo && \ {% elif base == "alpine" %} cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ {% endif %} - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -184,6 +185,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }} ENV ROCKET_PROFILE="release" \ diff --git a/src/api/admin.rs b/src/api/admin.rs index 7037bfb1..48f36afd 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -716,6 +716,36 @@ fn web_vault_compare(active: &str, latest: &str) -> i8 { } } +fn check_template_overrides() -> Vec<&'static str> { + let template_folder = std::path::PathBuf::from(CONFIG.templates_folder()); + let mut overrides = Vec::new(); + for folder in ["admin", "email", "scss"] { + if folder_has_hbs_files(&template_folder.join(folder)) { + overrides.push(folder); + } + } + + if folder_has_hbs_files(&template_folder) { + overrides.push("other"); + } + + overrides +} + +fn folder_has_hbs_files(dir: &std::path::Path) -> bool { + let Ok(files) = std::fs::read_dir(dir) else { + // No files in this directory at all, so we can return false + return false; + }; + + files.flatten().any(|f| { + // Validate if it is a file and if it has the `.hbs` extension and starts with a-z or 0-9 + f.file_type().is_ok_and(|t| t.is_file()) + && f.path().extension().is_some_and(|e| e.eq_ignore_ascii_case("hbs")) + && f.file_name().to_str().is_some_and(|n| n.starts_with(|c: char| c.is_ascii_alphanumeric())) + }) +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult> { use chrono::prelude::*; @@ -770,6 +800,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A "db_version": get_sql_server_version(&conn).await, "admin_url": format!("{}/diagnostics", admin_url()), "overrides": &CONFIG.get_overrides().join(", "), + "template_overrides": check_template_overrides().join(", "), "invalid_feature_flags": invalid_feature_flags, "host_arch": env::consts::ARCH, "host_os": env::consts::OS, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 0cdae612..2b51fd0c 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -870,7 +870,7 @@ async fn put_collections_admin( headers: Headers, conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { +) -> JsonResult { post_collections_admin(cipher_id, data, headers, conn, nt).await } @@ -881,7 +881,7 @@ async fn post_collections_admin( headers: Headers, conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { +) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else { @@ -940,7 +940,7 @@ async fn post_collections_admin( ) .await; - Ok(()) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::Organization, &conn).await?)) } #[derive(Deserialize)] diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 08e8d269..eb3d6dfd 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -1,6 +1,10 @@ use rocket::{Route, serde::json::Json}; use serde_json::Value; -use yubico::{config::Config, verify_async}; +use yubico_ng::{ + Verifier, YubicoError, + config::Config, + transport::{AsyncTransport, Response}, +}; use crate::{ CONFIG, @@ -14,12 +18,39 @@ use crate::{ models::{EventType, TwoFactor, TwoFactorType}, }, error::{Error, MapResult}, + http_client, }; pub fn routes() -> Vec { routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] } +struct HttpClientTransport { + client: reqwest::Client, +} + +impl HttpClientTransport { + fn new() -> Result { + http_client::get_reqwest_client_builder(false).redirect(reqwest::redirect::Policy::none()).build().map( + |client| Self { + client, + }, + ) + } +} + +impl AsyncTransport for HttpClientTransport { + type Error = YubicoError; + + async fn yubico_get(&self, url: &str) -> Result { + let response = self.client.get(url).send().await.map_err(YubicoError::transport)?; + Ok(Response { + status: response.status().as_u16(), + body: response.text().await.map_err(YubicoError::transport)?, + }) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct EnableYubikeyData { @@ -44,8 +75,7 @@ pub struct YubikeyMetadata { fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5]; - - data_keys.into_iter().flatten().cloned().collect() + data_keys.into_iter().flatten().filter(|e| !e.is_empty()).cloned().collect() } fn jsonify_yubikeys(yubikeys: Vec) -> Value { @@ -73,13 +103,15 @@ fn get_yubico_credentials() -> Result<(String, String), Error> { async fn verify_yubikey_otp(otp: String) -> EmptyResult { let (yubico_id, yubico_secret) = get_yubico_credentials()?; - let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret); - - match CONFIG.yubico_server() { - Some(server) => verify_async(otp, config.set_api_hosts(vec![server])).await, - None => verify_async(otp, config).await, + let mut config = Config::default().set_client_id(yubico_id).set_key(yubico_secret)?; + if let Some(yubico_server) = CONFIG.yubico_server() { + config = config.set_api_host(yubico_server); } - .map_res("Failed to verify OTP") + + let client = HttpClientTransport::new()?; + let verifier = Verifier::with_client(config, client)?; + + verifier.verify(otp).await.map_res("Failed to verify OTP") } #[post("/two-factor/get-yubikey", data = "")] @@ -137,10 +169,9 @@ async fn activate_yubikey(data: Json, headers: Headers, conn: let yubikeys = parse_yubikeys(&data); if yubikeys.is_empty() { - return Ok(Json(json!({ - "enabled": false, - "object": "twoFactorU2f", - }))); + // Return an error to prevent saving empty keys which would cause users not being able to login anymore. + // To remove all keys users should click the `Deactivate all keys` button + err!("A key is required."); } // Ensure they are valid OTPs diff --git a/src/api/mod.rs b/src/api/mod.rs index 05c4215d..9a79ce95 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -30,7 +30,7 @@ pub use crate::api::{ }, web::catchers as web_catchers, web::routes as web_routes, - web::static_files, + web::{invalidate_css_cache, static_files}, }; use crate::{ CONFIG, diff --git a/src/api/web.rs b/src/api/web.rs index 5bd4c85d..a7eca9fc 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::{Arc, RwLock}, +}; use rocket::{ Catcher, Route, @@ -13,12 +16,13 @@ use crate::{ CONFIG, api::{ApiResult, EmptyResult, core::now}, auth::decode_file_download, + crypto::sha256_hex, db::{ DbConn, models::{AttachmentId, CipherId}, }, error::Error, - util::Cached, + util::{Cached, EtagCached}, }; pub fn routes() -> Vec { @@ -63,8 +67,27 @@ fn not_found() -> ApiResult> { Ok(Html(text)) } +struct CssCache { + css: String, + etag: String, +} + +static CSS_CACHE: RwLock>> = RwLock::new(None); + +pub fn invalidate_css_cache() { + *CSS_CACHE.write().unwrap() = None; +} + #[get("/css/vaultwarden.css")] -fn vaultwarden_css() -> Cached> { +fn vaultwarden_css() -> EtagCached> { + // If reload_templates is false, and we already have the CSS Cached, return this + if !CONFIG.reload_templates() + && let Some(cached) = CSS_CACHE.read().unwrap().as_ref() + { + return EtagCached::new(Css(cached.css.clone()), &cached.etag); + } + + // Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS let css_options = json!({ "emergency_access_allowed": CONFIG.emergency_access_allowed(), "load_user_scss": true, @@ -112,8 +135,18 @@ fn vaultwarden_css() -> Cached> { } }; - // Cache for one day should be enough and not too much - Cached::ttl(Css(css), 86_400, false) + let etag = sha256_hex(css.as_bytes()); + let cached = Arc::new(CssCache { + css, + etag, + }); + + if !CONFIG.reload_templates() { + *CSS_CACHE.write().unwrap() = Some(Arc::clone(&cached)); + } + + // Etag Caching will let the browser send us an etag to verify and send new content if needed + EtagCached::new(Css(cached.css.clone()), &cached.etag) } #[get("/")] diff --git a/src/config.rs b/src/config.rs index 687e2aaf..d5b50146 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1506,6 +1506,9 @@ impl Config { let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; operator.write(&CONFIG_FILENAME, config_str).await?; + // Invalidate CSS Cache because several config items might have impact on the rendered CSS + crate::api::invalidate_css_cache(); + Ok(()) } @@ -1588,6 +1591,9 @@ impl Config { writer._overrides = Vec::new(); } + // Invalidate CSS Cache because several config items might have impact on the rendered CSS + crate::api::invalidate_css_cache(); + Ok(()) } diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 3a6b610c..86cbf5d0 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -298,12 +298,16 @@ impl Event { ) -> Vec { conn.run(move |conn| { event::table - .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid))) + .inner_join( + users_organizations::table + .on(users_organizations::uuid.eq(member_uuid).and(users_organizations::org_uuid.eq(org_uuid))), + ) .filter(event::org_uuid.eq(org_uuid)) .filter(event::event_date.between(start, end)) .filter( - event::user_uuid - .eq(users_organizations::user_uuid.nullable()) + event::org_user_uuid + .eq(member_uuid) + .or(event::user_uuid.eq(users_organizations::user_uuid.nullable())) .or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())), ) .select(event::all_columns) diff --git a/src/error.rs b/src/error.rs index ecbc8199..d90c38e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,7 +58,7 @@ use serde_json::{Error as SerdeErr, Value}; use std::io::Error as IoErr; use std::time::SystemTimeError as TimeErr; use webauthn_rs::prelude::WebauthnError as WebauthnErr; -use yubico::yubicoerror::YubicoError as YubiErr; +use yubico_ng::error::YubicoError as YubiErr; #[derive(Serialize)] pub struct Empty {} diff --git a/src/static/scripts/admin.js b/src/static/scripts/admin.js index 3f6bb1df..fa949a40 100644 --- a/src/static/scripts/admin.js +++ b/src/static/scripts/admin.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser */ -/* exported BASE_URL, _post _delete */ +/* exported BASE_URL, _post, _delete */ function getBaseUrl() { // If the base URL is `https://vaultwarden.example.com/base/path/admin/`, diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 2cff4410..ae4d4235 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -1,5 +1,4 @@ "use strict"; -/* eslint-env es2017, browser */ /* global BASE_URL:readable, bootstrap:readable */ var dnsCheck = false; @@ -80,37 +79,44 @@ async function generateSupportString(event, dj) { event.preventDefault(); event.stopPropagation(); + // Health check Markdown emoji, if something is a failure or not + const chk = v => v ? "true :white_check_mark:" : "false :x:"; + // Yes/No Markdown emoji, if something is not a failure, but just yes or no + const yn = v => v ? "yes :heavy_plus_sign:" : "no :heavy_minus_sign:"; + + const template_overrides = dj.template_overrides !== "" ? ` (${dj.template_overrides})` : ""; let supportString = "### Your environment (Generated via diagnostics page)\n\n"; supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Web-vault version: v${dj.active_web_release}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; - supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; + supportString += `* Running within a container: ${yn(dj.running_within_container)} (Base: ${dj.container_base_image})\n`; supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database version: ${dj.db_version}\n`; - supportString += `* Uses config.json: ${dj.overrides !== ""}\n`; - supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; + supportString += `* Uses config.json: ${yn(dj.overrides !== "")}\n`; + supportString += `* Uses custom templates: ${yn(dj.template_overrides !== "")}${template_overrides}\n`; + supportString += `* Uses a reverse proxy: ${yn(dj.ip_header_exists)}\n`; if (dj.ip_header_exists) { - supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`; + supportString += `* IP Header check: ${chk(dj.ip_header_match)} (${dj.ip_header_name})\n`; } - supportString += `* Internet access: ${dj.has_http_access}\n`; - supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; - supportString += `* DNS Check: ${dnsCheck}\n`; + supportString += `* Internet access: ${chk(dj.has_http_access)}\n`; + supportString += `* Internet access via a proxy: ${yn(dj.uses_proxy)}\n`; + supportString += `* DNS Check: ${chk(dnsCheck)}\n`; if (dj.tz_env !== "") { supportString += `* TZ environment: ${dj.tz_env}\n`; } - supportString += `* Browser/Server Time Check: ${timeCheck}\n`; - supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; - supportString += `* Domain Configuration Check: ${domainCheck}\n`; - supportString += `* HTTPS Check: ${httpsCheck}\n`; + supportString += `* Browser/Server Time Check: ${chk(timeCheck)}\n`; + supportString += `* Server/NTP Time Check: ${chk(ntpTimeCheck)}\n`; + supportString += `* Domain Configuration Check: ${chk(domainCheck)}\n`; + supportString += `* HTTPS Check: ${chk(httpsCheck)}\n`; if (dj.enable_websocket) { - supportString += `* Websocket Check: ${websocketCheck}\n`; + supportString += `* Websocket Check: ${chk(websocketCheck)}\n`; } else { supportString += "* Websocket Check: disabled\n"; } - supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; + supportString += `* HTTP Response Checks: ${chk(httpResponseCheck)}\n`; if (dj.invalid_feature_flags != "") { - supportString += `* Invalid feature flags: true\n`; + supportString += "* Invalid feature flags: true\n"; } const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js index c885344e..33314ad7 100644 --- a/src/static/scripts/admin_organizations.js +++ b/src/static/scripts/admin_organizations.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser, jquery */ -/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global jQuery, _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteOrganization(event) { event.preventDefault(); diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js index 3d61a508..9061719e 100644 --- a/src/static/scripts/admin_settings.js +++ b/src/static/scripts/admin_settings.js @@ -1,5 +1,4 @@ "use strict"; -/* eslint-env es2017, browser */ /* global _post:readable, BASE_URL:readable */ function smtpTest(event) { diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index 99e39aab..a2a643c3 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser, jquery */ -/* global _post:readable, _delete:readable BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global jQuery, _post:readable, _delete:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteUser(event) { event.preventDefault(); diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index ddde389b..0c889353 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -77,6 +77,16 @@ No {{/unless}} +
Uses custom templates
+
+ {{#if page_data.template_overrides}} + Yes + Details + {{/if}} + {{#unless page_data.template_overrides}} + No + {{/unless}} +
Uses a reverse proxy
{{#if page_data.ip_header_exists}} diff --git a/src/storage.rs b/src/storage.rs index ac88d026..689be302 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -67,7 +67,7 @@ pub(crate) fn operator_for_path(path: &str) -> Result bool { diff --git a/src/util.rs b/src/util.rs index 91f075d1..0e8a93e4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -257,6 +257,44 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache } } +pub struct EtagCached { + response: R, + etag: String, +} + +impl EtagCached { + /// An `etag` response should always be quoted + pub fn new(response: R, etag: &str) -> Self { + Self { + response, + etag: format!("\"{etag}\""), + } + } +} + +impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagCached { + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { + // Check and validate a `If-None-Match` ETag header + // Multiple tags could be returned for the same URI if the browser has multiple versions cached + // Also, weak tags are prefixed with `W/`, but ETags are always weak, so just strip it too before comparing + let etag_matches = request + .headers() + .get_one("If-None-Match") + .is_some_and(|v| v.split(',').any(|t| t.trim().trim_start_matches("W/") == self.etag)); + + let mut res = if etag_matches { + Response::build().status(Status::NotModified).ok()? + } else { + self.response.respond_to(request)? + }; + + // Both 200 (OK) and 304 (Not Modified) need to return the etag and cache-control + res.set_raw_header("Etag", self.etag); + res.set_raw_header("Cache-Control", "public, no-cache"); + Ok(res) + } +} + // Log all the routes from the main paths list, and the attachments endpoint // Effectively ignores, any static file route, and the alive endpoint const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"]; From 0cefa4cca7c9f2a5579dd290f78193b543818c51 Mon Sep 17 00:00:00 2001 From: lmogthb Date: Fri, 7 Aug 2026 14:09:43 +0200 Subject: [PATCH 04/34] Include user email in successful login logs (#7496) * Include user email in successful login logs * Modified disable account log to display Email instead of Display Name --------- Co-authored-by: Alejandro Olmos --- src/api/identity.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 9212ed8d..23411dc7 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -318,7 +318,7 @@ async fn sso_login( Some((user, _)) if !user.enabled => { err!( "This user has been disabled", - format!("IP: {}. Username: {}.", ip.ip, user.display_name()), + format!("IP: {}. Username: {}.", ip.ip, user.email), ErrorEvent { event: EventType::UserFailedLogIn } @@ -577,7 +577,7 @@ async fn authenticated_response( result["TwoFactorToken"] = Value::String(token); } - info!("User {} logged in successfully. IP: {}", user.display_name(), ip.ip); + info!("User {} logged in successfully. IP: {}", user.email, ip.ip); Ok(Json(result)) } From 9e78911a2f46818cd98582b5006b9836b46ad912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=B6nisch?= Date: Thu, 20 Aug 2026 17:34:38 +0200 Subject: [PATCH 05/34] Fix sendmail executable permission check (#7483) * Fix sendmail executable permission check * Use access check for sendmail command --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 1 + src/config.rs | 7 ++----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21defe79..b0a36edf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.1" @@ -3228,6 +3234,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5867,6 +5885,7 @@ dependencies = [ "macros", "mimalloc", "moka", + "nix", "num-derive", "num-traits", "opendal", diff --git a/Cargo.toml b/Cargo.toml index f65ead29..3e187ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ oidc-accept-string-booleans = ["openidconnect/accept-string-booleans"] unstable = [] [target."cfg(unix)".dependencies] +nix = { version = "0.31.3", features = ["fs"] } # Logging syslog = "7.0.0" diff --git a/src/config.rs b/src/config.rs index d5b50146..72b58252 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1162,11 +1162,8 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { } #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if !metadata.permissions().mode() & 0o111 != 0 { - err!(format!("sendmail command at `{path:?}` isn't executable")); - } + if nix::unistd::access(&path, nix::unistd::AccessFlags::X_OK).is_err() { + err!(format!("sendmail command at `{path:?}` isn't executable")); } } } From 46d71107f5094460dd5ecbe1dbac6e6c71e5189a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:03:56 +0200 Subject: [PATCH 06/34] add dummy revisionDate (#7608) --- src/db/models/org_policy.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 88b7872c..d501f8b9 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -91,6 +91,7 @@ impl OrgPolicy { "type": self.atype, "data": data_json, "enabled": self.enabled, + "revisionDate": null, "object": "policy", }); From fa2566d14fc745937ce104011475eca9e6c7a6f6 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Mon, 24 Aug 2026 19:38:23 +0200 Subject: [PATCH 07/34] Fix password change with newer web-vault (#7634) --- src/api/core/accounts.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0cb4d3c0..626f22bb 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -595,29 +595,52 @@ async fn post_keys(data: Json, headers: Headers, conn: DbConn) -> Json #[serde(rename_all = "camelCase")] struct ChangePassData { master_password_hash: String, - new_master_password_hash: String, master_password_hint: Option, - key: String, + authentication_data: Option, + unlock_data: Option, + + // Outdated values, might still be used by older clients + new_master_password_hash: Option, + key: Option, } #[post("/accounts/password", data = "")] async fn post_password(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let data: ChangePassData = data.into_inner(); - let mut user = headers.user; + let user = headers.user; if !user.check_valid_password(&data.master_password_hash) { err!("Invalid password") } - user.password_hint = clean_password_hint(data.master_password_hint.as_ref()); - enforce_password_hint_setting(user.password_hint.as_ref())?; - log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) .await; + let (new_master_password_hash, new_key) = + if let (Some(unlock_data), Some(authentication_data)) = (data.unlock_data, data.authentication_data) { + if authentication_data.kdf != unlock_data.kdf { + err!("KDF settings must be equal for authentication and unlock") + } + + if user.email != authentication_data.salt || user.email != unlock_data.salt { + err!("Invalid master password salt") + } + + (authentication_data.master_password_authentication_hash, unlock_data.master_key_wrapped_user_key) + } else if let (Some(new_master_password_hash), Some(new_key)) = (data.new_master_password_hash, data.key) { + (new_master_password_hash, new_key) + } else { + err!("Invalid request!") + }; + + let mut user = user; + + user.password_hint = clean_password_hint(data.master_password_hint.as_ref()); + enforce_password_hint_setting(user.password_hint.as_ref())?; + user.set_password( - &data.new_master_password_hash, - Some(data.key), + &new_master_password_hash, + Some(new_key), true, Some(vec![ String::from("post_rotatekey"), From 10e044f563e6224eb0271419f7b7f4140791dc7f Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 29 Aug 2026 08:01:45 -0700 Subject: [PATCH 08/34] chore: remove duplicate "the" in ciphers.rs comment (#7254) `src/api/core/ciphers.rs:170` comment said "similar to the the userDecryptionOptions" -> "similar to the userDecryptionOptions". Comment-only. Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- src/api/core/ciphers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 2b51fd0c..3e94ca7c 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -167,7 +167,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option Date: Sat, 29 Aug 2026 11:01:51 -0400 Subject: [PATCH 09/34] Ignore reset-password auto-enroll when mail is disabled (#7585) Account recovery requires SMTP. When mail is off, treat the organization reset-password auto-enroll policy as inactive so invite/accept flows are not forced to supply a reset-password key. Fixes #7459 --- src/db/models/org_policy.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index d501f8b9..2b45cd86 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -318,6 +318,13 @@ impl OrgPolicy { } pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool { + // Account recovery depends on outbound mail. When SMTP is disabled, treat the + // auto-enroll policy as inactive so invites/registration are not forced to + // supply a reset-password key (see check_reset_password_applicable). + if !CONFIG.mail_enabled() { + return false; + } + match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await { Some(policy) => match serde_json::from_str::(&policy.data) { Ok(opts) => { From 923f5d0b5eb7e223855031e35ba9606372ff5fa3 Mon Sep 17 00:00:00 2001 From: Timshel Date: Sat, 29 Aug 2026 15:02:01 +0000 Subject: [PATCH 10/34] Fix migration for MariaDB 12.2.2 (#7265) Co-authored-by: Timshel --- .../up.sql | 42 +++++++++++++------ playwright/docker-compose.yml | 2 +- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/migrations/mysql/2024-03-13-170000_sso_users_cascade/up.sql b/migrations/mysql/2024-03-13-170000_sso_users_cascade/up.sql index 9e5e46df..8d1eb178 100644 --- a/migrations/mysql/2024-03-13-170000_sso_users_cascade/up.sql +++ b/migrations/mysql/2024-03-13-170000_sso_users_cascade/up.sql @@ -1,15 +1,31 @@ --- Dynamically create DROP FOREIGN KEY --- Some versions of MySQL or MariaDB might fail if the key doesn't exists --- This checks if the key exists, and if so, will drop it. -SET @drop_sso_fk = IF((SELECT true FROM information_schema.TABLE_CONSTRAINTS WHERE - CONSTRAINT_SCHEMA = DATABASE() AND - TABLE_NAME = 'sso_users' AND - CONSTRAINT_NAME = 'sso_users_ibfk_1' AND - CONSTRAINT_TYPE = 'FOREIGN KEY') = true, - 'ALTER TABLE sso_users DROP FOREIGN KEY sso_users_ibfk_1', - 'SELECT 1'); -PREPARE stmt FROM @drop_sso_fk; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; +SELECT if ( + EXISTS( + SELECT CONSTRAINT_NAME FROM information_schema.table_constraints + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sso_users' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND CONSTRAINT_NAME = 'sso_users_ibfk_1' + ) + ,'ALTER TABLE sso_users DROP FOREIGN KEY `sso_users_ibfk_1`' + ,'SELECT "info: FK sso_users_ibfk_1 does not exist."' +) INTO @drop_stmt; +PREPARE drop_stmt FROM @drop_stmt; +EXECUTE drop_stmt; + +SELECT if ( + EXISTS( + SELECT CONSTRAINT_NAME FROM information_schema.table_constraints + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sso_users' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND CONSTRAINT_NAME = '1' + ) + ,'ALTER TABLE sso_users DROP FOREIGN KEY `1`' + ,'SELECT "info: FK sso_users 1 does not exist."' +) INTO @drop_stmt; +PREPARE drop_stmt FROM @drop_stmt; +EXECUTE drop_stmt; + +DEALLOCATE PREPARE drop_stmt; ALTER TABLE sso_users ADD FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE; diff --git a/playwright/docker-compose.yml b/playwright/docker-compose.yml index 5dd04ff4..5bfc47a5 100644 --- a/playwright/docker-compose.yml +++ b/playwright/docker-compose.yml @@ -61,7 +61,7 @@ services: Mariadb: profiles: ["playwright"] container_name: playwright_mariadb - image: mariadb:11.2.4 + image: mariadb:12.2.2 env_file: test.env healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] From 2073c03092d328e4b5fd19882ecfbe491dc8b5c7 Mon Sep 17 00:00:00 2001 From: Timshel Date: Sat, 29 Aug 2026 15:02:22 +0000 Subject: [PATCH 11/34] Add SSO_SIGNUPS_ALLOWED (#7272) * Add SSO_SIGNUPS_ALLOWED * Fix regression with domain_allowed in SSO onboarding --------- Co-authored-by: Timshel --- .env.template | 3 +++ src/api/identity.rs | 28 +++++++++++++++++++++++++++- src/config.rs | 13 +++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.env.template b/.env.template index 9fc29989..5f6f374c 100644 --- a/.env.template +++ b/.env.template @@ -518,6 +518,9 @@ ## Prevent users from logging in directly without going through SSO # SSO_ONLY=false +## Allow SSO flow to create account. You probably want to disable it when using a public provider. +# SSO_SIGNUPS_ALLOWED=true + ## On SSO Signup if a user with a matching email already exists make the association # SSO_SIGNUPS_MATCH_EMAIL=true diff --git a/src/api/identity.rs b/src/api/identity.rs index 23411dc7..2b1ddfb1 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -234,6 +234,24 @@ async fn sso_login( } ) } + Some((user, None)) + if user.private_key.is_none() + && !CONFIG.sso_signups_allowed() + && !CONFIG.is_email_domain_allowed(&user.email) + && !CONFIG.mail_enabled() + && Invitation::find_by_mail(&user.email, conn).await.is_none() => + { + error!( + "Login failure ({}), no invitation with email ({}) was found", + user_infos.identifier, user.email + ); + err_silent!( + "Missing invitation", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ) + } Some((user, None)) if user.private_key.is_some() && !CONFIG.sso_signups_match_email() => { error!( "Login failure ({}), existing non SSO user ({}) with same email ({}) and association is disabled", @@ -281,7 +299,15 @@ async fn sso_login( // Will trigger 2FA flow if needed let (user, mut device, twofactor_token, sso_user) = match user_with_sso { None => { - if !CONFIG.is_email_domain_allowed(&user_infos.email) { + if !CONFIG.is_sso_signup_allowed(&user_infos.email) { + if CONFIG.signups_domains_whitelist().is_empty() { + err!( + "Signups are disabled. You will need an invitation", + ErrorEvent { + event: EventType::UserFailedLogIn + } + ); + } err!( "Email domain not allowed", ErrorEvent { diff --git a/src/config.rs b/src/config.rs index 72b58252..2502dd02 100644 --- a/src/config.rs +++ b/src/config.rs @@ -817,6 +817,8 @@ make_config! { sso_enabled: bool, true, def, false; /// Only SSO login |> Disable Email+Master Password login sso_only: bool, true, def, false; + /// Allow SSO flow to create account |> You probably want to disable it when using a public provider + sso_signups_allowed: bool, true, def, true; /// Allow email association |> Associate existing non-SSO user based on email sso_signups_match_email: bool, true, def, true; /// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover. @@ -1544,6 +1546,17 @@ impl Config { } } + /// Tests whether SSO signup is allowed for an email address, taking into + /// account the sso_signups_allowed and signups_domains_whitelist settings. + pub fn is_sso_signup_allowed(&self, email: &str) -> bool { + if self.signups_domains_whitelist().is_empty() { + self.sso_signups_allowed() + } else { + // The whitelist setting overrides the signups_allowed setting. + self.is_email_domain_allowed(email) + } + } + // The registration link should be hidden if // - Signup is not allowed and email whitelist is empty unless mail is disabled and invitations are allowed // - The SSO is activated and password login is disabled. From fdc156b247846ca73f6aa3e9c676a6f2f57577cf Mon Sep 17 00:00:00 2001 From: Timshel Date: Sat, 29 Aug 2026 15:02:26 +0000 Subject: [PATCH 12/34] log_event take enum parameter not i32 (#7656) Co-authored-by: Timshel --- src/api/admin.rs | 6 ++--- src/api/core/ciphers.rs | 28 ++++++++------------- src/api/core/events.rs | 6 ++--- src/api/core/organizations.rs | 46 +++++++++++++++++----------------- src/api/core/two_factor/mod.rs | 14 +++-------- 5 files changed, 42 insertions(+), 58 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 48f36afd..eaa681dd 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -425,7 +425,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyR for membership in memberships { log_event( - EventType::OrganizationUserDeleted as i32, + EventType::OrganizationUserDeleted, &membership.uuid, &membership.org_uuid, &ACTING_ADMIN_USER.into(), @@ -446,7 +446,7 @@ async fn delete_sso_user(user_id: UserId, token: AdminToken, conn: DbConn) -> Em for membership in memberships { log_event( - EventType::OrganizationUserUnlinkedSso as i32, + EventType::OrganizationUserUnlinkedSso, &membership.uuid, &membership.org_uuid, &ACTING_ADMIN_USER.into(), @@ -571,7 +571,7 @@ async fn update_membership_type(data: Json, token: AdminToke OrgPolicy::check_user_allowed(&member_to_edit, "modify", &conn).await?; log_event( - EventType::OrganizationUserUpdated as i32, + EventType::OrganizationUserUpdated, &member_to_edit.uuid, &data.org_uuid, &ACTING_ADMIN_USER.into(), diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 3e94ca7c..13021ca3 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -553,16 +553,8 @@ pub async fn update_cipher_from_data( (_, _) => EventType::CipherUpdated, }; - log_event( - event_type as i32, - &cipher.uuid, - org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - conn, - ) - .await; + log_event(event_type, &cipher.uuid, org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn) + .await; } nt.send_cipher_update( ut, @@ -850,7 +842,7 @@ async fn post_collections_update( .await; log_event( - EventType::CipherUpdatedCollections as i32, + EventType::CipherUpdatedCollections, &cipher.uuid, org_uuid, &headers.user.uuid, @@ -930,7 +922,7 @@ async fn post_collections_admin( .await; log_event( - EventType::CipherUpdatedCollections as i32, + EventType::CipherUpdatedCollections, &cipher.uuid, org_uuid, &headers.user.uuid, @@ -1335,7 +1327,7 @@ async fn save_attachment( if let Some(org_id) = &cipher.organization_uuid { log_event( - EventType::CipherAttachmentCreated as i32, + EventType::CipherAttachmentCreated, &cipher.uuid, org_id, &headers.user.uuid, @@ -1696,7 +1688,7 @@ async fn purge_org_vault( nt.send_user_update(UpdateType::SyncVault, &user, headers.device.push_uuid.as_ref(), &conn).await; log_event( - EventType::OrganizationPurgedVault as i32, + EventType::OrganizationPurgedVault, &organization.org_id, &organization.org_id, &user.uuid, @@ -1824,9 +1816,9 @@ async fn delete_cipher_by_uuid( let event_type = if *delete_options == CipherDeleteOptions::SoftSingle || *delete_options == CipherDeleteOptions::SoftMulti { - EventType::CipherSoftDeleted as i32 + EventType::CipherSoftDeleted } else { - EventType::CipherDeleted as i32 + EventType::CipherDeleted }; log_event(event_type, &cipher.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn) @@ -1895,7 +1887,7 @@ async fn restore_cipher_by_uuid( if let Some(org_id) = &cipher.organization_uuid { log_event( - EventType::CipherRestored as i32, + EventType::CipherRestored, &cipher.uuid.clone(), org_id, &headers.user.uuid, @@ -1972,7 +1964,7 @@ async fn delete_cipher_attachment_by_id( if let Some(ref org_id) = cipher.organization_uuid { log_event( - EventType::CipherAttachmentDeleted as i32, + EventType::CipherAttachmentDeleted, &cipher.uuid, org_id, &headers.user.uuid, diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 5518fa3c..2c437a36 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -10,7 +10,7 @@ use crate::{ auth::{AdminHeaders, Headers}, db::{ DbConn, DbPool, - models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, + models::{Cipher, CipherId, Event, EventType, Membership, MembershipId, OrganizationId, UserId}, }, util::parse_date, }; @@ -267,7 +267,7 @@ async fn log_user_event_impl( } pub async fn log_event( - event_type: i32, + event_type: EventType, source_uuid: &str, org_id: &OrganizationId, act_user_id: &UserId, @@ -278,7 +278,7 @@ pub async fn log_event( if !CONFIG.org_events_enabled() { return; } - log_event_impl(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await; + log_event_impl(event_type as i32, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await; } #[expect(clippy::too_many_arguments)] diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 989ca47d..9082297f 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -269,7 +269,7 @@ async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, c } log_event( - EventType::OrganizationUserLeft as i32, + EventType::OrganizationUserLeft, &membership.uuid, &org_id, &headers.user.uuid, @@ -327,7 +327,7 @@ async fn post_organization( org.save(&conn).await?; log_event( - EventType::OrganizationUpdated as i32, + EventType::OrganizationUpdated, org_id.as_ref(), &org_id, &headers.user.uuid, @@ -514,7 +514,7 @@ async fn post_organization_collections( collection.save(&conn).await?; log_event( - EventType::CollectionCreated as i32, + EventType::CollectionCreated, &collection.uuid, &org_id, &headers.user.uuid, @@ -597,7 +597,7 @@ async fn post_bulk_access_collections( collection.save(&conn).await?; log_event( - EventType::CollectionUpdated as i32, + EventType::CollectionUpdated, &collection.uuid, &org_id, &headers.user.uuid, @@ -674,7 +674,7 @@ async fn post_organization_collection_update( collection.save(&conn).await?; log_event( - EventType::CollectionUpdated as i32, + EventType::CollectionUpdated, &collection.uuid, &org_id, &headers.user.uuid, @@ -723,7 +723,7 @@ async fn delete_organization_collection_impl( err!("Collection not found", "Collection does not exist or does not belong to this organization") }; log_event( - EventType::CollectionDeleted as i32, + EventType::CollectionDeleted, &collection.uuid, org_id, &headers.user.uuid, @@ -1148,7 +1148,7 @@ async fn send_invite( } log_event( - EventType::OrganizationUserInvited as i32, + EventType::OrganizationUserInvited, &new_member.uuid, &org_id, &headers.user.uuid, @@ -1447,7 +1447,7 @@ async fn confirm_invite_impl( OrgPolicy::check_user_allowed(&member_to_confirm, "confirm", conn).await?; log_event( - EventType::OrganizationUserConfirmed as i32, + EventType::OrganizationUserConfirmed, &member_to_confirm.uuid, org_id, &headers.user.uuid, @@ -1637,7 +1637,7 @@ async fn edit_member( } log_event( - EventType::OrganizationUserUpdated as i32, + EventType::OrganizationUserUpdated, &member_to_edit.uuid, &org_id, &headers.user.uuid, @@ -1724,7 +1724,7 @@ async fn delete_member_impl( } log_event( - EventType::OrganizationUserRemoved as i32, + EventType::OrganizationUserRemoved, &member_to_delete.uuid, org_id, &headers.user.uuid, @@ -2144,7 +2144,7 @@ async fn put_policy( } log_event( - EventType::OrganizationUserRemoved as i32, + EventType::OrganizationUserRemoved, &member.uuid, &org_id, &headers.user.uuid, @@ -2170,7 +2170,7 @@ async fn put_policy( policy.save(&conn).await?; log_event( - EventType::PolicyUpdated as i32, + EventType::PolicyUpdated, policy.uuid.as_ref(), &org_id, &headers.user.uuid, @@ -2339,7 +2339,7 @@ async fn revoke_member_impl( member.save(conn).await?; log_event( - EventType::OrganizationUserRevoked as i32, + EventType::OrganizationUserRevoked, &member.uuid, org_id, &headers.user.uuid, @@ -2437,7 +2437,7 @@ async fn restore_member_impl( member.save(conn).await?; log_event( - EventType::OrganizationUserRestored as i32, + EventType::OrganizationUserRestored, &member.uuid, org_id, &headers.user.uuid, @@ -2605,7 +2605,7 @@ async fn post_groups( let group = group_request.to_group(&org_id); log_event( - EventType::GroupCreated as i32, + EventType::GroupCreated, &group.uuid, &org_id, &headers.user.uuid, @@ -2646,7 +2646,7 @@ async fn put_group( GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; log_event( - EventType::GroupUpdated as i32, + EventType::GroupUpdated, &updated_group.uuid, &org_id, &headers.user.uuid, @@ -2679,7 +2679,7 @@ async fn add_update_group( user_entry.save(conn).await?; log_event( - EventType::OrganizationUserUpdatedGroups as i32, + EventType::OrganizationUserUpdatedGroups, &assigned_member, &org_id, &headers.user.uuid, @@ -2754,7 +2754,7 @@ async fn delete_group_impl( }; log_event( - EventType::GroupDeleted as i32, + EventType::GroupDeleted, &group.uuid, org_id, &headers.user.uuid, @@ -2865,7 +2865,7 @@ async fn put_group_members( user_entry.save(&conn).await?; log_event( - EventType::OrganizationUserUpdatedGroups as i32, + EventType::OrganizationUserUpdatedGroups, &assigned_member, &org_id, &headers.user.uuid, @@ -2903,7 +2903,7 @@ async fn post_delete_group_member( } log_event( - EventType::OrganizationUserUpdatedGroups as i32, + EventType::OrganizationUserUpdatedGroups, &member_id, &org_id, &headers.user.uuid, @@ -3039,7 +3039,7 @@ async fn recover_account( nt.send_logout(&user, None, &conn).await; log_event( - EventType::OrganizationUserAdminResetPassword as i32, + EventType::OrganizationUserAdminResetPassword, &member_id, &org_id, &headers.user.uuid, @@ -3166,9 +3166,9 @@ async fn put_reset_password_enrollment( membership.save(&conn).await?; let event_type = if membership.reset_password_key.is_some() { - EventType::OrganizationUserResetPasswordEnroll as i32 + EventType::OrganizationUserResetPasswordEnroll } else { - EventType::OrganizationUserResetPasswordWithdraw as i32 + EventType::OrganizationUserResetPasswordWithdraw }; log_event(event_type, &membership.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 8869d23d..c95fb297 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -190,7 +190,7 @@ pub async fn enforce_2fa_policy( member.save(conn).await?; log_event( - EventType::OrganizationUserRevoked as i32, + EventType::OrganizationUserRevoked, &member.uuid, &member.org_uuid, act_user_id, @@ -224,16 +224,8 @@ pub async fn enforce_2fa_policy_for_org( member.revoke(); member.save(conn).await?; - log_event( - EventType::OrganizationUserRevoked as i32, - &member.uuid, - org_id, - act_user_id, - device_type, - ip, - conn, - ) - .await; + log_event(EventType::OrganizationUserRevoked, &member.uuid, org_id, act_user_id, device_type, ip, conn) + .await; } } From 6729e835218edb29b644a99f65a3b76cde9a341d Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Thu, 3 Sep 2026 00:07:00 +0200 Subject: [PATCH 13/34] Misc Updates (#7676) * Misc Updates - Update Rust to v1.98.0 - Update all the crates and adjusted code where needed - Updated JavaScript libraries Removed jquery as this isn't needed anymore, adjusted code where needed - Updated all GitHub Actions - Fixed nightly clippy lint warnings Signed-off-by: BlackDex * Adjust email validation as suggested Signed-off-by: BlackDex --------- Signed-off-by: BlackDex --- .github/workflows/build.yml | 2 +- .github/workflows/hadolint.yml | 6 +- .github/workflows/release.yml | 4 +- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 579 +- Cargo.toml | 36 +- macros/Cargo.toml | 2 +- rust-toolchain.toml | 2 +- src/api/admin.rs | 4 +- src/api/web.rs | 3 - src/config.rs | 2 +- src/main.rs | 8 +- src/static/scripts/admin_organizations.js | 11 +- src/static/scripts/admin_users.js | 76 +- src/static/scripts/datatables.css | 160 +- src/static/scripts/datatables.js | 27072 ++++++++--------- src/static/scripts/jquery-4.0.0.slim.js | 6856 ----- src/static/templates/admin/organizations.hbs | 3 +- src/static/templates/admin/users.hbs | 7 +- src/util.rs | 5 +- 23 files changed, 13321 insertions(+), 21525 deletions(-) delete mode 100644 src/static/scripts/jquery-4.0.0.slim.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31a04012..f52bd8e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,7 +113,7 @@ jobs: # Enable Rust Caching - name: Rust Caching - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: # Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes. # Like changing the build host from Ubuntu 20.04 to 22.04 for example. diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 3111e20b..a429d10f 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -41,12 +41,12 @@ jobs: # Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian) # so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety. - name: Run hadolint on Dockerfile.debian - uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 + uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0 with: dockerfile: docker/Dockerfile.debian - name: Run hadolint on Dockerfile.alpine - uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 + uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0 with: dockerfile: docker/Dockerfile.alpine # End Test Dockerfiles with hadolint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d15dd88..311891b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,13 +58,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 942a99e9..7f41885d 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 779cd6e3..83cd581b 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 + uses: crate-ci/typos@4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index e1de58c3..5e7100b9 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9920696..5269c041 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 + rev: 4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 hooks: - id: typos always_run: true diff --git a/Cargo.lock b/Cargo.lock index b0a36edf..b8335e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -76,13 +77,13 @@ dependencies = [ [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.17", + "cpufeatures 0.3.1", "password-hash", ] @@ -311,13 +312,23 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", +] + +[[package]] +name = "asyncband" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0" +dependencies = [ + "hashbrown 0.17.1", + "slab", ] [[package]] @@ -349,9 +360,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4" +checksum = "a767267da9e2c2e189b2f9df8b5657e850ecf5352644734ba130d4a57095cf1b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -417,9 +428,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.105.0" +version = "1.108.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516" +checksum = "c15301b04372832947916607983b114b3374b9db0be058a00fb7513800de1f05" dependencies = [ "arc-swap", "aws-credential-types", @@ -443,9 +454,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.107.0" +version = "1.110.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c" +checksum = "72cc2c205cb27108183cf1856333f7d584c2ba0f505421b4209ca5828f9ea899" dependencies = [ "arc-swap", "aws-credential-types", @@ -469,9 +480,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.110.0" +version = "1.113.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13" +checksum = "68182ecb449f7537db0f4d5d25917789cf41e32074a9fe47b6a0b847fe1d2032" dependencies = [ "arc-swap", "aws-credential-types", @@ -583,9 +594,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.12.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" +checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -608,9 +619,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" +checksum = "954c563ce84507722d2679f07a35d21b9c6466b3872d513020d0281fc8112ac9" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -648,9 +659,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" +checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647" dependencies = [ "base64-simd", "bytes", @@ -780,11 +791,11 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "blake2" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" dependencies = [ - "digest 0.10.7", + "digest 0.11.3", ] [[package]] @@ -807,9 +818,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel 2.5.0", "async-task", @@ -884,27 +895,28 @@ dependencies = [ [[package]] name = "cached" -version = "2.0.2" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0df7748fe2f601e376916ab19e7bfc2c74461b8abe3bce2ce20036ad8de38f" +checksum = "133b6b7d6a828c24d5055ef51e67457002b4017ae5ea3d1b1552ca35a44b1119" dependencies = [ "ahash", + "async-lock", "cached_proc_macro", "cached_proc_macro_types", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "parking_lot", - "thiserror 2.0.19", - "tokio", + "thiserror 2.0.20", "web-time", ] [[package]] name = "cached_proc_macro" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e734c52502e6cf54dce2ba07108906b04b8fe57f4f5e3ef7d58267b4abf060" +checksum = "da80977bd46ecf98c593b651e393260f852678283989b8b7c4079304fe5e8936" dependencies = [ "darling 0.20.11", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.119", @@ -912,15 +924,15 @@ dependencies = [ [[package]] name = "cached_proc_macro_types" -version = "1.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26cf465651fa6ad902a2d327ba60c3a6bc61c6a2f4ad70d091cf20dfda0074ef" +checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159" [[package]] name = "cc" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -942,12 +954,12 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -989,9 +1001,9 @@ checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -1069,9 +1081,9 @@ dependencies = [ [[package]] name = "cookie" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ "percent-encoding", "time", @@ -1133,9 +1145,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -1152,9 +1164,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -1432,7 +1444,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -1558,9 +1570,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.11" +version = "2.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54d1f576cd3a3460f212a4615fd12ce1b6303c095b79a44449ffbe627753dc1" +checksum = "715377c6e464cb44bb89bd8487584240516c8d5052bc645d6babc50bb8be46c3" dependencies = [ "bigdecimal", "bitflags 2.13.1", @@ -1658,7 +1670,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1751,9 +1763,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" @@ -1896,18 +1908,19 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1948,9 +1961,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1963,9 +1976,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1973,15 +1986,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1990,9 +2003,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -2009,26 +2022,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -2038,9 +2051,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2164,7 +2177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.14.0", + "indexmap 2.14.1", "lasso", "once_cell", "phf 0.11.3", @@ -2183,9 +2196,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -2193,7 +2206,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.5.0", - "indexmap 2.14.0", + "indexmap 2.14.1", "slab", "tokio", "tokio-util", @@ -2213,9 +2226,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.4.3" +version = "6.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4633d16a2350341713c379d6d06a4b9e1845329386026a49ce4fd09c2f3b16f6" +checksum = "75c54236f9045c8004a77942bebc52145b4844639db934a5c70fe08617fbe61a" dependencies = [ "derive_builder", "log", @@ -2224,7 +2237,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", ] @@ -2260,6 +2273,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -2269,9 +2287,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2296,7 +2314,7 @@ dependencies = [ "ipnet", "jni", "rand 0.10.2", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tokio", "tracing", @@ -2317,7 +2335,7 @@ dependencies = [ "prefix-trie", "rand 0.10.2", "ring", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tracing", "url", @@ -2344,7 +2362,7 @@ dependencies = [ "resolv-conf", "smallvec", "system-configuration", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", ] @@ -2440,9 +2458,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2497,9 +2515,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -2523,7 +2541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "rustls 0.23.43", "tokio", @@ -2543,7 +2561,7 @@ dependencies = [ "futures-util", "http 1.5.0", "http-body 1.1.0", - "hyper 1.11.0", + "hyper 1.11.1", "ipnet", "libc", "percent-encoding", @@ -2582,9 +2600,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2596,9 +2614,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2609,9 +2627,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2623,16 +2641,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2643,15 +2662,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2702,9 +2721,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -2839,7 +2858,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -2899,9 +2918,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -2922,7 +2941,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.7", + "rand 0.8.8", "rsa", "serde", "serde_json", @@ -3013,9 +3032,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.37.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" dependencies = [ "cc", "pkg-config", @@ -3030,9 +3049,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -3051,9 +3070,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "value-bag", ] @@ -3078,7 +3097,7 @@ name = "macros" version = "0.1.0" dependencies = [ "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3102,10 +3121,11 @@ dependencies = [ [[package]] name = "mea" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0" +checksum = "c709842c4ce65cb91e2666ad5319dfc1efc3af0d34f02075eddca9000d9f8afb" dependencies = [ + "hashbrown 0.17.1", "slab", ] @@ -3159,9 +3179,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -3180,9 +3200,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "async-lock", "crossbeam-channel", @@ -3301,7 +3321,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "zeroize", ] @@ -3320,14 +3340,14 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -3344,9 +3364,9 @@ dependencies = [ [[package]] name = "num-modular" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" +checksum = "bd8e500409e6cd603b03e477c26a6caecdc27ac58979a53e881c75eafc079f44" [[package]] name = "num-order" @@ -3396,7 +3416,7 @@ dependencies = [ "chrono", "getrandom 0.2.17", "http 1.5.0", - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "serde_path_to_error", @@ -3426,9 +3446,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d" +checksum = "33dbff14cc9bb085224256d6a81289d2f3202e85b06f408d42534b42162a4231" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3437,11 +3457,12 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" +checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7" dependencies = [ "anyhow", + "asyncband", "base64 0.23.1", "bytes", "futures", @@ -3449,7 +3470,6 @@ dependencies = [ "jiff", "log", "md-5", - "mea", "percent-encoding", "quick-xml", "reqsign-core", @@ -3463,9 +3483,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826c4e17a30643b888fe983897f9a4b23b07066e1d069727a923cc8fb419a702" +checksum = "c7ef1e1c45f3f89282a59073897e0d685e51385fed0aea771714789525cff996" dependencies = [ "bytes", "log", @@ -3477,9 +3497,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.58.1" +version = "0.58.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e80cdf192d7eff05feed747894d64f81905ac4eaf132edf7ea270abdd2d663" +checksum = "c64335f9f24ccb62ac36f1d976342b48611a75ba61979813a4f78a4ebd94de42" dependencies = [ "base64 0.23.1", "bytes", @@ -3513,7 +3533,7 @@ dependencies = [ "oauth2", "p256", "p384", - "rand 0.8.7", + "rand 0.8.8", "rsa", "serde", "serde-value", @@ -3660,13 +3680,12 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", + "getrandom 0.4.3", + "phc", ] [[package]] @@ -3731,9 +3750,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -3741,9 +3760,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -3751,9 +3770,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -3764,13 +3783,24 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", + "getrandom 0.4.3", +] + [[package]] name = "phf" version = "0.11.3" @@ -3797,7 +3827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -3883,9 +3913,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "polling" @@ -3903,9 +3933,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -3918,9 +3948,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3971,6 +4001,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -4080,9 +4119,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -4174,22 +4213,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -4240,9 +4279,9 @@ dependencies = [ [[package]] name = "reqsign-aws-core" -version = "3.0.3" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af084e1f3cbf3e67e0c972765399bce54ecec804cceba46b39a8331f3c1bff" +checksum = "bac4749b7dfa7bfaccd01eb03e9dc795ed37e3f20d6f0f38e2c67ee85ad6bc86" dependencies = [ "bytes", "form_urlencoded", @@ -4261,9 +4300,9 @@ dependencies = [ [[package]] name = "reqsign-aws-v4" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac5b3b7cefa28933792b439186459f77f19f9b6edbeab41b8b187150361a206" +checksum = "ff250f0fd0b913fbd565e405acc553da0f13bde30bfb5403178c9d0313cdc15f" dependencies = [ "bytes", "http 1.5.0", @@ -4276,9 +4315,9 @@ dependencies = [ [[package]] name = "reqsign-core" -version = "3.2.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07dd510b1e1b9b241883e483358147fb2ed2d497a7b39b065ba61eb93deceb0" +checksum = "ff052daffb0599681c50f85c59e7236438976efe991ab864edd9f3b235501a0f" dependencies = [ "anyhow", "base64 0.23.1", @@ -4289,6 +4328,7 @@ dependencies = [ "http 1.5.0", "jiff", "log", + "mea", "percent-encoding", "sha1 0.11.0", "sha2 0.11.0", @@ -4297,9 +4337,9 @@ dependencies = [ [[package]] name = "reqsign-file-read-tokio" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663d9d55abd0df0830ef0ae43708297cc1371cf4e8ca91f3ac813c309cca8c98" +checksum = "b3235df90a6bca681aa47dd86f2393d122a6d77042aa8a7c81e218cd45c5bfc0" dependencies = [ "anyhow", "reqsign-core", @@ -4323,7 +4363,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls", "hyper-util", "js-sys", @@ -4413,14 +4453,14 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.14.0", + "indexmap 2.14.1", "log", "memchr", "multer", "num_cpus", "parking_lot", "pin-project-lite", - "rand 0.8.7", + "rand 0.8.8", "ref-cast", "rocket_codegen", "rocket_http", @@ -4445,7 +4485,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.14.0", + "indexmap 2.14.1", "proc-macro2", "quote", "rocket_http", @@ -4465,7 +4505,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.14.0", + "indexmap 2.14.1", "log", "memchr", "pear", @@ -4532,17 +4572,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "rtoolbox" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4608,7 +4648,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki 0.103.15", "subtle", "zeroize", ] @@ -4657,7 +4697,7 @@ dependencies = [ "rustls 0.23.43", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.13", + "rustls-webpki 0.103.15", "security-framework", "security-framework-sys", "webpki-root-certs", @@ -4682,9 +4722,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -4866,7 +4906,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -4875,7 +4915,6 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -4935,16 +4974,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" dependencies = [ "base64 0.22.1", "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.0", + "indexmap 2.14.1", + "jiff", "schemars 0.9.0", "schemars 1.2.2", "serde_core", @@ -4955,9 +4995,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -4983,7 +5023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -5005,7 +5045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -5084,7 +5124,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", ] @@ -5207,11 +5247,11 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svg-hush" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929223e80cdcec0482207576ea09692dd71b2b559057fc172e292ecec9a97559" +checksum = "e690409a034dc81758d2986dcc0e01ffe4a77c65902a120777f5182dd40b1f7f" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "data-url", "quick-error", "url", @@ -5231,9 +5271,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -5323,11 +5363,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -5343,13 +5383,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -5404,9 +5444,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -5452,7 +5492,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -5522,7 +5562,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -5556,13 +5596,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5570,6 +5619,18 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + [[package]] name = "toml_parser" version = "1.1.3+spec-1.1.0" @@ -5727,7 +5788,7 @@ dependencies = [ "http 1.5.0", "httparse", "log", - "rand 0.8.7", + "rand 0.8.8", "sha1 0.10.7", "thiserror 1.0.69", "url", @@ -5822,9 +5883,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -5979,9 +6040,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -5992,9 +6053,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -6002,9 +6063,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6012,9 +6073,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -6025,9 +6086,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -6047,9 +6108,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -6144,9 +6205,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.5" +version = "8.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" dependencies = [ "libc", ] @@ -6276,15 +6337,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -6429,6 +6481,9 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -6438,9 +6493,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x509-parser" @@ -6471,9 +6526,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" [[package]] name = "xmlparser" @@ -6528,18 +6583,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -6589,9 +6644,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -6600,9 +6655,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -6611,15 +6666,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 3e187ff3..7d711fe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2024" -rust-version = "1.95.0" +rust-version = "1.96.1" license = "AGPL-3.0-only" repository = "https://github.com/dani-garcia/vaultwarden" publish = false @@ -66,7 +66,7 @@ syslog = "7.0.0" macros = { path = "./macros" } # Logging -log = "0.4.33" +log = "0.4.34" fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] } # We need the `log` feature for `tracing` to enable logging for several crates to work, like lettre or webauthn-rs tracing = { version = "0.1.44", features = ["log"] } @@ -90,7 +90,7 @@ rmpv = "1.3.1" # MessagePack library dashmap = "6.2.1" # Async futures -futures = "0.3.33" +futures = "0.3.34" tokio = { version = "1.53.1", features = [ "fs", "io-util", @@ -107,7 +107,7 @@ serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # A safe, extensible ORM and Query builder -diesel = { version = "2.3.11", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.12", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = [ @@ -120,7 +120,7 @@ derive_more = { version = "2.1.1", features = [ diesel-derive-newtype = "2.1.3" # SQLite, statically bundled unless the `sqlite_system` feature is enabled -libsqlite3-sys = { version = "0.37.0", optional = true } +libsqlite3-sys = { version = "0.38.2", optional = true } # Crypto-related libraries rand = "0.10.2" @@ -129,7 +129,7 @@ rustls = { version = "0.23.43", features = ["ring", "std"], default-features = f subtle = "2.6.1" # UUID generation -uuid = { version = "1.24.0", features = ["v4"] } +uuid = { version = "1.26.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } @@ -180,7 +180,7 @@ percent-encoding = "2.3.2" # URL encoding library used for URL's in the emails email_address = "0.2.9" # HTML Template library -handlebars = { version = "6.4.3", features = ["dir_source"] } +handlebars = { version = "6.4.4", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) reqwest = { version = "0.13.4", default-features = false, features = [ @@ -212,13 +212,13 @@ regex = { version = "1.13.1", default-features = false, features = [ ] } data-url = "0.3.2" bytes = "1.12.1" -svg-hush = "0.9.6" +svg-hush = "0.9.7" # Cache function results (Used for version check and favicon fetching) -cached = { version = "2.0.2", features = ["async"] } +cached = { version = "3.1.1", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction -cookie = "0.18.1" +cookie = "0.18.2" cookie_store = "0.22.1" # Used by U2F, JWT and PostgreSQL @@ -236,7 +236,7 @@ ipnet = "2.12.1" # OIDC for SSO openidconnect = { version = "4.0.1", default-features = false } -moka = { version = "0.12.15", features = ["future"] } +moka = { version = "0.12.16", features = ["future"] } # Check client versions for specific features. semver = "1.0.28" @@ -245,10 +245,10 @@ semver = "1.0.28" # Mainly used for the musl builds, since the default musl malloc is very slow mimalloc = { version = "0.1.52", optional = true, default-features = false, features = ["secure"] } -which = "8.0.5" +which = "8.0.6" # Argon2 library with support for the PHC format -argon2 = "0.5.3" +argon2 = "0.6.0" # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN rpassword = "7.5.4" @@ -257,20 +257,20 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] } +opendal = { version = "0.58.2", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.10.1", optional = true, default-features = false, features = [ +aws-config = { version = "1.11.0", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", "sso", ] } aws-credential-types = { version = "1.3.0", optional = true } -aws-smithy-runtime-api = { version = "1.14.0", optional = true } +aws-smithy-runtime-api = { version = "1.15.0", optional = true } http = { version = "1.5.0", optional = true } -reqsign-aws-v4 = { version = "3.1.0", optional = true } -reqsign-core = { version = "3.2.1", optional = true } +reqsign-aws-v4 = { version = "3.3.0", optional = true } +reqsign-core = { version = "3.3.1", optional = true } # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces diff --git a/macros/Cargo.toml b/macros/Cargo.toml index f059a214..84d17192 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] quote = "1.0.47" -syn = "3.0.3" +syn = "3.0.4" [lints] workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9c5862a2..9bfb1d94 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.97.1" +channel = "1.98.0" components = [ "rustfmt", "clippy" ] profile = "minimal" diff --git a/src/api/admin.rs b/src/api/admin.rs index eaa681dd..4bdf8e71 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -231,7 +231,7 @@ fn validate_token(token: &str) -> bool { None => false, Some(t) if t.starts_with("$argon2") => { use argon2::password_hash::PasswordVerifier; - match argon2::password_hash::PasswordHash::new(t) { + match argon2::password_hash::phc::PasswordHash::new(t) { Ok(h) => { // NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance. argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok() @@ -647,7 +647,7 @@ use cached::macros::cached; /// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already /// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit /// Any cache will be lost if Vaultwarden is restarted -#[cached(ttl = 600, sync_writes = "default")] +#[cached(ttl_secs = 600, sync_writes = "default")] async fn get_release_info(has_http_access: bool) -> (String, String, String) { // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. if has_http_access { diff --git a/src/api/web.rs b/src/api/web.rs index a7eca9fc..d6d8d62c 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -301,9 +301,6 @@ pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Erro "jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))), "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))), "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))), - "jquery-4.0.0.slim.js" => { - Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-4.0.0.slim.js"))) - } _ => err!(format!("Static file not found: {filename}")), } } diff --git a/src/config.rs b/src/config.rs index 2502dd02..87bea195 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1272,7 +1272,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { if !cfg.disable_admin_token { match cfg.admin_token.as_ref() { Some(t) if t.starts_with("$argon2") => { - if let Err(e) = argon2::password_hash::PasswordHash::new(t) { + if let Err(e) = argon2::password_hash::phc::PasswordHash::new(t) { err!(format!("The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: '{e}'")) } } diff --git a/src/main.rs b/src/main.rs index 28645694..437354af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,9 +137,7 @@ fn parse_args() { if let Some(command) = pargs.subcommand().unwrap_or_default() { if command == "hash" { - use argon2::{ - Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13, password_hash::SaltString, - }; + use argon2::{Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13}; let mut argon2_params = ParamsBuilder::new(); let preset: Option = pargs.opt_value_from_str(["-p", "--preset"]).unwrap_or_default(); @@ -172,10 +170,10 @@ fn parse_args() { } let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap()); - let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap(); + let salt = crypto::get_random_bytes::<32>(); let argon2_timer = tokio::time::Instant::now(); - if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) { + if let Ok(password_hash) = argon2.hash_password_with_salt(password.as_bytes(), &salt) { println!( "\n\ ADMIN_TOKEN='{password_hash}'\n\n\ diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js index 33314ad7..0aa57dcd 100644 --- a/src/static/scripts/admin_organizations.js +++ b/src/static/scripts/admin_organizations.js @@ -1,5 +1,5 @@ "use strict"; -/* global jQuery, _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global DataTable, _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteOrganization(event) { event.preventDefault(); @@ -41,8 +41,9 @@ function initActions() { // onLoad events document.addEventListener("DOMContentLoaded", (/*event*/) => { - jQuery("#orgs-table").DataTable({ - "drawCallback": function() { + const columnCount = document.getElementById("orgs-table").querySelectorAll("thead th").length; + new DataTable("#orgs-table", { + "drawCallback": function () { initActions(); }, "stateSave": true, @@ -53,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { ], "pageLength": -1, // Default show all "columnDefs": [{ - "targets": [4,5], + "targets": [columnCount - 2, columnCount - 1], // Do not include the last two columns into the search/order features "searchable": false, "orderable": false }] @@ -66,4 +67,4 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { if (btnReload) { btnReload.addEventListener("click", reload); } -}); \ No newline at end of file +}); diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index a2a643c3..63ee2d7b 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -1,5 +1,5 @@ "use strict"; -/* global jQuery, _post:readable, _delete:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global DataTable, _post:readable, _delete:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteUser(event) { event.preventDefault(); @@ -141,7 +141,7 @@ function inviteUser(event) { ); } -function resendUserInvite (event) { +function resendUserInvite(event) { event.preventDefault(); event.stopPropagation(); const id = event.target.parentNode.dataset.vwUserUuid; @@ -179,37 +179,9 @@ const ORG_TYPES = { }, }; -// Special sort function to sort dates in ISO format -jQuery.extend(jQuery.fn.dataTableExt.oSort, { - "date-iso-pre": function(a) { - let x; - const sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); - if (sortDate !== "") { - const dtParts = sortDate.split(" "); - const timeParts = (undefined != dtParts[1]) ? dtParts[1].split(":") : ["00", "00", "00"]; - const dateParts = dtParts[0].split("-"); - x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1; - if (isNaN(x)) { - x = 0; - } - } else { - x = Infinity; - } - return x; - }, - - "date-iso-asc": function(a, b) { - return a - b; - }, - - "date-iso-desc": function(a, b) { - return b - a; - } -}); - const userOrgTypeDialog = document.getElementById("userOrgTypeDialog"); // Fill the form and title -userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { +userOrgTypeDialog.addEventListener("show.bs.modal", function (event) { // Get shared values const userEmail = event.relatedTarget.parentNode.dataset.vwUserEmail; const userUuid = event.relatedTarget.parentNode.dataset.vwUserUuid; @@ -227,7 +199,7 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { }, false); // Prevent accidental submission of the form with valid elements after the modal has been hidden. -userOrgTypeDialog.addEventListener("hide.bs.modal", function() { +userOrgTypeDialog.addEventListener("hide.bs.modal", function () { document.getElementById("userOrgTypeDialogOrgName").textContent = ""; document.getElementById("userOrgTypeDialogUserEmail").textContent = ""; document.getElementById("userOrgTypeUserUuid").value = ""; @@ -249,7 +221,7 @@ function updateUserOrgType(event) { function initUserTable() { // Color all the org buttons per type - document.querySelectorAll("button[data-vw-org-type]").forEach(function(e) { + document.querySelectorAll("button[data-vw-org-type]").forEach(function (e) { const orgType = ORG_TYPES[e.dataset.vwOrgType]; e.style.backgroundColor = orgType.bg; if (orgType.font !== undefined) { @@ -285,12 +257,37 @@ function initUserTable() { } } +// Special sort function to sort dates in ISO format and have anything else as 0 +DataTable.ext.type.order["date-iso-pre"] = function (a) { + let x; + const sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); + if (sortDate !== "") { + const dtParts = sortDate.split(" "); + const timeParts = (undefined != dtParts[1]) ? dtParts[1].split(":") : ["00", "00", "00"]; + const dateParts = dtParts[0].split("-"); + x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1; + if (isNaN(x)) { + x = 0; + } + } else { + x = Infinity; + } + return x; +}; + // onLoad events document.addEventListener("DOMContentLoaded", (/*event*/) => { - const size = jQuery("#users-table > thead th").length; - const ssoOffset = size-7; - jQuery("#users-table").DataTable({ - "drawCallback": function() { + DataTable.ext.type.detect.unshift(function (data) { + if (typeof data !== "string") { return null; } + return data.indexOf("data-sort-type=\"date-iso\"") !== -1 + ? "date-iso" + : null; + }); + + const columnCount = document.getElementById("users-table").querySelectorAll("thead th").length; + new DataTable("#users-table", { + "typeDetect": true, + "drawCallback": function () { initUserTable(); }, "stateSave": true, @@ -301,10 +298,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { ], "pageLength": -1, // Default show all "columnDefs": [{ - "targets": [1 + ssoOffset, 2 + ssoOffset], - "type": "date-iso" - }, { - "targets": size-1, + "targets": columnCount - 1, // Do not include the last column into the search/order features "searchable": false, "orderable": false }] diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index e518c143..48b7400c 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,25 +4,32 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.8 + * https://datatables.net/download/#bs5/dt-3.0.3 * * Included libraries: - * DataTables 2.3.8 + * DataTables 3.0.3 */ +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ :root { - --dt-row-selected: 13, 110, 253; - --dt-row-selected-text: 255, 255, 255; - --dt-row-selected-link: 228, 228, 228; - --dt-row-stripe: 0, 0, 0; - --dt-row-hover: 0, 0, 0; - --dt-column-ordering: 0, 0, 0; - --dt-header-align-items: center; - --dt-header-vertical-align: middle; - --dt-html-background: white; + --dt_background-selected: 13, 110, 253; + --dt_color-selected: 255, 255, 255; + --dt_link_color-selected: 228, 228, 228; + --dt-row_background: transparent; + --dt-row_background-selected: var(--dt_background-selected); + --dt-row-text_color-selected: var(--dt_color-selected); + --dt-row-link_color-selected: var(--dt_link_color-selected); + --dt-row_background-stripe: 0, 0, 0; + --dt-row_background-hover: 0, 0, 0; + --dt-column-ordering_background: 0, 0, 0; + --dt-header-cell_align-items: center; + --dt-header-cell_vertical-align: middle; + --dt-html_background: white; } :root.dark { - --dt-html-background: rgb(33, 37, 41); + --dt-html_background: rgb(33, 37, 41); } table.dataTable tbody td.dt-control { @@ -44,16 +51,13 @@ table.dataTable tbody tr.dt-hasChild td.dt-control:before { border-bottom: 0px solid transparent; border-right: 5px solid transparent; } -table.dataTable tfoot:empty { - display: none; -} -html.dark table.dataTable td.dt-control:before, +:root.dark table.dataTable td.dt-control:before, :root[data-bs-theme=dark] table.dataTable td.dt-control:before, :root[data-theme=dark] table.dataTable td.dt-control:before { border-left-color: rgba(255, 255, 255, 0.5); } -html.dark table.dataTable tr.dt-hasChild td.dt-control:before, +:root.dark table.dataTable tr.dt-hasChild td.dt-control:before, :root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before, :root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { border-top-color: rgba(255, 255, 255, 0.5); @@ -84,6 +88,25 @@ div.dt-scroll-body tfoot tr td div.dt-scroll-sizing { overflow: hidden !important; } +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ +:root { + --dt-order-arrow_color: rgb(51, 51, 51); + --dt-order-arrow_color-current: rgb(51, 51, 51); + --dt-order-arrow-height: 7px; + --dt-order-arrow_opacity: 0.125; + --dt-order-arrow_opacity-current: 0.65; + --dt-order-arrow-width: 8px; + --dt-order-arrow-gap: 1px; + --dt-order-header_outline-hover: 2px solid rgba(0, 0, 0, 0.05); +} +:root.dark, :root[data-bs-theme=dark], :root[data-theme=dark] { + --dt-order-arrow_color: rgb(229, 233, 238); + --dt-order-arrow_color-current: rgb(229, 233, 238); + --dt-order-header_outline-hover: 2px solid rgba(255, 255, 255, 0.05); +} + table.dataTable thead > tr > th:active, table.dataTable thead > tr > td:active { outline: none; @@ -91,20 +114,18 @@ table.dataTable thead > tr > td:active { table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before { - position: absolute; - display: block; - bottom: 50%; - content: "\25B2"; - content: "\25B2"/""; + bottom: calc(50% + var(--dt-order-arrow-gap)); + border-bottom: var(--dt-order-arrow-height) solid var(--dt-order-arrow_color); + border-left: calc(var(--dt-order-arrow-width) / 2) solid transparent; + border-right: calc(var(--dt-order-arrow-width) / 2) solid transparent; } table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { - position: absolute; - display: block; - top: 50%; - content: "\25BC"; - content: "\25BC"/""; + top: calc(50% + 1px); + border-top: var(--dt-order-arrow-height) solid var(--dt-order-arrow_color); + border-left: calc(var(--dt-order-arrow-width) / 2) solid transparent; + border-right: calc(var(--dt-order-arrow-width) / 2) solid transparent; } table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order, @@ -112,8 +133,8 @@ table.dataTable thead > tr > td.dt-orderable-desc .dt-column-order, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order { position: relative; - width: 12px; - height: 20px; + width: var(--dt-order-arrow-width); + align-self: stretch; } table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, table.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before, @@ -124,10 +145,14 @@ table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { + position: absolute; + display: block; + content: " "; + height: 0; + width: 0; left: 0; - opacity: 0.125; - line-height: 9px; - font-size: 0.8em; + color: var(--dt-order-arrow_color); + opacity: var(--dt-order-arrow_opacity); } table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > td.dt-orderable-asc, @@ -137,13 +162,18 @@ table.dataTable thead > tr > td.dt-orderable-desc { table.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover, table.dataTable thead > tr > td.dt-orderable-asc:hover, table.dataTable thead > tr > td.dt-orderable-desc:hover { - outline: 2px solid rgba(0, 0, 0, 0.05); + outline: var(--dt-order-header_outline-hover); outline-offset: -2px; } -table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, -table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before { + border-bottom-color: var(--dt-order-arrow_color-current); + opacity: var(--dt-order-arrow_opacity-current); +} +table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after, table.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after { - opacity: 0.6; + border-top-color: var(--dt-order-arrow_color-current); + opacity: var(--dt-order-arrow_opacity-current); } table.dataTable thead > tr > th.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, table.dataTable thead > tr > th.sorting_desc_disabled .dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled .dt-column-order:before, table.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty, @@ -166,7 +196,7 @@ table.dataTable tfoot > tr > td div.dt-column-header, table.dataTable tfoot > tr > td div.dt-column-footer { display: flex; justify-content: space-between; - align-items: var(--dt-header-align-items); + align-items: var(--dt-header-cell_align-items); gap: 4px; } table.dataTable thead > tr > th div.dt-column-header .dt-column-title, @@ -202,7 +232,14 @@ div.dt-scroll-body > table.dataTable > thead > tr > td { :root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover, :root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover, :root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover { - outline: 2px solid rgba(255, 255, 255, 0.05); + outline: var(--dt-order-header_outline-hover); +} + +/*! DataTables Bootstrap 5 integration + * © SpryMedia Ltd - datatables.net/license + */ +:root { + --dt-processing-circle_background: var(--dt_background-selected); } div.dt-processing { @@ -228,8 +265,7 @@ div.dt-processing > div:last-child > div { width: 13px; height: 13px; border-radius: 50%; - background: rgb(13, 110, 253); - background: rgb(var(--dt-row-selected)); + background: rgb(var(--dt-processing-circle_background)); animation-timing-function: cubic-bezier(0, 1, 1, 0); } div.dt-processing > div:last-child > div:nth-child(1) { @@ -342,7 +378,7 @@ table.dataTable thead td, table.dataTable tfoot th, table.dataTable tfoot td { text-align: left; - vertical-align: var(--dt-header-vertical-align); + vertical-align: var(--dt-header-cell_vertical-align); } table.dataTable thead th.dt-head-left, table.dataTable thead td.dt-head-left, @@ -425,11 +461,16 @@ table.dataTable tbody td.dt-body-nowrap { white-space: nowrap; } -/*! Bootstrap 5 integration for DataTables - * - * ©2020 SpryMedia Ltd, all rights reserved. - * License: MIT datatables.net/license/mit - */ +:root { + --dt_background-selected: 13, 110, 253; +} + +:root[data-bs-theme=dark] { + --dt-row_background-hover: 255, 255, 255; + --dt-row_background-stripe: 255, 255, 255; + --dt-column-ordering_background: 255, 255, 255; +} + table.table.dataTable { clear: both; margin-bottom: 0; @@ -443,31 +484,26 @@ table.table.dataTable > :not(caption) > * > * { background-color: var(--bs-table-bg); } table.table.dataTable > tbody > tr { - background-color: transparent; + background-color: var(--dt-row_background); } table.table.dataTable > tbody > tr.selected > * { - box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); - box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); - color: rgb(255, 255, 255); - color: rgb(var(--dt-row-selected-text)); + box-shadow: inset 0 0 0 9999px rgb(var(--dt-row_background-selected)); + color: rgb(var(--dt-row-text_color-selected)); } table.table.dataTable > tbody > tr.selected a { - color: rgb(228, 228, 228); - color: rgb(var(--dt-row-selected-link)); + color: rgb(var(--dt-row-link_color-selected)); } table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-stripe), 0.05); } table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-selected), 0.95); } table.table.dataTable.table-hover > tbody > tr:hover > * { - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-hover), 0.075); } table.table.dataTable.table-hover > tbody > tr.selected:hover > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); - box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row_background-selected), 0.975); } div.dt-container div.dt-layout-start > *:not(:last-child) { @@ -616,10 +652,4 @@ div.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child padding-right: 0; } -:root[data-bs-theme=dark] { - --dt-row-hover: 255, 255, 255; - --dt-row-stripe: 255, 255, 255; - --dt-column-ordering: 255, 255, 255; -} - diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index c9f9ea56..1ae94cd2 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,14196 +4,12792 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.3.8 + * https://datatables.net/download/#bs5/dt-3.0.3 * * Included libraries: - * DataTables 2.3.8 + * DataTables 3.0.3 */ -/*! DataTables 2.3.8 - * © SpryMedia Ltd - datatables.net/license +/*! DataTables 3.0.3 + * Copyright (c) SpryMedia Ltd - datatables.net/license */ -(function( factory ) { - "use strict"; - - if ( typeof define === 'function' && define.amd ) { +(function(factory){ + if (typeof define === 'function' && define.amd) { // AMD - define( ['jquery'], function ( $ ) { - return factory( $, window, document ); - } ); + define([], function () { + return factory(window, document); + }); } - else if ( typeof exports === 'object' ) { + else if (typeof exports === 'object') { // CommonJS - // jQuery's factory checks for a global window - if it isn't present then it - // returns a factory function that expects the window object - var jq = require('jquery'); + var cjsRequires = function (root) { }; if (typeof window === 'undefined') { - module.exports = function (root, $) { - if ( ! root ) { + module.exports = function (root) { + if (! root) { // CommonJS environments without a window global must pass a // root. This will give an error otherwise root = window; } - if ( ! $ ) { - $ = jq( root ); - } - - return factory( $, root, root.document ); + cjsRequires(root); + return factory(root, root.document); }; } else { - module.exports = factory( jq, window, window.document ); + cjsRequires(window); + module.exports = factory(window, window.document); } } else { // Browser - window.DataTable = factory( jQuery, window, document ); + window.DataTable = factory(window, document); } -}(function( $, window, document ) { - "use strict"; +}(function(window, document) { +'use strict'; - - var DataTable = function ( selector, options ) - { - // Check if called with a window or jQuery object for DOM less applications - // This is for backwards compatibility - if (DataTable.factory(selector, options)) { - return DataTable; - } - - // When creating with `new`, create a new DataTable, returning the API instance - if (this instanceof DataTable) { - return $(selector).DataTable(options); - } - else { - // Argument switching - options = selector; - } - - var _that = this; - var emptyInit = options === undefined; - var len = this.length; - - if ( emptyInit ) { - options = {}; - } - - // Method to get DT API instance from jQuery object - this.api = function () - { - return new _Api( this ); - }; - - this.each(function() { - // For each initialisation we want to give it a clean initialisation - // object that can be bashed around - var o = {}; - var oInit = len > 1 ? // optimisation for single table case - _fnExtend( o, options, true ) : - options; - - - var i=0, iLen; - var sId = this.getAttribute( 'id' ); - var defaults = DataTable.defaults; - var $this = $(this); - - // Sanity check - if ( this.nodeName.toLowerCase() != 'table' ) - { - _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); - return; - } - - // Special case for options - if (oInit.on && oInit.on.options) { - _fnListener($this, 'options', oInit.on.options); - } - - $this.trigger( 'options.dt', oInit ); - - /* Backwards compatibility for the defaults */ - _fnCompatOpts( defaults ); - _fnCompatCols( defaults.column ); - - /* Convert the camel-case defaults to Hungarian */ - _fnCamelToHungarian( defaults, defaults, true ); - _fnCamelToHungarian( defaults.column, defaults.column, true ); - - /* Setting up the initialisation object */ - _fnCamelToHungarian( defaults, $.extend( oInit, _fnEscapeObject($this.data()) ), true ); - - - - /* Check to see if we are re-initialising a table */ - var allSettings = DataTable.settings; - for ( i=0, iLen=allSettings.length ; i'), - fastData: function (row, column, type) { - return _fnGetCellData(oSettings, row, column, type); - } - } ); - oSettings.nTable = this; - oSettings.oInit = oInit; - - allSettings.push( oSettings ); - - // Make a single API instance available for internal handling - oSettings.api = new _Api( oSettings ); - - // Need to add the instance after the instance after the settings object has been added - // to the settings array, so we can self reference the table instance if more than one - oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable(); - - // Backwards compatibility, before we apply all the defaults - _fnCompatOpts( oInit ); - - // If the length menu is given, but the init display length is not, use the length menu - if ( oInit.aLengthMenu && ! oInit.iDisplayLength ) - { - oInit.iDisplayLength = Array.isArray(oInit.aLengthMenu[0]) - ? oInit.aLengthMenu[0][0] - : $.isPlainObject( oInit.aLengthMenu[0] ) - ? oInit.aLengthMenu[0].value - : oInit.aLengthMenu[0]; - } - - // Apply the defaults and init options to make a single init object will all - // options defined from defaults and instance options. - oInit = _fnExtend( $.extend( true, {}, defaults ), oInit ); - - - // Map the initialisation options onto the settings object - _fnMap( oSettings.oFeatures, oInit, [ - "bPaginate", - "bLengthChange", - "bFilter", - "bSort", - "bSortMulti", - "bInfo", - "bProcessing", - "bAutoWidth", - "bSortClasses", - "bServerSide", - "bDeferRender" - ] ); - _fnMap( oSettings, oInit, [ - "ajax", - "fnFormatNumber", - "sServerMethod", - "aaSorting", - "aaSortingFixed", - "aLengthMenu", - "sPaginationType", - "iStateDuration", - "bSortCellsTop", - "iTabIndex", - "sDom", - "fnStateLoadCallback", - "fnStateSaveCallback", - "renderer", - "searchDelay", - "rowId", - "caption", - "layout", - "orderDescReverse", - "orderIndicators", - "orderHandler", - "titleRow", - "typeDetect", - "columnTitleTag", - [ "iCookieDuration", "iStateDuration" ], // backwards compat - [ "oSearch", "oPreviousSearch" ], - [ "aoSearchCols", "aoPreSearchCols" ], - [ "iDisplayLength", "_iDisplayLength" ] - ] ); - _fnMap( oSettings.oScroll, oInit, [ - [ "sScrollX", "sX" ], - [ "sScrollXInner", "sXInner" ], - [ "sScrollY", "sY" ], - [ "bScrollCollapse", "bCollapse" ] - ] ); - _fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" ); - - /* Callback functions which are array driven */ - _fnCallbackReg( oSettings, 'aoDrawCallback', oInit.fnDrawCallback ); - _fnCallbackReg( oSettings, 'aoStateSaveParams', oInit.fnStateSaveParams ); - _fnCallbackReg( oSettings, 'aoStateLoadParams', oInit.fnStateLoadParams ); - _fnCallbackReg( oSettings, 'aoStateLoaded', oInit.fnStateLoaded ); - _fnCallbackReg( oSettings, 'aoRowCallback', oInit.fnRowCallback ); - _fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow ); - _fnCallbackReg( oSettings, 'aoHeaderCallback', oInit.fnHeaderCallback ); - _fnCallbackReg( oSettings, 'aoFooterCallback', oInit.fnFooterCallback ); - _fnCallbackReg( oSettings, 'aoInitComplete', oInit.fnInitComplete ); - _fnCallbackReg( oSettings, 'aoPreDrawCallback', oInit.fnPreDrawCallback ); - - oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId ); - - // Add event listeners - if (oInit.on) { - Object.keys(oInit.on).forEach(function (key) { - _fnListener($this, key, oInit.on[key]); - }); - } - - /* Browser support detection */ - _fnBrowserDetect( oSettings ); - - var oClasses = oSettings.oClasses; - - $.extend( oClasses, DataTable.ext.classes, oInit.oClasses ); - $this.addClass( oClasses.table ); - - if (! oSettings.oFeatures.bPaginate) { - oInit.iDisplayStart = 0; - } - - if ( oSettings.iInitDisplayStart === undefined ) - { - /* Display start point, taking into account the save saving */ - oSettings.iInitDisplayStart = oInit.iDisplayStart; - oSettings._iDisplayStart = oInit.iDisplayStart; - } - - var defer = oInit.iDeferLoading; - if ( defer !== null ) - { - oSettings.deferLoading = true; - - var tmp = Array.isArray(defer); - oSettings._iRecordsDisplay = tmp ? defer[0] : defer; - oSettings._iRecordsTotal = tmp ? defer[1] : defer; - } - - /* - * Columns - * See if we should load columns automatically or use defined ones - */ - var columnsInit = []; - var thead = this.getElementsByTagName('thead'); - var initHeaderLayout = _fnDetectHeader( oSettings, thead[0] ); - - // If we don't have a columns array, then generate one with nulls - if ( oInit.aoColumns ) { - columnsInit = oInit.aoColumns; - } - else if ( initHeaderLayout.length ) { - for ( i=0, iLen=initHeaderLayout[0].length ; i').prependTo( $this ); - } - - caption.html( oSettings.caption ); - } - - // Store the caption side, so we can remove the element from the document - // when creating the element - if (caption.length) { - caption[0]._captionSide = caption.css('caption-side'); - oSettings.captionNode = caption[0]; - } - - // Place the colgroup element in the correct location for the HTML structure - if (caption.length) { - oSettings.colgroup.insertAfter(caption); - } - else { - oSettings.colgroup.prependTo(oSettings.nTable); - } - - if ( thead.length === 0 ) { - thead = $('').appendTo($this); - } - oSettings.nTHead = thead[0]; - - var tbody = $this.children('tbody'); - if ( tbody.length === 0 ) { - tbody = $('').insertAfter(thead); - } - oSettings.nTBody = tbody[0]; - - var tfoot = $this.children('tfoot'); - if ( tfoot.length === 0 ) { - // If we are a scrolling table, and no footer has been given, then we need to create - // a tfoot element for the caption element to be appended to - tfoot = $('').appendTo($this); - } - oSettings.nTFoot = tfoot[0]; - - // Copy the data index array - oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); - - // Initialisation complete - table can be drawn - oSettings.bInitialised = true; - - // Language definitions - var oLanguage = oSettings.oLanguage; - $.extend( true, oLanguage, oInit.oLanguage ); - - if ( oLanguage.sUrl ) { - // Get the language definitions from a file - $.ajax( { - dataType: 'json', - url: oLanguage.sUrl, - success: function ( json ) { - _fnCamelToHungarian( defaults.oLanguage, json ); - $.extend( true, oLanguage, json, oSettings.oInit.oLanguage ); - - _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); - _fnInitialise( oSettings ); - }, - error: function () { - // Error occurred loading language file - _fnLog( oSettings, 0, 'i18n file loading error', 21 ); - - // Continue on as best we can - _fnInitialise( oSettings ); - } - } ); - } - else { - _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); - _fnInitialise( oSettings ); - } - } ); - _that = null; - return this; - }; - - - - /** - * DataTables extensions - * - * This namespace acts as a collection area for plug-ins that can be used to - * extend DataTables capabilities. Indeed many of the build in methods - * use this method to provide their own capabilities (sorting methods for - * example). - * - * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy - * reasons - * - * @namespace - */ - DataTable.ext = _ext = { - /** - * DataTables build type (expanded by the download builder) - * - * @type string - */ - builder: "bs5/dt-2.3.8", - - /** - * Buttons. For use with the Buttons extension for DataTables. This is - * defined here so other extensions can define buttons regardless of load - * order. It is _not_ used by DataTables core. - * - * @type object - * @default {} - */ - buttons: {}, - - - /** - * ColumnControl buttons and content - * - * @type object - */ - ccContent: {}, - - - /** - * Element class names - * - * @type object - * @default {} - */ - classes: {}, - - - /** - * Error reporting. - * - * How should DataTables report an error. Can take the value 'alert', - * 'throw', 'none' or a function. - * - * @type string|function - * @default alert - */ - errMode: "alert", - - /** HTML entity escaping */ - escape: { - /** When reading data-* attributes for initialisation options */ - attributes: false - }, - - /** - * Legacy so v1 plug-ins don't throw js errors on load - */ - feature: [], - - /** - * Feature plug-ins. - * - * This is an object of callbacks which provide the features for DataTables - * to be initialised via the `layout` option. - */ - features: {}, - - - /** - * Row searching. - * - * This method of searching is complimentary to the default type based - * searching, and a lot more comprehensive as it allows you complete control - * over the searching logic. Each element in this array is a function - * (parameters described below) that is called for every row in the table, - * and your logic decides if it should be included in the searching data set - * or not. - * - * Searching functions have the following input parameters: - * - * 1. `{object}` DataTables settings object: see - * {@link DataTable.models.oSettings} - * 2. `{array|object}` Data for the row to be processed (same as the - * original format that was passed in as the data source, or an array - * from a DOM data source - * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which - * can be useful to retrieve the `TR` element if you need DOM interaction. - * - * And the following return is expected: - * - * * {boolean} Include the row in the searched result set (true) or not - * (false) - * - * Note that as with the main search ability in DataTables, technically this - * is "filtering", since it is subtractive. However, for consistency in - * naming we call it searching here. - * - * @type array - * @default [] - * - * @example - * // The following example shows custom search being applied to the - * // fourth column (i.e. the data[3] index) based on two input values - * // from the end-user, matching the data in a certain range. - * $.fn.dataTable.ext.search.push( - * function( settings, data, dataIndex ) { - * var min = document.getElementById('min').value * 1; - * var max = document.getElementById('max').value * 1; - * var version = data[3] == "-" ? 0 : data[3]*1; - * - * if ( min == "" && max == "" ) { - * return true; - * } - * else if ( min == "" && version < max ) { - * return true; - * } - * else if ( min < version && "" == max ) { - * return true; - * } - * else if ( min < version && version < max ) { - * return true; - * } - * return false; - * } - * ); - */ - search: [], - - - /** - * Selector extensions - * - * The `selector` option can be used to extend the options available for the - * selector modifier options (`selector-modifier` object data type) that - * each of the three built in selector types offer (row, column and cell + - * their plural counterparts). For example the Select extension uses this - * mechanism to provide an option to select only rows, columns and cells - * that have been marked as selected by the end user (`{selected: true}`), - * which can be used in conjunction with the existing built in selector - * options. - * - * Each property is an array to which functions can be pushed. The functions - * take three attributes: - * - * * Settings object for the host table - * * Options object (`selector-modifier` object type) - * * Array of selected item indexes - * - * The return is an array of the resulting item indexes after the custom - * selector has been applied. - * - * @type object - */ - selector: { - cell: [], - column: [], - row: [] - }, - - - /** - * Legacy configuration options. Enable and disable legacy options that - * are available in DataTables. - * - * @type object - */ - legacy: { - /** - * Enable / disable DataTables 1.9 compatible server-side processing - * requests - * - * @type boolean - * @default null - */ - ajax: null - }, - - - /** - * Pagination plug-in methods. - * - * Each entry in this object is a function and defines which buttons should - * be shown by the pagination rendering method that is used for the table: - * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the - * buttons are displayed in the document, while the functions here tell it - * what buttons to display. This is done by returning an array of button - * descriptions (what each button will do). - * - * Pagination types (the four built in options and any additional plug-in - * options defined here) can be used through the `paginationType` - * initialisation parameter. - * - * The functions defined take two parameters: - * - * 1. `{int} page` The current page index - * 2. `{int} pages` The number of pages in the table - * - * Each function is expected to return an array where each element of the - * array can be one of: - * - * * `first` - Jump to first page when activated - * * `last` - Jump to last page when activated - * * `previous` - Show previous page when activated - * * `next` - Show next page when activated - * * `{int}` - Show page of the index given - * * `{array}` - A nested array containing the above elements to add a - * containing 'DIV' element (might be useful for styling). - * - * Note that DataTables v1.9- used this object slightly differently whereby - * an object with two functions would be defined for each plug-in. That - * ability is still supported by DataTables 1.10+ to provide backwards - * compatibility, but this option of use is now decremented and no longer - * documented in DataTables 1.10+. - * - * @type object - * @default {} - * - * @example - * // Show previous, next and current page buttons only - * $.fn.dataTableExt.oPagination.current = function ( page, pages ) { - * return [ 'previous', page, 'next' ]; - * }; - */ - pager: {}, - - - renderer: { - pageButton: {}, - header: {} - }, - - - /** - * Ordering plug-ins - custom data source - * - * The extension options for ordering of data available here is complimentary - * to the default type based ordering that DataTables typically uses. It - * allows much greater control over the data that is being used to - * order a column, but is necessarily therefore more complex. - * - * This type of ordering is useful if you want to do ordering based on data - * live from the DOM (for example the contents of an 'input' element) rather - * than just the static string that DataTables knows of. - * - * The way these plug-ins work is that you create an array of the values you - * wish to be ordering for the column in question and then return that - * array. The data in the array much be in the index order of the rows in - * the table (not the currently ordering order!). Which order data gathering - * function is run here depends on the `dt-init columns.orderDataType` - * parameter that is used for the column (if any). - * - * The functions defined take two parameters: - * - * 1. `{object}` DataTables settings object: see - * {@link DataTable.models.oSettings} - * 2. `{int}` Target column index - * - * Each function is expected to return an array: - * - * * `{array}` Data for the column to be ordering upon - * - * @type array - * - * @example - * // Ordering using `input` node values - * $.fn.dataTable.ext.order['dom-text'] = function ( settings, col ) - * { - * return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) { - * return $('input', td).val(); - * } ); - * } - */ - order: {}, - - - /** - * Type based plug-ins. - * - * Each column in DataTables has a type assigned to it, either by automatic - * detection or by direct assignment using the `type` option for the column. - * The type of a column will effect how it is ordering and search (plug-ins - * can also make use of the column type if required). - * - * @namespace - */ - type: { - /** - * Automatic column class assignment - */ - className: {}, - - /** - * Type detection functions. - * - * The functions defined in this object are used to automatically detect - * a column's type, making initialisation of DataTables super easy, even - * when complex data is in the table. - * - * The functions defined take two parameters: - * - * 1. `{*}` Data from the column cell to be analysed - * 2. `{settings}` DataTables settings object. This can be used to - * perform context specific type detection - for example detection - * based on language settings such as using a comma for a decimal - * place. Generally speaking the options from the settings will not - * be required - * - * Each function is expected to return: - * - * * `{string|null}` Data type detected, or null if unknown (and thus - * pass it on to the other type detection functions. - * - * @type array - * - * @example - * // Currency type detection plug-in: - * $.fn.dataTable.ext.type.detect.push( - * function ( data, settings ) { - * // Check the numeric part - * if ( ! data.substring(1).match(/[0-9]/) ) { - * return null; - * } - * - * // Check prefixed by currency - * if ( data.charAt(0) == '$' || data.charAt(0) == '£' ) { - * return 'currency'; - * } - * return null; - * } - * ); - */ - detect: [], - - /** - * Automatic renderer assignment - */ - render: {}, - - - /** - * Type based search formatting. - * - * The type based searching functions can be used to pre-format the - * data to be search on. For example, it can be used to strip HTML - * tags or to de-format telephone numbers for numeric only searching. - * - * Note that is a search is not defined for a column of a given type, - * no search formatting will be performed. - * - * Pre-processing of searching data plug-ins - When you assign the sType - * for a column (or have it automatically detected for you by DataTables - * or a type detection plug-in), you will typically be using this for - * custom sorting, but it can also be used to provide custom searching - * by allowing you to pre-processing the data and returning the data in - * the format that should be searched upon. This is done by adding - * functions this object with a parameter name which matches the sType - * for that target column. This is the corollary of afnSortData - * for searching data. - * - * The functions defined take a single parameter: - * - * 1. `{*}` Data from the column cell to be prepared for searching - * - * Each function is expected to return: - * - * * `{string|null}` Formatted string that will be used for the searching. - * - * @type object - * @default {} - * - * @example - * $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) { - * return d.replace(/\n/g," ").replace( /<.*?>/g, "" ); - * } - */ - search: {}, - - - /** - * Type based ordering. - * - * The column type tells DataTables what ordering to apply to the table - * when a column is sorted upon. The order for each type that is defined, - * is defined by the functions available in this object. - * - * Each ordering option can be described by three properties added to - * this object: - * - * * `{type}-pre` - Pre-formatting function - * * `{type}-asc` - Ascending order function - * * `{type}-desc` - Descending order function - * - * All three can be used together, only `{type}-pre` or only - * `{type}-asc` and `{type}-desc` together. It is generally recommended - * that only `{type}-pre` is used, as this provides the optimal - * implementation in terms of speed, although the others are provided - * for compatibility with existing JavaScript sort functions. - * - * `{type}-pre`: Functions defined take a single parameter: - * - * 1. `{*}` Data from the column cell to be prepared for ordering - * - * And return: - * - * * `{*}` Data to be sorted upon - * - * `{type}-asc` and `{type}-desc`: Functions are typical JavaScript sort - * functions, taking two parameters: - * - * 1. `{*}` Data to compare to the second parameter - * 2. `{*}` Data to compare to the first parameter - * - * And returning: - * - * * `{*}` Ordering match: <0 if first parameter should be sorted lower - * than the second parameter, ===0 if the two parameters are equal and - * >0 if the first parameter should be sorted height than the second - * parameter. - * - * @type object - * @default {} - * - * @example - * // Numeric ordering of formatted numbers with a pre-formatter - * $.extend( $.fn.dataTable.ext.type.order, { - * "string-pre": function(x) { - * a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" ); - * return parseFloat( a ); - * } - * } ); - * - * @example - * // Case-sensitive string ordering, with no pre-formatting method - * $.extend( $.fn.dataTable.ext.order, { - * "string-case-asc": function(x,y) { - * return ((x < y) ? -1 : ((x > y) ? 1 : 0)); - * }, - * "string-case-desc": function(x,y) { - * return ((x < y) ? 1 : ((x > y) ? -1 : 0)); - * } - * } ); - */ - order: {} - }, - - /** - * Unique DataTables instance counter - * - * @type int - * @private - */ - _unique: 0, - - - // - // Depreciated - // The following properties are retained for backwards compatibility only. - // The should not be used in new projects and will be removed in a future - // version - // - - /** - * Version check function. - * @type function - * @depreciated Since 1.10 - */ - fnVersionCheck: DataTable.fnVersionCheck, - - - /** - * Index for what 'this' index API functions should use - * @type int - * @deprecated Since v1.10 - */ - iApiIndex: 0, - - - /** - * Software version - * @type string - * @deprecated Since v1.10 - */ - sVersion: DataTable.version - }; - - - // - // Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts - // - $.extend( _ext, { - afnFiltering: _ext.search, - aTypes: _ext.type.detect, - ofnSearch: _ext.type.search, - oSort: _ext.type.order, - afnSortData: _ext.order, - aoFeatures: _ext.feature, - oStdClasses: _ext.classes, - oPagination: _ext.pager - } ); - - - $.extend( DataTable.ext.classes, { - container: 'dt-container', - empty: { - row: 'dt-empty' - }, - info: { - container: 'dt-info' - }, - layout: { - row: 'dt-layout-row', - cell: 'dt-layout-cell', - tableRow: 'dt-layout-table', - tableCell: '', - start: 'dt-layout-start', - end: 'dt-layout-end', - full: 'dt-layout-full' - }, - length: { - container: 'dt-length', - select: 'dt-input' - }, - order: { - canAsc: 'dt-orderable-asc', - canDesc: 'dt-orderable-desc', - isAsc: 'dt-ordering-asc', - isDesc: 'dt-ordering-desc', - none: 'dt-orderable-none', - position: 'sorting_' - }, - processing: { - container: 'dt-processing' - }, - scrolling: { - body: 'dt-scroll-body', - container: 'dt-scroll', - footer: { - self: 'dt-scroll-foot', - inner: 'dt-scroll-footInner' - }, - header: { - self: 'dt-scroll-head', - inner: 'dt-scroll-headInner' - } - }, - search: { - container: 'dt-search', - input: 'dt-input' - }, - table: 'dataTable', - tbody: { - cell: '', - row: '' - }, - thead: { - cell: '', - row: '' - }, - tfoot: { - cell: '', - row: '' - }, - paging: { - active: 'current', - button: 'dt-paging-button', - container: 'dt-paging', - disabled: 'disabled', - nav: '' - } - } ); - - - /* - * It is useful to have variables which are scoped locally so only the - * DataTables functions can access them and they don't leak into global space. - * At the same time these functions are often useful over multiple files in the - * core and API, so we list, or at least document, all variables which are used - * by DataTables as private variables here. This also ensures that there is no - * clashing of variable names and that they can easily referenced for reuse. - */ - - - // Defined else where - // _selector_run - // _selector_opts - // _selector_row_indexes - - var _ext; // DataTable.ext - var _Api; // DataTable.Api - var _api_register; // DataTable.Api.register - var _api_registerPlural; // DataTable.Api.registerPlural - - var _re_dic = {}; - var _re_new_lines = /[\r\n\u2028]/g; - var _re_html = /<([^>]*>)/g; - var _max_str_len = Math.pow(2, 28); - - // This is not strict ISO8601 - Date.parse() is quite lax, although - // implementations differ between browsers. - var _re_date = /^\d{2,4}[./-]\d{1,2}[./-]\d{1,2}([T ]{1}\d{1,2}[:.]\d{2}([.:]\d{2})?)?$/; - - // Escape regular expression special characters - var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); - - // https://en.wikipedia.org/wiki/Foreign_exchange_market - // - \u20BD - Russian ruble. - // - \u20a9 - South Korean Won - // - \u20BA - Turkish Lira - // - \u20B9 - Indian Rupee - // - R - Brazil (R$) and South Africa - // - fr - Swiss Franc - // - kr - Swedish krona, Norwegian krone and Danish krone - // - \u2009 is thin space and \u202F is narrow no-break space, both used in many - // - Ƀ - Bitcoin - // - Ξ - Ethereum - // standards as thousands separators. - var _re_formatted_numeric = /['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi; - - - var _empty = function ( d ) { - return !d || d === true || d === '-' ? true : false; - }; - - - var _intVal = function ( s ) { - var integer = parseInt( s, 10 ); - return !isNaN(integer) && isFinite(s) ? integer : null; - }; - - // Convert from a formatted number with characters other than `.` as the - // decimal place, to a JavaScript number - var _numToDecimal = function ( num, decimalPoint ) { - // Cache created regular expressions for speed as this function is called often - if ( ! _re_dic[ decimalPoint ] ) { - _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' ); - } - return typeof num === 'string' && decimalPoint !== '.' ? - num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : - num; - }; - - - var _isNumber = function ( d, decimalPoint, formatted, allowEmpty ) { - var type = typeof d; - var strType = type === 'string'; - - if ( type === 'number' || type === 'bigint') { - return true; - } - - // If empty return immediately so there must be a number if it is a - // formatted string (this stops the string "k", or "kr", etc being detected - // as a formatted number for currency - if ( allowEmpty && _empty( d ) ) { - return true; - } - - if ( decimalPoint && strType ) { - d = _numToDecimal( d, decimalPoint ); - } - - if ( formatted && strType ) { - d = d.replace( _re_formatted_numeric, '' ); - } - - return !isNaN( parseFloat(d) ) && isFinite( d ); - }; - - - // A string without HTML in it can be considered to be HTML still - var _isHtml = function ( d ) { - return _empty( d ) || typeof d === 'string'; - }; - - // Is a string a number surrounded by HTML? - var _htmlNumeric = function ( d, decimalPoint, formatted, allowEmpty ) { - if ( allowEmpty && _empty( d ) ) { - return true; - } - - // input and select strings mean that this isn't just a number - if (typeof d === 'string' && d.match(/<(input|select)/i)) { - return null; - } - - var html = _isHtml( d ); - return ! html ? - null : - _isNumber( _stripHtml( d ), decimalPoint, formatted, allowEmpty ) ? - true : - null; - }; - - - var _pluck = function ( a, prop, prop2 ) { - var out = []; - var i=0, iLen=a.length; - - // Could have the test in the loop for slightly smaller code, but speed - // is essential here - if ( prop2 !== undefined ) { - for ( ; i _max_str_len) { - throw new Error('Exceeded max str len'); - } - - var previous; - - input = input.replace(_re_html, replacement || ''); // Complete tags - - // Safety for incomplete script tag - use do / while to ensure that - // we get all instances - do { - previous = input; - input = input.replace(/ diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 4c91bc0e..b1dfb17d 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -14,7 +14,7 @@ Entries Attachments Organizations - Actions + Actions @@ -47,10 +47,10 @@ {{/if}} - {{created_at}} + {{created_at}} - {{last_active}} + {{last_active}} {{cipher_count}} @@ -153,7 +153,6 @@ - diff --git a/src/util.rs b/src/util.rs index 0e8a93e4..6de2d803 100644 --- a/src/util.rs +++ b/src/util.rs @@ -537,10 +537,7 @@ pub fn is_valid_email(email: &str) -> bool { let Ok(email_url) = url::Url::parse(&format!("https://{}", email.domain())) else { return false; }; - if email_url.path().ne("/") || email_url.domain().is_none() || email_url.query().is_some() { - return false; - } - true + email_url.domain().is_some() && email_url.path() == "/" && email_url.query().is_none() } // From a6c3bd6d1826fb527822df4a2655f34fd440d7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 3 Sep 2026 21:17:50 +0200 Subject: [PATCH 14/34] Update rust docker version (#7689) --- docker/DockerSettings.yaml | 2 +- docker/Dockerfile.alpine | 8 ++++---- docker/Dockerfile.debian | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 4c5e851b..fdbf40f2 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,7 @@ vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.97.1 # Rust version to be used +rust_version: 1.98.0 # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.24" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 7045138d..491aa9e0 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.97.1 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.97.1 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.97.1 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.97.1 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.98.0 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.98.0 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.98.0 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.98.0 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 9ab02568..280559e2 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.97.1-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.98.0-slim-trixie AS build # hadolint ignore=DL3067 COPY --from=xx / / ARG TARGETARCH From 32d85d03bb5ec401d1378f4cd60139be1d8db3f4 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:13:32 +0200 Subject: [PATCH 15/34] Fix organization import failing with missing field groups (#7699) --- src/api/core/organizations.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 9082297f..c0c90426 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -132,7 +132,6 @@ struct FullCollectionData { name: String, groups: Vec, users: Vec, - id: Option, external_id: Option, } @@ -1793,11 +1792,22 @@ async fn bulk_public_keys( use super::ciphers::CipherData; use super::ciphers::update_cipher_from_data; +// The import endpoint only ever uses the name/id/external_id of a collection. +// Bitwarden's own server ignores `groups`/`users` here too, so do not make them +// mandatory: clients are free to leave them out. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ImportCollectionData { + name: String, + id: Option, + external_id: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ImportData { ciphers: Vec, - collections: Vec, + collections: Vec, collection_relationships: Vec, } From 2ffad8775d8712329aab7d00a05a94f64098170b Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:13:40 +0200 Subject: [PATCH 16/34] Add `pm-32413-multi-client-password-management` feature flag (#7677) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index 5f6f374c..d22145b8 100644 --- a/.env.template +++ b/.env.template @@ -390,6 +390,7 @@ ## ## The following flags are available: ## - "pm-5594-safari-account-switching": Enable account switching in Safari. (Safari >= 2026.2.0) +## - "pm-32413-multi-client-password-management": Enable changing the master password directly in the client. (Desktop/Extension >= 2026.4.0) ## - "ssh-agent": Enable SSH agent support on Desktop. (Desktop >= 2024.12.0) ## - "ssh-agent-v2": Enable newer SSH agent support. (Desktop >= 2026.2.1) ## - "ssh-key-vault-item": Enable the creation and use of SSH key vault items. (Clients >= 2024.12.0) diff --git a/src/config.rs b/src/config.rs index 87bea195..7e21ecf1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1425,6 +1425,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "desktop-ui-migration-milestone-4", // Auth Team "pm-5594-safari-account-switching", + "pm-32413-multi-client-password-management", // Autofill Team "ssh-agent", "ssh-agent-v2", From 277e1536ebe426296519f8bd8bf99f5f6c1c5c77 Mon Sep 17 00:00:00 2001 From: The CRahn <5043504+crahn@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:13:51 -0500 Subject: [PATCH 17/34] Log IP/username on two-factor email-login credential failures (#7654) The three "Username or password is incorrect" errors in send_email_login() (email.rs) don't log the client IP or submitted identifier, unlike the equivalent wrong-password error in password_login() (identity.rs), which logs both via format!("IP: {}. Username: {username}.", ip.ip). This makes the two code paths inconsistent for the same underlying error, and means log-based tooling that keys on the identity.rs error's "IP: x.x.x.x" pattern can't do the same for this endpoint. Bring email.rs's three call sites in line with identity.rs's existing format. The two email-present branches log IP+Username (the email submitted); the device-identifier-only branch (SSO path, no email in scope) logs IP+Device instead of fabricating a username. Verified: cargo build/test/clippy/fmt all pass with the sqlite feature (matching one leg of this repo's own CI matrix), including the two existing unit tests in this file. --- src/api/core/two_factor/email.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 44ba2e7f..3667b871 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -63,13 +63,19 @@ async fn send_email_login(data: Json, client_headers: Client let user = if let Some(email) = email { let Some(user) = User::find_by_mail(email, &conn).await else { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Username: {email}.", client_headers.ip.ip) + ) }; if let Some(master_password_hash) = master_password_hash { // Check password if !user.check_valid_password(master_password_hash) { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Username: {email}.", client_headers.ip.ip) + ) } } else if let Some(auth_request_id) = auth_request_id { let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else { @@ -96,7 +102,10 @@ async fn send_email_login(data: Json, client_headers: Client }; // SSO login only sends device id, so we get the user by the most recently used device let Some(user) = User::find_by_device_for_email2fa(device_identifier, &conn).await else { - err!("Username or password is incorrect. Try again.") + err!( + "Username or password is incorrect. Try again", + format!("IP: {}. Device: {device_identifier}.", client_headers.ip.ip) + ) }; user From 57fbed1bed2e42b540cb790dd536e02633c4f445 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 8 Sep 2026 10:14:01 +0000 Subject: [PATCH 18/34] Support admin reset 2fa (#7435) * Support admin reset 2fa * Fix recovery email --------- Co-authored-by: Timshel --- playwright/tests/organization.smtp.spec.ts | 7 +- src/api/core/organizations.rs | 89 ++++++++++++------- src/auth.rs | 12 ++- src/config.rs | 2 +- src/db/models/event.rs | 8 +- src/mail.rs | 14 ++- .../email/admin_account_recovery.hbs | 12 +++ ...ml.hbs => admin_account_recovery.html.hbs} | 11 ++- .../templates/email/admin_reset_password.hbs | 4 - 9 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 src/static/templates/email/admin_account_recovery.hbs rename src/static/templates/email/{admin_reset_password.html.hbs => admin_account_recovery.html.hbs} (55%) delete mode 100644 src/static/templates/email/admin_reset_password.hbs diff --git a/playwright/tests/organization.smtp.spec.ts b/playwright/tests/organization.smtp.spec.ts index 6d0eb859..1e97ed5d 100644 --- a/playwright/tests/organization.smtp.spec.ts +++ b/playwright/tests/organization.smtp.spec.ts @@ -127,6 +127,9 @@ test('Organization is visible', async ({ page }) => { }); test('Recover user password', async ({ page }) => { + await logUser(test, page, users.user2, { mailBuffer: mail2Buffer }); + await activateTOTP(test, page, users.user2); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); let newPassword = "TotoNewPassword"; @@ -138,9 +141,10 @@ test('Recover user password', async ({ page }) => { await page.getByRole('menuitem', { name: 'Recover account' }).click(); await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword); await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword); + await page.getByRole('checkbox', { name: 'Reset two-step login' }).check(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Account recovery success'); - await mail2Buffer.expect((m) => m.subject.includes('Master Password Has Been Changed')); + await mail2Buffer.expect((m) => m.subject.includes('Admin account recovery from Test organization')); }); let user2 = { @@ -150,6 +154,7 @@ test('Recover user password', async ({ page }) => { }; await logUser(test, page, user2, { mailBuffer: mail2Buffer, + mail2fa: true, notNewDevice: true, }); }); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index c0c90426..4f490854 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use num_traits::FromPrimitive; -use rocket::{Route, serde::json::Json}; +use rocket::{Route, http::Status, serde::json::Json}; use serde_json::Value; use crate::{ @@ -17,7 +17,8 @@ use crate::{ models::{ Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, - OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId, + OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, TwoFactor, TwoFactorType, User, + UserId, }, }, mail, @@ -390,7 +391,7 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } Ok(Json(json!({ @@ -886,11 +887,11 @@ struct OrgIdData { #[get("/ciphers/organization-details?")] async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { if data.organization_id != headers.membership.org_uuid { - err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "Organization id's do not match", Status::NotFound.code); } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } Ok(Json(json!({ @@ -954,7 +955,7 @@ async fn get_members( } if !headers.membership.has_full_access() { - err_code!("Resource not found.", "User does not have full access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have full access", Status::NotFound.code); } let mut users_json = Vec::new(); @@ -2486,7 +2487,7 @@ async fn get_groups_data( || Collection::has_manageable_collection_by_user(&org_id, &headers.membership.user_uuid, &conn).await }; if !allowed { - err_code!("Resource not found.", "User does not have access", rocket::http::Status::NotFound.code); + err_code!("Resource not found.", "User does not have access", Status::NotFound.code); } let groups: Vec = if CONFIG.org_groups_enabled() { @@ -2937,8 +2938,8 @@ struct OrganizationUserResetPasswordEnrollmentRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrganizationUserRecoverAccountRequest { - new_master_password_hash: String, - key: String, + new_master_password_hash: Option, + key: Option, #[serde(default)] reset_master_password: bool, @@ -2982,12 +2983,7 @@ async fn put_recover_account( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let req = data.into_inner(); - if req.reset_master_password && !req.reset_two_factor { - recover_account(org_id, member_id, headers, req, conn, nt).await - } else { - err!("Unsupported operation") - } + recover_account(org_id, member_id, headers, data.into_inner(), conn, nt).await } // Deprecated since `v2026.4.2` @@ -3007,7 +3003,7 @@ async fn recover_account( org_id: OrganizationId, member_id: MembershipId, headers: AdminHeaders, - reset_request: OrganizationUserRecoverAccountRequest, + req: OrganizationUserRecoverAccountRequest, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { @@ -3022,7 +3018,7 @@ async fn recover_account( err!("User to reset isn't member of required organization") }; - let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else { + let Some(mut user) = User::find_by_uuid(&member.user_uuid, &conn).await else { err!("User not found") }; @@ -3035,29 +3031,56 @@ async fn recover_account( err!("Organization user must be confirmed for password reset functionality"); } - // Sending email before resetting password to ensure working email configuration and the resulting - // user notification. Also this might add some protection against security flaws and misuse - if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await { + let fallback_2fa_email = if req.reset_two_factor && CONFIG.email_2fa_auto_fallback() { + TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn).await.is_none() + } else { + false + }; + + // Sending email first ensure working email configuration and the resulting user notification. + // Also this might add some protection against security flaws and misuse + if let Err(e) = mail::send_admin_account_recovery( + &user.email, + user.display_name(), + &org.name, + req.reset_master_password, + req.reset_two_factor, + fallback_2fa_email, + ) + .await + { err!(format!("Error sending user reset password email: {e:#?}")); } - let mut user = user; - user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn) - .await?; + if req.reset_master_password { + if let Some(key) = req.key + && let Some(hash) = req.new_master_password_hash + { + user.set_password(hash.as_str(), Some(key), true, None, &conn).await?; + } else { + err_code!("Unprocessable request", "Missing fields to reset password", Status::UnprocessableEntity.code); + } + } + + if req.reset_two_factor { + TwoFactor::delete_all_by_user(&user.uuid, &conn).await?; + if !fallback_2fa_email || two_factor::email::find_and_activate_email_2fa(&user.uuid, &conn).await.is_err() { + two_factor::enforce_2fa_policy(&user, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn) + .await?; + } + } + user.save(&conn).await?; nt.send_logout(&user, None, &conn).await; - log_event( - EventType::OrganizationUserAdminResetPassword, - &member_id, - &org_id, - &headers.user.uuid, - headers.device.atype, - &headers.ip.ip, - &conn, - ) - .await; + if req.reset_master_password { + headers.log_event(EventType::OrganizationUserAdminResetPassword, &member_id, &org_id, &conn).await; + } + + if req.reset_two_factor { + headers.log_event(EventType::OrganizationUserAdminResetTwoFactor, &member_id, &org_id, &conn).await; + } Ok(()) } diff --git a/src/auth.rs b/src/auth.rs index 762088e5..07373389 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -23,14 +23,14 @@ use rocket::{ use crate::{ CONFIG, - api::ApiResult, + api::{ApiResult, core::log_event}, config::PathType, db::{ DbConn, models::{ AttachmentId, CipherId, Collection, CollectionId, Device, DeviceId, DeviceType, EmergencyAccessId, - Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId, SendFileId, - SendId, User, UserId, UserStampException, + EventType, Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId, + SendFileId, SendId, User, UserId, UserStampException, }, }, error::Error, @@ -822,6 +822,12 @@ pub struct AdminHeaders { pub org_id: OrganizationId, } +impl AdminHeaders { + pub async fn log_event(&self, event_type: EventType, source_uuid: &str, org_id: &OrganizationId, conn: &DbConn) { + log_event(event_type, source_uuid, org_id, &self.user.uuid, self.device.atype, &self.ip.ip, conn).await; + } +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for AdminHeaders { type Error = &'static str; diff --git a/src/config.rs b/src/config.rs index 7e21ecf1..37fc3e85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1745,7 +1745,7 @@ where reg!("email/email_footer"); reg!("email/email_footer_text"); - reg!("email/admin_reset_password", ".html"); + reg!("email/admin_account_recovery", ".html"); reg!("email/change_email_existing", ".html"); reg!("email/change_email_invited", ".html"); reg!("email/change_email", ".html"); diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 86cbf5d0..1f307979 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -43,7 +43,7 @@ pub struct Event { pub provider_org_uuid: Option, } -// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs +// Upstream enum: https://github.com/bitwarden/server/blob/v2026.6.2/src/Core/Dirt/Enums/EventType.cs #[derive(Debug, Copy, Clone)] pub enum EventType { // User @@ -108,6 +108,12 @@ pub enum EventType { OrganizationUserRejectedAuthRequest = 1514, OrganizationUserDeleted = 1515, // Both user and organization user data were deleted OrganizationUserLeft = 1516, // User voluntarily left the organization + // OrganizationUserAutomaticallyConfirmed = 1517, + // OrganizationUserSelfRevoked = 1518, // User self-revoked due to declining organization data ownership policy + OrganizationUserAdminResetTwoFactor = 1519, + // OrganizationUserRevoked_TwoFactorNonCompliance = 1520, + // OrganizationUserRevoked_SingleOrganizationNonCompliance = 1521, + // OrganizationUserNotificationBannerActionClicked = 1522, // Organization OrganizationUpdated = 1600, diff --git a/src/mail.rs b/src/mail.rs index a7e5e5ae..b20f2853 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -633,14 +633,24 @@ pub async fn send_test(address: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } -pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: &str) -> EmptyResult { +pub async fn send_admin_account_recovery( + address: &str, + user_name: &str, + org_name: &str, + reset_password: bool, + reset_2fa: bool, + fallback_2fa_email: bool, +) -> EmptyResult { let (subject, body_html, body_text) = get_text( - "email/admin_reset_password", + "email/admin_account_recovery", json!({ "url": CONFIG.domain(), "img_src": CONFIG._smtp_img_src(), "user_name": user_name, "org_name": org_name, + "reset_password": reset_password, + "reset_2fa": reset_2fa, + "fallback_2fa_email": fallback_2fa_email, }), )?; send_email(address, &subject, body_html, body_text).await diff --git a/src/static/templates/email/admin_account_recovery.hbs b/src/static/templates/email/admin_account_recovery.hbs new file mode 100644 index 00000000..a35a1d05 --- /dev/null +++ b/src/static/templates/email/admin_account_recovery.hbs @@ -0,0 +1,12 @@ +Admin account recovery from {{org_name}} organization + +{{#if reset_password}} +The master password for {{user_name}} has been changed. +{{/if}} +{{#if reset_2fa}} +Your two-step verification providers have been reset.{{#if fallback_2fa_email}} Email two factor has been activated as a fallback.{{/if}} +{{/if}} + +If you did not initiate this request, please reach out to your administrator immediately. + +{{> email/email_footer_text }} diff --git a/src/static/templates/email/admin_reset_password.html.hbs b/src/static/templates/email/admin_account_recovery.html.hbs similarity index 55% rename from src/static/templates/email/admin_reset_password.html.hbs rename to src/static/templates/email/admin_account_recovery.html.hbs index d9749d22..cf8eebed 100644 --- a/src/static/templates/email/admin_reset_password.html.hbs +++ b/src/static/templates/email/admin_account_recovery.html.hbs @@ -1,10 +1,17 @@ -Master Password Has Been Changed +Admin account recovery from {{org_name}} organization {{> email/email_header }}
- The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. + {{#if reset_password}} + The master password for {{user_name}} has been changed. + {{/if}} + {{#if reset_2fa}} + Your two-step verification providers have been reset.{{#if fallback_2fa_email}} Email two factor has been activated as a fallback.{{/if}} + {{/if}} +
+ If you did not initiate this request, please reach out to your administrator immediately.
diff --git a/src/static/templates/email/admin_reset_password.hbs b/src/static/templates/email/admin_reset_password.hbs deleted file mode 100644 index f70423f1..00000000 --- a/src/static/templates/email/admin_reset_password.hbs +++ /dev/null @@ -1,4 +0,0 @@ -Master Password Has Been Changed - -The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. -{{> email/email_footer_text }} From f1ff61300844b0664907393c0fe93f5092654784 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 8 Sep 2026 12:14:07 +0200 Subject: [PATCH 19/34] fix(security): revoke 2FA remember tokens when credentials or 2FA change (#7682) --- src/api/core/two_factor/mod.rs | 5 +++-- src/api/identity.rs | 6 ++++++ src/db/models/device.rs | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index c95fb297..0eb6563e 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -16,8 +16,8 @@ use crate::{ db::{ DbConn, DbPool, models::{ - DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor, - TwoFactorIncomplete, TwoFactorType, User, UserId, + Device, DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, + TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId, }, }, mail, @@ -151,6 +151,7 @@ async fn disable_twofactor(data: Json, headers: Headers, c if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { twofactor.delete(&conn).await?; + Device::clear_twofactor_remember_by_user(&user.uuid, &conn).await?; log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn) .await; } diff --git a/src/api/identity.rs b/src/api/identity.rs index 2b1ddfb1..a2525d9b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -905,6 +905,12 @@ async fn twofactor_auth( // Remove all twofactors from the user TwoFactor::delete_all_by_user(&user.uuid, conn).await?; + + // No device may keep skipping 2FA once every second factor is gone. + // `device` is cleared in memory too, since saving it later would restore its token. + Device::clear_twofactor_remember_by_user(&user.uuid, conn).await?; + device.delete_twofactor_remember(); + enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 6c1b686a..cc8f1cec 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -266,10 +266,22 @@ impl Device { let devices = Self::find_by_user(user_uuid, conn).await; for mut device in devices { device.refresh_token = Device::generate_refresh_token(); + device.twofactor_remember = None; device.save(false, conn).await?; } Ok(()) } + + pub async fn clear_twofactor_remember_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { + conn.run(move |conn| { + diesel::update(devices::table) + .filter(devices::user_uuid.eq(user_uuid)) + .set(devices::twofactor_remember.eq::>(None)) + .execute(conn) + .map_res("Error removing two factor remember tokens") + }) + .await + } } #[derive(Display)] From f1c36b8c1d9b2cdd0f1cf6f1c4062f81c3a70302 Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 8 Sep 2026 12:14:16 +0200 Subject: [PATCH 20/34] fix(security): rate limit prelogin and auth request endpoints (#7681) --- src/api/core/accounts.rs | 16 +++++++++++----- src/api/identity.rs | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 626f22bb..3ea6eada 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1340,11 +1340,13 @@ pub struct PreloginData { } #[post("/accounts/prelogin", data = "")] -async fn post_prelogin(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn post_prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } -pub async fn prelogin(data: Json, conn: DbConn) -> Json { +pub async fn prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&ip.ip)?; + let data: PreloginData = data.into_inner(); let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await { @@ -1352,7 +1354,7 @@ pub async fn prelogin(data: Json, conn: DbConn) -> Json { None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None), }; - Json(json!({ + Ok(Json(json!({ "kdf": kdf_type, "kdfIterations": kdf_iter, "kdfMemory": kdf_mem, @@ -1364,7 +1366,7 @@ pub async fn prelogin(data: Json, conn: DbConn) -> Json { "parallelism": kdf_para }, "salt": null, - })) + }))) } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs @@ -1595,6 +1597,8 @@ async fn post_auth_request( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&client_headers.ip.ip)?; + let data = data.into_inner(); let Some(user) = User::find_by_mail(&data.email, &conn).await else { @@ -1756,6 +1760,8 @@ async fn get_auth_request_response( client_headers: ClientHeaders, conn: DbConn, ) -> JsonResult { + crate::ratelimit::check_limit_unauthenticated(&client_headers.ip.ip)?; + let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &conn).await else { err!("AuthRequest doesn't exist", "User not found") }; diff --git a/src/api/identity.rs b/src/api/identity.rs index a2525d9b..7bd12a78 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1056,13 +1056,13 @@ async fn json_err_twofactor( } #[post("/accounts/prelogin", data = "")] -async fn post_prelogin(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn post_prelogin(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } #[post("/accounts/prelogin/password", data = "")] -async fn prelogin_password(data: Json, conn: DbConn) -> Json { - prelogin(data, conn).await +async fn prelogin_password(data: Json, ip: ClientIp, conn: DbConn) -> JsonResult { + prelogin(data, ip, conn).await } #[post("/accounts/register", data = "")] From b7667e27bf3500a2446d39446b1a7b10b8b25991 Mon Sep 17 00:00:00 2001 From: niniconi <112842746+niniconi@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:42:58 +0800 Subject: [PATCH 21/34] fix: Correct invalid comment syntax in .dockerignore (#7274) Docker only recognizes `#` as a valid comment indicator in .dockerignore files. Using `//` causes the lines to be incorrectly parsed as glob patterns rather than comments. While this may not cause fatal errors if no matching files exist, it is syntactically invalid and could lead to unexpected behavior. Corrected the syntax to use `#`. --- .dockerignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index a9a358a3..d6ac6b9b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ -// Ignore everything +# Ignore everything * -// Allow what is needed +# Allow what is needed !.git !docker/healthcheck.sh !docker/start.sh From de7abaaafa5ce6627e43efa52840f6df6f43da23 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 9 Sep 2026 11:51:23 +0200 Subject: [PATCH 22/34] Update Rust and adjust DockerSettings (#7690) - Update Rust to v1.98.1 which resolves a build issues with strange outcomes - Adjusted the DockerSettings and render_template to extract the `rust_version` from the `rust-toolchain.toml` file. This should prevent mismatches and forgetting to update DockerSettings. - Updated typos in GHA and Pre-Commit - Updated all possible crates including hickory which has several CVE's fixed. Signed-off-by: BlackDex --- .github/workflows/typos.yml | 2 +- .github/workflows/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 357 +++++++++++++++++++---------------- Cargo.toml | 19 +- docker/DockerSettings.yaml | 3 +- docker/Dockerfile.alpine | 8 +- docker/Dockerfile.debian | 2 +- docker/render_template | 10 +- macros/Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 11 files changed, 227 insertions(+), 182 deletions(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 83cd581b..00fcbab4 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 + uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 5e7100b9..31153b07 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5269c041..e8319414 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: 4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0 + rev: d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 hooks: - id: typos always_run: true diff --git a/Cargo.lock b/Cargo.lock index b8335e5b..f9c763d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +checksum = "24a8ec73eb862508b7041723c89386365894b4e7d9f6998bf1b8529e5b0ee254" dependencies = [ "compression-codecs", "compression-core", @@ -318,7 +318,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -360,9 +360,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a767267da9e2c2e189b2f9df8b5657e850ecf5352644734ba130d4a57095cf1b" +checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -403,9 +403,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" +checksum = "ef47857a1d4488b528f4a5d5715fa7c3300820897824152234d3fa22b1426657" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.108.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15301b04372832947916607983b114b3374b9db0be058a00fb7513800de1f05" +checksum = "c3cfe74df5d9ad2fedd691973ad3521ebf4f27a3c68c792556686aedb5519bab" dependencies = [ "arc-swap", "aws-credential-types", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.110.0" +version = "1.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cc2c205cb27108183cf1856333f7d584c2ba0f505421b4209ca5828f9ea899" +checksum = "81b0ec31ed6191bd11350aae4b2004198f2db21350cb0a20c57e0a92e55dd161" dependencies = [ "arc-swap", "aws-credential-types", @@ -480,9 +480,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.113.0" +version = "1.114.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68182ecb449f7537db0f4d5d25917789cf41e32074a9fe47b6a0b847fe1d2032" +checksum = "ef45745026107ec30c4ef86bd8ae4b002e7e5f6a86e4225240bdf6b06a0b944a" dependencies = [ "arc-swap", "aws-credential-types", @@ -619,9 +619,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954c563ce84507722d2679f07a35d21b9c6466b3872d513020d0281fc8112ac9" +checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -659,9 +659,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.6.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647" +checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9" dependencies = [ "base64-simd", "bytes", @@ -694,9 +694,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" +checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -895,9 +895,9 @@ dependencies = [ [[package]] name = "cached" -version = "3.1.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133b6b7d6a828c24d5055ef51e67457002b4017ae5ea3d1b1552ca35a44b1119" +checksum = "c5a6cf8262820194a1488ece477f5fb5ba9256ef25229d525470e71d6e9a5835" dependencies = [ "ahash", "async-lock", @@ -911,9 +911,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da80977bd46ecf98c593b651e393260f852678283989b8b7c4079304fe5e8936" +checksum = "ece0579b43cf6e927b3370c7d44359c7122e7e52b3ea635bc8484da571442edb" dependencies = [ "darling 0.20.11", "proc-macro-crate", @@ -930,9 +930,9 @@ checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159" [[package]] name = "cc" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "jobserver", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +checksum = "100590da849306918656ffbb22576bbdfc1382b50ad10d1d50ec177d3b205fb2" dependencies = [ "brotli", "compression-core", @@ -1025,9 +1025,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" [[package]] name = "concurrent-queue" @@ -1134,6 +1134,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1191,27 +1197,27 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -1308,12 +1314,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -1346,15 +1352,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1381,13 +1387,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.1", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1570,9 +1576,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.12" +version = "2.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715377c6e464cb44bb89bd8487584240516c8d5052bc645d6babc50bb8be46c3" +checksum = "e3b934ddbdcb2abb9f9fc9c30bd47bcc5618b615eea1d334cda5fdf8ff9b072a" dependencies = [ "bigdecimal", "bitflags 2.13.1", @@ -1607,9 +1613,9 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.3.9" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +checksum = "ecbd51fb6c020672543641167efa4e6417ff7ad76849ed556ace3595e72de03a" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", @@ -1670,7 +1676,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1809,11 +1815,17 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -1908,9 +1920,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" @@ -2028,7 +2040,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -2177,7 +2189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" dependencies = [ "codemap", - "indexmap 2.14.1", + "indexmap 2.14.2", "lasso", "once_cell", "phf 0.11.3", @@ -2206,7 +2218,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.5.0", - "indexmap 2.14.1", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -2299,9 +2311,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +checksum = "084e7bd6a377435d568f652153e571b50970d7ccc1d1eeec0519f834632287e1" dependencies = [ "async-trait", "cfg-if", @@ -2323,9 +2335,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d" dependencies = [ "data-encoding", "idna", @@ -2343,9 +2355,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +checksum = "0e4f9f4603319422d482738f3f6fe5aac03157fdbfed1cd85a3ff45adb09072f" dependencies = [ "cfg-if", "futures-util", @@ -2483,9 +2495,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -2543,9 +2555,9 @@ dependencies = [ "http 1.5.0", "hyper 1.11.1", "hyper-util", - "rustls 0.23.43", + "rustls 0.23.44", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tower-service", ] @@ -2721,9 +2733,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.1" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -2752,9 +2764,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" dependencies = [ "serde", ] @@ -2918,9 +2930,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -2999,12 +3011,12 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-native-certs", "serde", "socket2 0.6.5", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tracing", "url", ] @@ -3097,7 +3109,7 @@ name = "macros" version = "0.1.0" dependencies = [ "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3189,9 +3201,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -3237,6 +3249,33 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "mysqlclient-sys" version = "0.5.2" @@ -3340,7 +3379,7 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3446,9 +3485,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dbff14cc9bb085224256d6a81289d2f3202e85b06f408d42534b42162a4231" +checksum = "f950151f9587a51a7bed70a15fa0cff464eae96e41ae7499f97067bdafdf43eb" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3457,9 +3496,9 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7" +checksum = "a43405d217dfdfb543f58847336d3af672897dd1939bb7dcf314b63cf364f1c9" dependencies = [ "anyhow", "asyncband", @@ -3483,9 +3522,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ef1e1c45f3f89282a59073897e0d685e51385fed0aea771714789525cff996" +checksum = "fb9caf04d6d38713299dd4abac984b95ab16ee20e1ff09160f495c5a64644083" dependencies = [ "bytes", "log", @@ -3497,9 +3536,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.58.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64335f9f24ccb62ac36f1d976342b48611a75ba61979813a4f78a4ebd94de42" +checksum = "388b1d39b62535c62803754ebef89808859558697366dbedd0299345887ba461" dependencies = [ "base64 0.23.1", "bytes", @@ -3750,9 +3789,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" +checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" dependencies = [ "memchr", "ucd-trie", @@ -3760,9 +3799,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" +checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" dependencies = [ "pest", "pest_generator", @@ -3770,9 +3809,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" +checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" dependencies = [ "pest", "pest_meta", @@ -3783,9 +3822,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" +checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" dependencies = [ "pest", ] @@ -3939,9 +3978,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] @@ -4228,7 +4267,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4348,11 +4387,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "cookie", "cookie_store", @@ -4371,7 +4410,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4379,7 +4418,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.5", "tokio-util", "tower", "tower-http", @@ -4453,7 +4492,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.14.1", + "indexmap 2.14.2", "log", "memchr", "multer", @@ -4485,7 +4524,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.14.1", + "indexmap 2.14.2", "proc-macro2", "quote", "rocket_http", @@ -4505,7 +4544,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.14.1", + "indexmap 2.14.2", "log", "memchr", "pear", @@ -4640,9 +4679,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "log", "once_cell", @@ -4694,7 +4733,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.43", + "rustls 0.23.44", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", @@ -4906,7 +4945,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4974,16 +5013,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.1", + "indexmap 2.14.2", "jiff", "schemars 0.9.0", "schemars 1.2.2", @@ -4995,14 +5034,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -5142,9 +5181,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" [[package]] name = "socket2" @@ -5271,9 +5310,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -5389,7 +5428,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5454,9 +5493,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" dependencies = [ "tinyvec_macros", ] @@ -5492,7 +5531,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5507,11 +5546,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ - "rustls 0.23.43", + "rustls 0.23.44", "tokio", ] @@ -5611,7 +5650,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.14.1", + "indexmap 2.14.2", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5625,7 +5664,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap 2.14.1", + "indexmap 2.14.2", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.4", @@ -5901,9 +5940,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.2" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" [[package]] name = "vaultwarden" @@ -5965,7 +6004,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.43", + "rustls 0.23.44", "semver", "serde", "serde_json", @@ -6040,9 +6079,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -6053,9 +6092,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -6063,9 +6102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6073,22 +6112,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -6108,9 +6147,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -6583,18 +6622,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -6672,7 +6711,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -6689,27 +6728,27 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "bf06bd8162af0734b344780deb55b42a2429ae430870d13fcc12f238e880fe6e" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ae42c0555055784c70058d19ba8e275528e8a99a706684868ace5da4e716a4ab" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 7d711fe1..cc6dff02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ members = ["macros"] name = "vaultwarden" version = "1.0.0" authors = ["Daniel García "] -readme = "README.md" build = "build.rs" repository.workspace = true edition.workspace = true @@ -107,7 +106,7 @@ serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # A safe, extensible ORM and Query builder -diesel = { version = "2.3.12", features = ["chrono", "r2d2", "numeric"] } +diesel = { version = "2.3.13", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = [ @@ -125,7 +124,7 @@ libsqlite3-sys = { version = "0.38.2", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.43", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.44", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation @@ -183,7 +182,7 @@ email_address = "0.2.9" handlebars = { version = "6.4.4", features = ["dir_source"] } # HTTP client (Used for favicons, version check, DUO and HIBP API) -reqwest = { version = "0.13.4", default-features = false, features = [ +reqwest = { version = "0.13.5", default-features = false, features = [ # Misc "charset", "cookies", @@ -201,7 +200,7 @@ reqwest = { version = "0.13.4", default-features = false, features = [ "socks", "system-proxy", ] } -hickory-resolver = "0.26.1" +hickory-resolver = "0.26.2" # Favicon extraction libraries html5gum = "0.8.4" @@ -215,7 +214,7 @@ bytes = "1.12.1" svg-hush = "0.9.7" # Cache function results (Used for version check and favicon fetching) -cached = { version = "3.1.1", features = ["async"] } +cached = { version = "4.0.0", features = ["async"] } # Used for custom short lived cookie jar during favicon extraction cookie = "0.18.2" @@ -232,7 +231,7 @@ pastey = "0.2.3" governor = "0.10.4" # CIDR parsing for the trusted proxies of the client IP header -ipnet = "2.12.1" +ipnet = "2.12.2" # OIDC for SSO openidconnect = { version = "4.0.1", default-features = false } @@ -257,17 +256,17 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.58.2", default-features = false, features = ["services-fs"] } +opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.11.0", optional = true, default-features = false, features = [ +aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", "sso", ] } aws-credential-types = { version = "1.3.0", optional = true } -aws-smithy-runtime-api = { version = "1.15.0", optional = true } +aws-smithy-runtime-api = { version = "1.16.0", optional = true } http = { version = "1.5.0", optional = true } reqsign-aws-v4 = { version = "3.3.0", optional = true } reqsign-core = { version = "3.3.1", optional = true } diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index fdbf40f2..6ae6e9a8 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -5,7 +5,8 @@ vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags xx_image_digest: "sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707" -rust_version: 1.98.0 # Rust version to be used +# The `rust_version` variable is extracted from `rust-toolchain.toml` +# rust_version: x.yy.z # Rust version to be used debian_version: trixie # Debian release name to be used alpine_version: "3.24" # Alpine version to be used # For which platforms/architectures will we try to build images diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 491aa9e0..a91deb07 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -32,10 +32,10 @@ FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330 ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 ## And for Alpine we define all build images here, they will only be loaded when actually used -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.98.0 AS build_amd64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.98.0 AS build_arm64 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.98.0 AS build_armv7 -FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.98.0 AS build_armv6 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.98.1 AS build_amd64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.98.1 AS build_arm64 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.98.1 AS build_armv7 +FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.98.1 AS build_armv6 ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 280559e2..b8490609 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 -FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.98.0-slim-trixie AS build +FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.98.1-slim-trixie AS build # hadolint ignore=DL3067 COPY --from=xx / / ARG TARGETARCH diff --git a/docker/render_template b/docker/render_template index 401e0ad0..84ca8ead 100755 --- a/docker/render_template +++ b/docker/render_template @@ -3,17 +3,23 @@ import os import argparse import json +import tomllib import yaml import jinja2 # Load settings file -with open("DockerSettings.yaml", 'r') as yaml_file: +with open('DockerSettings.yaml', 'r', encoding='utf-8') as yaml_file: yaml_data = yaml.safe_load(yaml_file) +# Extract the rust_version from the rust-toolchain.toml file +script_dir = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(script_dir, '..', 'rust-toolchain.toml'), 'rb') as toolchain_file: + yaml_data["rust_version"] = tomllib.load(toolchain_file)["toolchain"]["channel"] + settings_env = jinja2.Environment( loader=jinja2.FileSystemLoader(os.getcwd()), ) -settings_yaml = yaml.safe_load(settings_env.get_template("DockerSettings.yaml").render(yaml_data)) +settings_yaml = yaml.safe_load(settings_env.get_template('DockerSettings.yaml').render(yaml_data)) args_parser = argparse.ArgumentParser() args_parser.add_argument('template_file', help='Jinja2 template file to render.') diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 84d17192..34cb913d 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] quote = "1.0.47" -syn = "3.0.4" +syn = "3.0.5" [lints] workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9bfb1d94..2be20926 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.98.0" +channel = "1.98.1" components = [ "rustfmt", "clippy" ] profile = "minimal" From 5b51b60f9407bc4e088eb1dcb035a5d395178aab Mon Sep 17 00:00:00 2001 From: Chase Douglas Date: Wed, 9 Sep 2026 05:30:25 -0700 Subject: [PATCH 23/34] Route service clients through shared HTTP setup (#7639) * storage: route OpenDAL through HTTP client OpenDAL 0.58 requires applications to provide an HTTP transport. Its default installer creates a standalone client, bypassing Vaultwarden DNS, redirect, proxy, timeout, and request configuration. Build the client through the internal HTTP interface and inject it into OpenDAL's public reqwest transport. * http: honor block setting on redirects Clients can disable host blocking for administrator-configured private services. DNS resolution honors this setting, but the redirect policy still performs config-backed host checks. Capture the setting in the redirect policy and skip those checks when blocking is disabled. This also avoids re-entering CONFIG when a remote configuration request is redirected during startup. * http: make DNS setup bootstrap-safe Remote configuration can require an HTTP client while CONFIG is still initializing. Building the DNS resolver currently reads CONFIG.dns_prefer_ipv6(), so loading an S3-backed config can deadlock. Build one resolver without consulting CONFIG. Order addresses for each lookup using the merged setting when available, falling back to the environment and then IPv4-first during bootstrap. * aws: use internal HTTP client The AWS SDK connector builds a raw reqwest client, bypassing Vaultwarden TLS, DNS, redirect, proxy, timeout, and request setup. Construct it through the internal HTTP client interface and retain the standard ten-second request deadline. Permit private AWS metadata and service endpoints by disabling non-global IP blocking. Preserve timeout errors when adapting reqwest failures to the AWS SDK so the runtime receives the correct connector error category. * Added comment for the prefer IPv function Signed-off-by: BlackDex --------- Signed-off-by: BlackDex Co-authored-by: BlackDex --- Cargo.lock | 15 ++++++++ Cargo.toml | 2 ++ src/http_client.rs | 87 ++++++++++++++++++++++++++++++++++++++-------- src/storage.rs | 19 ++++++---- 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c763d1..55a7233b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3520,6 +3520,20 @@ dependencies = [ "web-time", ] +[[package]] +name = "opendal-http-transport-reqwest" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401999057db611e592f883fcf2cbd6754ff37af587deaadd07b8c1398b2b6b06" +dependencies = [ + "bytes", + "futures", + "http 1.5.0", + "http-body 1.1.0", + "opendal-core", + "reqwest", +] + [[package]] name = "opendal-service-fs" version = "0.59.1" @@ -5989,6 +6003,7 @@ dependencies = [ "num-derive", "num-traits", "opendal", + "opendal-http-transport-reqwest", "openidconnect", "openssl", "pastey 0.2.3", diff --git a/Cargo.toml b/Cargo.toml index cc6dff02..d3a3d5e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ vendored_openssl = ["openssl/vendored"] enable_mimalloc = ["dep:mimalloc"] s3 = [ "opendal/services-s3", + "dep:opendal-http-transport-reqwest", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", @@ -257,6 +258,7 @@ grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } +opendal-http-transport-reqwest = { version = "0.59.1", default-features = false, features = ["rustls-no-provider"], optional = true } # For retrieving AWS credentials, including temporary SSO credentials aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ diff --git a/src/http_client.rs b/src/http_client.rs index 0831d990..5ef293fc 100644 --- a/src/http_client.rs +++ b/src/http_client.rs @@ -14,7 +14,10 @@ use reqwest::{ }; use url::Host; -use crate::{CONFIG, util::is_global}; +use crate::{ + CONFIG, + util::{get_env_bool, is_global}, +}; pub fn make_http_request(method: reqwest::Method, url: &str) -> Result { static INSTANCE: LazyLock = @@ -36,7 +39,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { let mut headers = header::HeaderMap::new(); headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); - let redirect_policy = reqwest::redirect::Policy::custom(|attempt| { + let redirect_policy = reqwest::redirect::Policy::custom(move |attempt| { if attempt.previous().len() >= 5 { return attempt.error("Too many redirects"); } @@ -45,7 +48,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { return attempt.error("Invalid host"); }; - if let Err(e) = should_block_host(&host) { + if enforce_block && let Err(e) = should_block_host(&host) { return attempt.error(e); } @@ -59,6 +62,14 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder { .timeout(Duration::from_secs(10)) } +fn dns_prefer_ipv6() -> bool { + // CONFIG may require DNS to initialize, so avoid forcing it during bootstrap. + match LazyLock::get(&CONFIG) { + Some(config) => config.dns_prefer_ipv6(), + None => get_env_bool("DNS_PREFER_IPV6").unwrap_or(false), + } +} + fn should_block_ip(ip: IpAddr) -> bool { if !CONFIG.http_request_block_non_global_ips() { return false; @@ -258,12 +269,8 @@ impl CustomDnsResolver { fn new() -> Arc { TokioResolver::builder(TokioRuntimeProvider::default()) .and_then(|mut builder| { - // Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first - // This might cause issues on IPv4 only systems or containers - // Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default - if !CONFIG.dns_prefer_ipv6() { - builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6; - } + // Query both families; the preferred order is applied per lookup below. + builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6; builder.build() }) .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) @@ -289,6 +296,17 @@ impl CustomDnsResolver { } } +fn sort_addresses(addresses: &mut [SocketAddr], prefer_ipv6: bool) { + // `sort_by_key` orders `false` before `true`. + // When IPv6 is preferred, IPv6 addresses return `false` for `is_ipv4()` and sort first. + // When IPv4 is preferred, IPv4 addresses return `false` for `is_ipv6()` and sort first. + if prefer_ipv6 { + addresses.sort_by_key(SocketAddr::is_ipv4); + } else { + addresses.sort_by_key(SocketAddr::is_ipv6); + } +} + fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> { let Ok(host) = get_valid_host(name) else { return Err(CustomHttpClientError::Invalid { @@ -320,7 +338,9 @@ impl Resolve for CustomDns { let this = Arc::clone(&self.resolver); Box::pin(async move { let name = name.as_str(); - let results = this.resolve_domain(name, enforce_block).await?; + let mut results = this.resolve_domain(name, enforce_block).await?; + // Recheck after bootstrap so long-lived clients adopt the loaded config. + sort_addresses(&mut results, dns_prefer_ipv6()); if results.is_empty() { warn!("Unable to resolve {name} to any valid IP address"); } @@ -339,10 +359,29 @@ pub(crate) mod aws { }; use reqwest::Client; + use super::get_reqwest_client_builder; + // Adapter that wraps reqwest to be compatible with the AWS SDK #[derive(Debug)] pub(crate) struct AwsReqwestConnector { - pub(crate) client: Client, + client: Client, + } + + impl AwsReqwestConnector { + pub(crate) fn new() -> Self { + let client = get_reqwest_client_builder(false).build().expect("Failed to build AWS HTTP client"); + Self { + client, + } + } + } + + fn connector_error(error: reqwest::Error) -> ConnectorError { + if error.is_timeout() { + ConnectorError::timeout(Box::new(error)) + } else { + ConnectorError::io(Box::new(error)) + } } impl HttpConnector for AwsReqwestConnector { @@ -362,10 +401,10 @@ pub(crate) mod aws { req_builder = req_builder.body(body_bytes.to_vec()); } - let response = req_builder.send().await.map_err(|e| ConnectorError::io(Box::new(e)))?; + let response = req_builder.send().await.map_err(connector_error)?; let status = response.status().into(); - let bytes = response.bytes().await.map_err(|e| ConnectorError::io(Box::new(e)))?; + let bytes = response.bytes().await.map_err(connector_error)?; Ok(HttpResponse::new(status, bytes.into())) }; @@ -391,7 +430,7 @@ pub(crate) mod aws { mod tests { use super::*; use crate::util::is_global_hardcoded; - use std::net::Ipv4Addr; + use std::net::{Ipv4Addr, Ipv6Addr}; use url::Host; // === @@ -404,6 +443,26 @@ mod tests { } } + #[test] + fn dns_setup_does_not_initialize_config() { + assert!(LazyLock::get(&CONFIG).is_none()); + drop(CustomDns::instance(false)); + assert!(LazyLock::get(&CONFIG).is_none()); + } + + #[test] + fn dns_preference_orders_addresses() { + let ipv4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0); + let mut addresses = [ipv6, ipv4]; + + sort_addresses(&mut addresses, false); + assert_eq!(addresses, [ipv4, ipv6]); + + sort_addresses(&mut addresses, true); + assert_eq!(addresses, [ipv6, ipv4]); + } + #[test] fn dotted_decimal_loopback_normalizes() { let ip = parse_to_ip("127.0.0.1").unwrap(); diff --git a/src/storage.rs b/src/storage.rs index 689be302..32562a0d 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -77,10 +77,18 @@ pub(crate) fn operator_for_path(path: &str) -> Result = LazyLock::new(|| { + // Storage endpoints are administrator-configured and may be private. + crate::http_client::get_reqwest_client_builder(false).build().expect("Failed to build OpenDAL HTTP client") + }); + pub(super) fn is_uri(path: &str) -> bool { path.starts_with("s3://") } @@ -177,12 +185,7 @@ mod s3 { let chain = DEFAULT_CREDENTIAL_CHAIN .get_or_init(|| { - let reqwest_client = reqwest::Client::builder().build().unwrap(); - let connector = AwsReqwestConnector { - client: reqwest_client, - }; - - let conf = ProviderConfig::default().with_http_client(connector); + let conf = ProviderConfig::default().with_http_client(AwsReqwestConnector::new()); DefaultCredentialsChain::builder().configure(conf).build() }) @@ -236,7 +239,9 @@ mod s3 { builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider)); } - Ok(opendal::Operator::new(builder)?) + let http_transport = opendal::HttpTransporter::new(ReqwestTransport::new(HTTP_CLIENT.clone())); + let context = opendal::OperationContext::new().with_http_transport(http_transport); + Ok(opendal::Operator::new(builder)?.with_context(context)) } fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool { From e992cbb4f520c53d50b22fa3cc1f2f77c4a95a81 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:31:03 +0200 Subject: [PATCH 24/34] Fix iOS registration token response (#7714) * Return registration token as text/plain for Accept: */* * fix register verification response content negotiation --- src/api/identity.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 7bd12a78..6808ddde 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -3,7 +3,7 @@ use num_traits::FromPrimitive; use rocket::{ Route, form::{Form, FromForm}, - http::{Cookie, CookieJar, SameSite}, + http::{Accept, Cookie, CookieJar, MediaType, SameSite}, response::Redirect, serde::json::Json, }; @@ -1083,11 +1083,18 @@ enum RegisterVerificationResponse { #[response(status = 204)] NoContent(()), Token(Json), + PlainToken(String), +} + +// Return JSON only when the client explicitly requests it, otherwise return plain text. +fn accepts_json(accept: Option<&Accept>) -> bool { + accept.is_some_and(|accept| accept.preferred().media_type() == &MediaType::JSON) } #[post("/accounts/register/send-verification-email", data = "")] async fn register_verification_email( data: Json, + accept: Option<&Accept>, ip: ClientIp, conn: DbConn, ) -> ApiResult { @@ -1125,7 +1132,11 @@ async fn register_verification_email( } else { // If email verification is not required, return the token directly // the clients will use this token to finish the registration - Ok(RegisterVerificationResponse::Token(Json(token))) + Ok(if accepts_json(accept) { + RegisterVerificationResponse::Token(Json(token)) + } else { + RegisterVerificationResponse::PlainToken(token) + }) } } From 25dfedafd73cdeee47a50c1dfa7a451afe1ae1bd Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 9 Sep 2026 14:43:47 +0000 Subject: [PATCH 25/34] Use insert_into when possible (#6437) Co-authored-by: Timshel --- src/api/core/accounts.rs | 2 +- src/db/models/archive.rs | 9 ++- src/db/models/attachment.rs | 22 ++---- src/db/models/auth_request.rs | 30 +++---- src/db/models/cipher.rs | 22 ++---- src/db/models/collection.rs | 92 +++++++--------------- src/db/models/device.rs | 9 ++- src/db/models/emergency_access.rs | 22 ++---- src/db/models/event.rs | 27 ++++--- src/db/models/folder.rs | 33 +++----- src/db/models/group.rs | 127 +++++++++--------------------- src/db/models/organization.rs | 68 +++++----------- src/db/models/send.rs | 22 ++---- src/db/models/user.rs | 18 +++-- 14 files changed, 179 insertions(+), 324 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 3ea6eada..69be1334 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1611,7 +1611,7 @@ async fn post_auth_request( _ => err!("AuthRequest doesn't exist", "Device verification failed"), }; - let mut auth_request = AuthRequest::new( + let auth_request = AuthRequest::new( user.uuid.clone(), data.device_identifier.clone(), client_headers.device_type, diff --git a/src/db/models/archive.rs b/src/db/models/archive.rs index 83d547f2..2330fac4 100644 --- a/src/db/models/archive.rs +++ b/src/db/models/archive.rs @@ -41,17 +41,20 @@ impl Archive { ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; db_run! { conn: - sqlite, mysql { - diesel::replace_into(archives::table) + mysql { + diesel::insert_into(archives::table) .values(( archives::user_uuid.eq(user_uuid), archives::cipher_uuid.eq(cipher_uuid), archives::archived_at.eq(archived_at), )) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(archives::archived_at.eq(archived_at)) .execute(conn) .map_res("Error saving archive") } - postgresql { + postgresql, sqlite { diesel::insert_into(archives::table) .values(( archives::user_uuid.eq(user_uuid), diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 244f8c27..0536dde5 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -82,24 +82,16 @@ impl Attachment { impl Attachment { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(attachments::table) + mysql { + diesel::insert_into(attachments::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(attachments::table) - .filter(attachments::id.eq(&self.id)) - .set(self) - .execute(conn) - .map_res("Error saving attachment") - } - Err(e) => Err(e.into()), - }.map_res("Error saving attachment") + .map_res("Error saving attachment") } - postgresql { + postgresql, sqlite { diesel::insert_into(attachments::table) .values(self) .on_conflict(attachments::id) diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs index a3876661..cc4b60fd 100644 --- a/src/db/models/auth_request.rs +++ b/src/db/models/auth_request.rs @@ -82,31 +82,23 @@ impl AuthRequest { } impl AuthRequest { - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { + pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(auth_requests::table) - .values(&*self) + mysql { + diesel::insert_into(auth_requests::table) + .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(auth_requests::table) - .filter(auth_requests::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error auth_request") - } - Err(e) => Err(e.into()), - }.map_res("Error auth_request") + .map_res("Error saving auth_request") } - postgresql { + postgresql, sqlite { diesel::insert_into(auth_requests::table) - .values(&*self) + .values(self) .on_conflict(auth_requests::uuid) .do_update() - .set(&*self) + .set(self) .execute(conn) .map_res("Error saving auth_request") } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index eed5041d..721d9790 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -440,24 +440,16 @@ impl Cipher { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(ciphers::table) + mysql { + diesel::insert_into(ciphers::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(ciphers::table) - .filter(ciphers::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving cipher") - } - Err(e) => Err(e.into()), - }.map_res("Error saving cipher") + .map_res("Error saving cipher") } - postgresql { + postgresql, sqlite { diesel::insert_into(ciphers::table) .values(&*self) .on_conflict(ciphers::uuid) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 8aec90ea..be108f13 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -168,24 +168,16 @@ impl Collection { self.update_users_revision(conn).await; db_run! { conn: - sqlite, mysql { - match diesel::replace_into(collections::table) + mysql { + diesel::insert_into(collections::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(collections::table) - .filter(collections::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving collection") - } - Err(e) => Err(e.into()), - }.map_res("Error saving collection") + .map_res("Error saving collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(collections::table) .values(self) .on_conflict(collections::uuid) @@ -728,53 +720,30 @@ impl CollectionUser { ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; + let values = ( + users_collections::user_uuid.eq(user_uuid), + users_collections::collection_uuid.eq(collection_uuid), + users_collections::read_only.eq(read_only), + users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + mysql { + diesel::insert_into(users_collections::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(values) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(users_collections::table) - .filter(users_collections::user_uuid.eq(user_uuid)) - .filter(users_collections::collection_uuid.eq(collection_uuid)) - .set(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) - .execute(conn) - .map_res("Error adding user to collection") - } - Err(e) => Err(e.into()), - }.map_res("Error adding user to collection") + .map_res("Error adding user to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + .values(values) .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) .do_update() - .set(( - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - users_collections::manage.eq(manage), - )) + .set(values) .execute(conn) .map_res("Error adding user to collection") } @@ -909,19 +878,18 @@ impl CollectionCipher { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here. - // Table ciphers_collections does not have ForeignKey Constraints which would cause conflicts. - // This table has no constraints pointing to itself, but only to others. - diesel::replace_into(ciphers_collections::table) + mysql { + diesel::insert_into(ciphers_collections::table) .values(( ciphers_collections::cipher_uuid.eq(cipher_uuid), ciphers_collections::collection_uuid.eq(collection_uuid), )) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error adding cipher to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(ciphers_collections::table) .values(( ciphers_collections::cipher_uuid.eq(cipher_uuid), diff --git a/src/db/models/device.rs b/src/db/models/device.rs index cc8f1cec..5e5f1f97 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -146,15 +146,18 @@ impl Device { } db_run! { conn: - sqlite, mysql { + mysql { crate::util::retry(|| - diesel::replace_into(devices::table) + diesel::insert_into(devices::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn), 10, ).map_res("Error saving device") } - postgresql { + postgresql, sqlite { crate::util::retry(|| diesel::insert_into(devices::table) .values(&*self) diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index 45fad91f..09783061 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -146,24 +146,16 @@ impl EmergencyAccess { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(emergency_access::table) + mysql { + diesel::insert_into(emergency_access::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(emergency_access::table) - .filter(emergency_access::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error updating emergency access") - } - Err(e) => Err(e.into()), - }.map_res("Error saving emergency access") + .map_res("Error saving emergency access") } - postgresql { + postgresql, sqlite { diesel::insert_into(emergency_access::table) .values(&*self) .on_conflict(emergency_access::uuid) diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 1f307979..2d9ed8b2 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -208,20 +208,23 @@ impl Event { /// Basic Queries pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - diesel::replace_into(event::table) - .values(self) - .execute(conn) - .map_res("Error saving event") + mysql { + diesel::insert_into(event::table) + .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) + .execute(conn) + .map_res("Error saving event") } - postgresql { + postgresql, sqlite { diesel::insert_into(event::table) - .values(self) - .on_conflict(event::uuid) - .do_update() - .set(self) - .execute(conn) - .map_res("Error saving event") + .values(self) + .on_conflict(event::uuid) + .do_update() + .set(self) + .execute(conn) + .map_res("Error saving event") } } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 745608e3..adbe993f 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -77,24 +77,16 @@ impl Folder { self.updated_at = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(folders::table) + mysql { + diesel::insert_into(folders::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(folders::table) - .filter(folders::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving folder") - } - Err(e) => Err(e.into()), - }.map_res("Error saving folder") + .map_res("Error saving folder") } - postgresql { + postgresql, sqlite { diesel::insert_into(folders::table) .values(&*self) .on_conflict(folders::uuid) @@ -147,16 +139,15 @@ impl Folder { impl FolderCipher { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here. - // Table folders_ciphers does not have ForeignKey Constraints which would cause conflicts. - // This table has no constraints pointing to itself, but only to others. - diesel::replace_into(folders_ciphers::table) + mysql { + diesel::insert_into(folders_ciphers::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error adding cipher to folder") } - postgresql { + postgresql, sqlite { diesel::insert_into(folders_ciphers::table) .values(self) .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid)) diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 37037de6..32e9333f 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -166,24 +166,16 @@ impl Group { self.revision_date = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(groups::table) + mysql { + diesel::insert_into(groups::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(groups::table) - .filter(groups::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving group") - } - Err(e) => Err(e.into()), - }.map_res("Error saving group") + .map_res("Error saving group") } - postgresql { + postgresql, sqlite { diesel::insert_into(groups::table) .values(&*self) .on_conflict(groups::uuid) @@ -326,53 +318,30 @@ impl CollectionGroup { group_user.update_user_revision(conn).await; } + let values = ( + collections_groups::collections_uuid.eq(&self.collections_uuid), + collections_groups::groups_uuid.eq(&self.groups_uuid), + collections_groups::read_only.eq(&self.read_only), + collections_groups::hide_passwords.eq(&self.hide_passwords), + collections_groups::manage.eq(&self.manage), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(collections_groups::table) - .values(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(&self.read_only), - collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), - )) + mysql { + diesel::insert_into(collections_groups::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(values) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(collections_groups::table) - .filter(collections_groups::collections_uuid.eq(&self.collections_uuid)) - .filter(collections_groups::groups_uuid.eq(&self.groups_uuid)) - .set(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(&self.read_only), - collections_groups::hide_passwords.eq(&self.hide_passwords), - collections_groups::manage.eq(&self.manage), - )) - .execute(conn) - .map_res("Error adding group to collection") - } - Err(e) => Err(e.into()), - }.map_res("Error adding group to collection") + .map_res("Error adding group to collection") } - postgresql { + postgresql, sqlite { diesel::insert_into(collections_groups::table) - .values(( - collections_groups::collections_uuid.eq(&self.collections_uuid), - collections_groups::groups_uuid.eq(&self.groups_uuid), - collections_groups::read_only.eq(self.read_only), - collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), - )) + .values(values) .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid)) .do_update() - .set(( - collections_groups::read_only.eq(self.read_only), - collections_groups::hide_passwords.eq(self.hide_passwords), - collections_groups::manage.eq(self.manage), - )) + .set(values) .execute(conn) .map_res("Error adding group to collection") } @@ -497,43 +466,25 @@ impl GroupUser { pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { self.update_user_revision(conn).await; + let values = ( + groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), + groups_users::groups_uuid.eq(&self.groups_uuid), + ); + db_run! { conn: - sqlite, mysql { - match diesel::replace_into(groups_users::table) - .values(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + mysql { + diesel::insert_into(groups_users::table) + .values(values) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(groups_users::table) - .filter(groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid)) - .filter(groups_users::groups_uuid.eq(&self.groups_uuid)) - .set(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) - .execute(conn) - .map_res("Error adding user to group") - } - Err(e) => Err(e.into()), - }.map_res("Error adding user to group") + .map_res("Error adding user to group") } - postgresql { + postgresql, sqlite { diesel::insert_into(groups_users::table) - .values(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + .values(values) .on_conflict((groups_users::users_organizations_uuid, groups_users::groups_uuid)) - .do_update() - .set(( - groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid), - groups_users::groups_uuid.eq(&self.groups_uuid), - )) + .do_nothing() .execute(conn) .map_res("Error adding user to group") } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index bdb69864..29016865 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -353,25 +353,16 @@ impl Organization { } db_run! { conn: - sqlite, mysql { - match diesel::replace_into(organizations::table) + mysql { + diesel::insert_into(organizations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(organizations::table) - .filter(organizations::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving organization") - } - Err(e) => Err(e.into()), - }.map_res("Error saving organization") - + .map_res("Error saving organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(organizations::table) .values(self) .on_conflict(organizations::uuid) @@ -753,24 +744,16 @@ impl Membership { User::update_uuid_revision(&self.user_uuid, conn).await; db_run! { conn: - sqlite, mysql { - match diesel::replace_into(users_organizations::table) + mysql { + diesel::insert_into(users_organizations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(users_organizations::table) - .filter(users_organizations::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error adding user to organization") - }, - Err(e) => Err(e.into()), - }.map_res("Error adding user to organization") + .map_res("Error adding user to organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(users_organizations::table) .values(self) .on_conflict(users_organizations::uuid) @@ -1186,25 +1169,16 @@ impl Membership { impl OrganizationApiKey { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - match diesel::replace_into(organization_api_key::table) + mysql { + diesel::insert_into(organization_api_key::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(organization_api_key::table) - .filter(organization_api_key::uuid.eq(&self.uuid)) - .set(self) - .execute(conn) - .map_res("Error saving organization") - } - Err(e) => Err(e.into()), - }.map_res("Error saving organization") - + .map_res("Error saving organization") } - postgresql { + postgresql, sqlite { diesel::insert_into(organization_api_key::table) .values(self) .on_conflict((organization_api_key::uuid, organization_api_key::org_uuid)) diff --git a/src/db/models/send.rs b/src/db/models/send.rs index c5bc98c4..d7de7749 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -202,24 +202,16 @@ impl Send { self.revision_date = Utc::now().naive_utc(); db_run! { conn: - sqlite, mysql { - match diesel::replace_into(sends::table) + mysql { + diesel::insert_into(sends::table) .values(&*self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_update() + .set(&*self) .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(sends::table) - .filter(sends::uuid.eq(&self.uuid)) - .set(&*self) - .execute(conn) - .map_res("Error saving send") - } - Err(e) => Err(e.into()), - }.map_res("Error saving send") + .map_res("Error saving send") } - postgresql { + postgresql, sqlite { diesel::insert_into(sends::table) .values(&*self) .on_conflict(sends::uuid) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 93d750d5..81cb8d84 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -463,15 +463,17 @@ impl Invitation { } db_run! { conn: - sqlite, mysql { - // Not checking for ForeignKey Constraints here - // Table invitations does not have any ForeignKey Constraints. - diesel::replace_into(invitations::table) + // Not checking for ForeignKey Constraints here + // Table invitations does not have any ForeignKey Constraints. + mysql { + diesel::insert_into(invitations::table) .values(self) + .on_conflict(diesel::dsl::DuplicatedKeys) + .do_nothing() .execute(conn) .map_res("Error saving invitation") } - postgresql { + postgresql, sqlite { diesel::insert_into(invitations::table) .values(self) .on_conflict(invitations::email) @@ -528,13 +530,13 @@ pub struct UserId(String); impl SsoUser { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: - sqlite, mysql { - diesel::replace_into(sso_users::table) + mysql { + diesel::insert_into(sso_users::table) .values(self) .execute(conn) .map_res("Error saving SSO user") } - postgresql { + postgresql, sqlite { diesel::insert_into(sso_users::table) .values(self) .execute(conn) From eb212e23fad88e6136723f43e5b73543fa7026d3 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 9 Sep 2026 18:24:33 +0200 Subject: [PATCH 26/34] Fix archiveDate update (#7722) When `archiveDate` is set to `null` it should unarchive it for that specific user, which is what Bitwarden does. This should fix this by checking and validating if it is `null` Fixes #7581 Signed-off-by: BlackDex --- src/api/core/ciphers.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 13021ca3..50be6732 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -537,11 +537,12 @@ pub async fn update_cipher_from_data( cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; - if let Some(dt_str) = data.archived_date { - match NaiveDateTime::parse_from_str(&dt_str, "%+") { + match data.archived_date { + Some(dt_str) => match NaiveDateTime::parse_from_str(&dt_str, "%+") { Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?, Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"), - } + }, + None => cipher.unarchive(&headers.user.uuid, conn).await?, } if ut != UpdateType::None { From fddc8e71be5904333acd135734cd1e8f4d8f9c20 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:52:49 +0200 Subject: [PATCH 27/34] Support the vault banner policy (#7748) --- src/api/core/events.rs | 20 ++++++++++++++++++++ src/db/models/event.rs | 2 +- src/db/models/org_policy.rs | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 2c437a36..a5b5b6b1 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -199,6 +199,26 @@ async fn post_events_collect(data: Json>, headers: Headers, .await; } } + // Only the vault notification banner click is accepted from clients. The rest of + // the 1500..=1599 range is written server-side and must not be forgeable by a client. + t if t == EventType::OrganizationUserNotificationBannerActionClicked as i32 => { + if let Some(org_id) = &event.organization_id + && let Some(membership) = + Membership::find_confirmed_by_user_and_org(&headers.user.uuid, org_id, &conn).await + { + log_event_impl( + event.r#type, + &membership.uuid, + org_id, + &headers.user.uuid, + headers.device.atype, + Some(event_date), + &headers.ip.ip, + &conn, + ) + .await; + } + } _ => { // The cipher determines the organization the event is logged to, so make sure the // user can actually access it instead of trusting the provided cipher uuid. diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 2d9ed8b2..cc0eb504 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -113,7 +113,7 @@ pub enum EventType { OrganizationUserAdminResetTwoFactor = 1519, // OrganizationUserRevoked_TwoFactorNonCompliance = 1520, // OrganizationUserRevoked_SingleOrganizationNonCompliance = 1521, - // OrganizationUserNotificationBannerActionClicked = 1522, + OrganizationUserNotificationBannerActionClicked = 1522, // Organization OrganizationUpdated = 1600, diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 2b45cd86..bf927d5f 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -49,6 +49,7 @@ pub enum OrgPolicyType { // AutotypeDefaultSetting = 17, // Not supported yet // AutoConfirm = 18, // Not supported (not implemented yet) // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed) + OrganizationUserNotification = 20, } // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 From 8719f86fcf7a58a145eb89ae10987ac11f3135b1 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:52:57 +0200 Subject: [PATCH 28/34] Add basic auth response client feature flag (#7745) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index d22145b8..73226055 100644 --- a/.env.template +++ b/.env.template @@ -406,6 +406,7 @@ ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-4": Special feature flag for desktop UI (Desktop >= 2026.2.0) +## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0) # EXPERIMENTAL_CLIENT_FEATURE_FLAGS= ## Require new device emails. When a user logs in an email is required to be sent. diff --git a/src/config.rs b/src/config.rs index 37fc3e85..18cc6504 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1427,6 +1427,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "pm-5594-safari-account-switching", "pm-32413-multi-client-password-management", // Autofill Team + "enable-basic-auth-response", "ssh-agent", "ssh-agent-v2", // Key Management Team From 9c8aa2359ff3e38f5b179b68bba34baeb9d72778 Mon Sep 17 00:00:00 2001 From: Timshel Date: Fri, 18 Sep 2026 14:53:06 +0000 Subject: [PATCH 29/34] User key id (#7693) Co-authored-by: Timshel --- .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../mysql/2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + .../2026-09-02-120000_add_key_id/down.sql | 0 .../2026-09-02-120000_add_key_id/up.sql | 1 + src/api/core/accounts.rs | 23 +++++++++++++++-- src/api/core/ciphers.rs | 25 +++++++++++++++++-- src/db/models/mod.rs | 2 +- src/db/models/user.rs | 24 ++++++++++++++++++ src/db/schema.rs | 1 + 13 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/mysql/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/mysql/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/postgresql/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/postgresql/2026-09-02-120000_add_key_id/up.sql create mode 100644 migrations/sqlite/2026-09-02-120000_add_key_id/down.sql create mode 100644 migrations/sqlite/2026-09-02-120000_add_key_id/up.sql diff --git a/migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql b/migrations/cockroachdb/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql b/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/cockroachdb/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/mysql/2026-09-02-120000_add_key_id/down.sql b/migrations/mysql/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2026-09-02-120000_add_key_id/up.sql b/migrations/mysql/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/mysql/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/postgresql/2026-09-02-120000_add_key_id/down.sql b/migrations/postgresql/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql b/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/postgresql/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/migrations/sqlite/2026-09-02-120000_add_key_id/down.sql b/migrations/sqlite/2026-09-02-120000_add_key_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql b/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql new file mode 100644 index 00000000..d25396cf --- /dev/null +++ b/migrations/sqlite/2026-09-02-120000_add_key_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN key_id TEXT; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 69be1334..8cc5e55b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -21,8 +21,9 @@ use crate::{ DbConn, DbPool, models::{ AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest, - EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, - OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, + EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, KeyId, Membership, + MembershipId, OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, + UserKdfType, }, }, mail, @@ -46,6 +47,7 @@ pub fn routes() -> Vec { post_set_password, post_kdf, post_rotatekey, + post_user_key, post_sstamp, post_email_token, post_email, @@ -1025,6 +1027,23 @@ async fn post_rotatekey(data: Json, headers: Headers, conn: DbConn, nt: save_result } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct KeyIdData { + user_key_id: KeyId, +} + +#[post("/accounts/key-management/user-key-id", data = "")] +async fn post_user_key(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + let mut user = headers.user; + if user.key_id.is_some() { + err_code!("Unexpected data", Status::UnprocessableEntity.code); + } + + user.key_id = Some(data.into_inner().user_key_id); + user.save(&conn).await +} + #[post("/accounts/security-stamp", data = "")] async fn post_sstamp(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { let data: PasswordOrOtpData = data.into_inner(); diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 50be6732..d8081cba 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -6,6 +6,7 @@ use rocket::{ Route, form::{Form, FromForm}, fs::TempFile, + http::Status, serde::json::Json, }; use serde_json::Value; @@ -21,8 +22,8 @@ use crate::{ DbConn, DbPool, models::{ Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, - CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, - MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, + CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, KeyId, + Membership, MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, }, }, util::{NumberOrString, deser_opt_nonempty_str, save_temp_file}, @@ -198,6 +199,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option, + pub encrypted_for: UserId, // Added in web-v2025.6.0 + // Added in web-v2025.8.1, Optional for compat + pub encrypted_by_key_id: Option, + /* Login = 1, SecureNote = 2, @@ -333,6 +339,10 @@ async fn post_ciphers_create( ) -> JsonResult { let mut data: ShareCipherData = data.into_inner(); + if data.cipher.encrypted_for != headers.user.uuid { + err_code!("Invalid user cipher", Status::UnprocessableEntity.code); + } + // This check is usually only needed in update_cipher_from_data(), but we // need it here as well to avoid creating an empty cipher in the call to // cipher.save() below. @@ -362,6 +372,17 @@ async fn post_ciphers_create( async fn post_ciphers(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { let mut data: CipherData = data.into_inner(); + if data.encrypted_for != headers.user.uuid { + err_code!("Invalid user cipher", Status::UnprocessableEntity.code); + } + + if let Some(cipher_key_id) = &data.encrypted_by_key_id + && let Some(user_key_id) = &headers.user.key_id + && cipher_key_id != user_key_id + { + err_code!("Invalid key cipher", Status::UnprocessableEntity.code); + } + // The web/browser clients set this field to null as expected, but the // mobile clients seem to set the invalid value `0001-01-01T00:00:00`, // which results in a warning message being logged. This field isn't diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 0ed8ef91..0e4073a5 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -39,4 +39,4 @@ pub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeResponseError, SsoAuth}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; -pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException}; +pub use self::user::{Invitation, KeyId, SsoUser, User, UserId, UserKdfType, UserStampException}; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 81cb8d84..36089d1a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -69,6 +69,8 @@ pub struct User { pub avatar_color: Option, pub external_id: Option, // Todo: Needs to be removed in the future, this is not used anymore. + + pub key_id: Option, } #[derive(Identifiable, Queryable, Insertable)] @@ -154,6 +156,8 @@ impl User { avatar_color: None, external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. + + key_id: None, } } @@ -527,6 +531,26 @@ impl Invitation { #[from(forward)] pub struct UserId(String); +#[derive( + Clone, + Debug, + DieselNewType, + FromForm, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + AsRef, + Deref, + Display, + From, + UuidFromParam, +)] +#[deref(forward)] +#[from(forward)] +pub struct KeyId(String); + impl SsoUser { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: diff --git a/src/db/schema.rs b/src/db/schema.rs index af342186..98b1eda6 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -217,6 +217,7 @@ table! { api_key -> Nullable, avatar_color -> Nullable, external_id -> Nullable, + key_id -> Nullable, } } From c3f5477525aed713ed340358d3ca982309979032 Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:53:17 +0200 Subject: [PATCH 30/34] Add accepted organization data to sync response (#7666) --- src/api/core/ciphers.rs | 7 +++++++ src/db/models/org_policy.rs | 20 ++++++++++++++++++++ src/db/models/organization.rs | 15 +++++++++++++++ src/db/models/user.rs | 6 ++++++ 4 files changed, 48 insertions(+) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d8081cba..af79a9ca 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -162,6 +162,12 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect(); + let policies_new_json: Vec = OrgPolicy::find_accepted_and_confirmed_by_user(&headers.user.uuid, &conn) + .await + .iter() + .map(OrgPolicy::to_json) + .collect(); + let domains_json = if data.exclude_domains { Value::Null } else { @@ -194,6 +200,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option Vec { + conn.run(move |conn| { + org_policies::table + .inner_join( + users_organizations::table.on(users_organizations::org_uuid + .eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))), + ) + .filter( + users_organizations::status + .eq(MembershipStatus::Accepted as i32) + .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), + ) + .select(org_policies::all_columns) + .load::(conn) + .expect("Error loading org_policy") + }) + .await + } + pub async fn find_by_org_and_type( org_uuid: &OrganizationId, policy_type: OrgPolicyType, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 29016865..d615a3fc 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -844,6 +844,21 @@ impl Membership { .await } + pub async fn find_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { + conn.run(move |conn| { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter( + users_organizations::status + .eq(MembershipStatus::Accepted as i32) + .or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)), + ) + .load::(conn) + .unwrap_or_default() + }) + .await + } + pub async fn find_invited_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec { conn.run(move |conn| { users_organizations::table diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 36089d1a..3412b142 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -263,6 +263,11 @@ impl User { orgs_json.push(c.to_json(conn).await); } + let mut orgs_new_json = Vec::new(); + for c in Membership::find_accepted_and_confirmed_by_user(&self.uuid, conn).await { + orgs_new_json.push(c.to_json(conn).await); + } + let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).await.is_empty(); // TODO: Might want to save the status field in the DB @@ -303,6 +308,7 @@ impl User { "privateKey": self.private_key, "securityStamp": self.security_stamp, "organizations": orgs_json, + "organizationsNew": orgs_new_json, "providers": [], "providerOrganizations": [], "forcePasswordReset": false, From 64c56411f6a16ac4466c12199587101566cf7efa Mon Sep 17 00:00:00 2001 From: "Berk D. Demir" <11135+bdd@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:53:27 -0700 Subject: [PATCH 31/34] Add `pm-32009-new-item-types` feature flag (#7478) Introduced in 2026.4.0 for all clients, enables new item types bank account, driver's license, and passport. --- .env.template | 1 + src/api/core/ciphers.rs | 9 +++++++++ src/config.rs | 2 ++ 3 files changed, 12 insertions(+) diff --git a/.env.template b/.env.template index 73226055..8ed6c5c2 100644 --- a/.env.template +++ b/.env.template @@ -402,6 +402,7 @@ ## - "cxp-import-mobile": Enable the import via CXP on iOS (Clients >= 2025.9.2) ## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) ## - "pm-30529-webauthn-related-origins": +## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0) ## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index af79a9ca..a5b7e58b 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -279,6 +279,9 @@ pub struct CipherData { Card = 3, Identity = 4, SshKey = 5 + BankAccount = 6 + DriversLicense = 7 + Passport = 8 */ pub r#type: i32, pub name: String, @@ -291,6 +294,9 @@ pub struct CipherData { card: Option, identity: Option, ssh_key: Option, + bank_account: Option, + drivers_license: Option, + passport: Option, favorite: Option, reprompt: Option, @@ -538,6 +544,9 @@ pub async fn update_cipher_from_data( 3 => data.card, 4 => data.identity, 5 => data.ssh_key, + 6 => data.bank_account, + 7 => data.drivers_license, + 8 => data.passport, _ => err!("Invalid type"), }; diff --git a/src/config.rs b/src/config.rs index 18cc6504..0bb22f7f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1442,6 +1442,8 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "cxp-export-mobile", // Platform Team "pm-30529-webauthn-related-origins", + // Vault Team + "pm-32009-new-item-types", ]; impl Config { From a24683636a448949b0bf535fb2c88ec7f3814b2c Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 18 Sep 2026 18:44:50 +0200 Subject: [PATCH 32/34] Update Crates, GHA and JS (#7751) Signed-off-by: BlackDex --- .github/workflows/hadolint.yml | 2 +- .github/workflows/release.yml | 6 +- .github/workflows/trivy.yml | 2 +- .github/workflows/typos.yml | 2 +- .pre-commit-config.yaml | 2 +- Cargo.lock | 271 +- Cargo.toml | 14 +- macros/Cargo.toml | 2 +- src/static/scripts/datatables.css | 16 +- src/static/scripts/datatables.js | 9864 +++++++++++++++-------------- 10 files changed, 5101 insertions(+), 5080 deletions(-) diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index a429d10f..c4b83aa6 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -20,7 +20,7 @@ jobs: steps: # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 311891b7..903a5e22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,13 +58,13 @@ jobs: steps: - name: Initialize QEMU binfmt support - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 with: platforms: "arm64,arm" # Start Docker Buildx - name: Setup Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 # https://github.com/moby/buildkit/issues/3969 # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills with: @@ -185,7 +185,7 @@ jobs: - name: Bake ${{ matrix.base_image }} containers id: bake_vw - uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0 + uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 env: BASE_TAGS: "${{ steps.determine-version.outputs.BASE_TAGS }}" SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 7f41885d..f0fbf192 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 00fcbab4..3c880e91 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 + uses: crate-ci/typos@512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8319414..d2a509e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 + rev: 512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2 hooks: - id: typos always_run: true diff --git a/Cargo.lock b/Cargo.lock index 55a7233b..2ccd4b1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.45" +version = "0.4.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a8ec73eb862508b7041723c89386365894b4e7d9f6998bf1b8529e5b0ee254" +checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" dependencies = [ "compression-codecs", "compression-core", @@ -318,18 +318,14 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "asyncband" -version = "0.6.7" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0" -dependencies = [ - "hashbrown 0.17.1", - "slab", -] +checksum = "f2d85fd3d291fabcc40c7232c92c280ec1754fd7b5d7ea769f143222143e179a" [[package]] name = "atomic" @@ -403,9 +399,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.2" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef47857a1d4488b528f4a5d5715fa7c3300820897824152234d3fa22b1426657" +checksum = "25b43ad47adc2517efe3d706559d94b97e50e80e0321b3cadbc9f77cee88adcd" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -428,9 +424,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.109.0" +version = "1.111.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cfe74df5d9ad2fedd691973ad3521ebf4f27a3c68c792556686aedb5519bab" +checksum = "65700d75f6da78c89a61a49f1539979400a4609d3329376b1dc2133d7a1b2b90" dependencies = [ "arc-swap", "aws-credential-types", @@ -454,9 +450,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.111.0" +version = "1.113.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b0ec31ed6191bd11350aae4b2004198f2db21350cb0a20c57e0a92e55dd161" +checksum = "eb1147c254af7b6cee3ace90ae7da6f21366ce9a9b899035fa8a0fdf586eb75a" dependencies = [ "arc-swap", "aws-credential-types", @@ -480,9 +476,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.114.0" +version = "1.116.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef45745026107ec30c4ef86bd8ae4b002e7e5f6a86e4225240bdf6b06a0b944a" +checksum = "035c7392e751d20bf1060d625d023587eb14099646110a9cff7c697bfe4b253a" dependencies = [ "arc-swap", "aws-credential-types", @@ -507,9 +503,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +checksum = "31d955e76ff96acd555bf06fa0fa6d5bf9335fa84ae7c64481b20ae61d231f70" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -561,9 +557,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.63.0" +version = "0.63.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +checksum = "3385d469edbe8b60cc72002784652b5efca39178192aa9cc4b44c9875c6bdc18" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -581,9 +577,9 @@ dependencies = [ [[package]] name = "aws-smithy-query" -version = "0.62.0" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" +checksum = "f1d1d71f6562be974caa85442ecd90194c40fdb5df045f182a6c2e872ce95056" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -594,9 +590,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.14.0" +version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606" +checksum = "3296253d3a91b3f938a3f2bcce4daebadcb4aa4228153fcec90f9d23532b4484" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -619,9 +615,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.16.0" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223" +checksum = "6d881a7b7ad179fd6611680c9de89f716fb00ab40299a9a7b8c6913e8f7511a8" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -648,9 +644,9 @@ dependencies = [ [[package]] name = "aws-smithy-schema" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +checksum = "e8f395d93304280b64b7632fea798d177e74897fe7f063416ce627cd6fa24829" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -659,9 +655,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9" +checksum = "0b791f3ac597193fe1d08b82366986eb1f5bc31f2ac6c194c0855276116c76cd" dependencies = [ "base64-simd", "bytes", @@ -682,9 +678,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.62.0" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" +checksum = "0b932c8d6dc127fc980eecd78f8694ae9b9551b69a93a7def2a199c1c0033daf" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -785,9 +781,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blake2" @@ -930,9 +926,9 @@ checksum = "f5813789573ae815c8b4be58c4428e0e7ae05f0227678ba9de332ded585b9159" [[package]] name = "cc" -version = "1.4.5" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", "jobserver", @@ -942,9 +938,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -1011,9 +1007,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100590da849306918656ffbb22576bbdfc1382b50ad10d1d50ec177d3b205fb2" +checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" dependencies = [ "brotli", "compression-core", @@ -1170,9 +1166,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -1360,7 +1356,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1393,7 +1389,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1567,7 +1563,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -1581,7 +1577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b934ddbdcb2abb9f9fc9c30bd47bcc5618b615eea1d334cda5fdf8ff9b072a" dependencies = [ "bigdecimal", - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "chrono", "diesel_derives", @@ -1676,7 +1672,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1920,9 +1916,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "flate2" @@ -2040,7 +2036,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2311,9 +2307,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e7bd6a377435d568f652153e571b50970d7ccc1d1eeec0519f834632287e1" +checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" dependencies = [ "async-trait", "cfg-if", @@ -2335,9 +2331,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" dependencies = [ "data-encoding", "idna", @@ -2355,9 +2351,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4f9f4603319422d482738f3f6fe5aac03157fdbfed1cd85a3ff45adb09072f" +checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" dependencies = [ "cfg-if", "futures-util", @@ -2555,7 +2551,7 @@ dependencies = [ "http 1.5.0", "hyper 1.11.1", "hyper-util", - "rustls 0.23.44", + "rustls 0.23.45", "tokio", "tokio-rustls 0.26.5", "tower-service", @@ -2805,9 +2801,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ "defmt", "jiff-core", @@ -2824,18 +2820,19 @@ dependencies = [ [[package]] name = "jiff-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" dependencies = [ "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ "jiff-core", "proc-macro2", @@ -2941,9 +2938,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "11.0.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -3011,7 +3008,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-native-certs", "serde", "socket2 0.6.5", @@ -3109,7 +3106,7 @@ name = "macros" version = "0.1.0" dependencies = [ "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3267,7 +3264,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3299,7 +3296,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3379,7 +3376,7 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -3485,9 +3482,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950151f9587a51a7bed70a15fa0cff464eae96e41ae7499f97067bdafdf43eb" +checksum = "9fe43e16d96bed57937eb7c4a28559bf4c5fc4a3951656505d1fdd4f856349b4" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3496,9 +3493,9 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a43405d217dfdfb543f58847336d3af672897dd1939bb7dcf314b63cf364f1c9" +checksum = "de4566a412776e3f65d53dd9fceced5b432a0a9c9a1e8d50ccf58823795e5c5b" dependencies = [ "anyhow", "asyncband", @@ -3522,9 +3519,9 @@ dependencies = [ [[package]] name = "opendal-http-transport-reqwest" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401999057db611e592f883fcf2cbd6754ff37af587deaadd07b8c1398b2b6b06" +checksum = "f772ce6c137ab43647726116d505a649a88d41928fcd813438eb30b02312bcbb" dependencies = [ "bytes", "futures", @@ -3536,9 +3533,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9caf04d6d38713299dd4abac984b95ab16ee20e1ff09160f495c5a64644083" +checksum = "a0e7d8974aebd33be899200c9d0d50b2141fd5fa5c462b29e20653e3ddbfe339" dependencies = [ "bytes", "log", @@ -3550,9 +3547,9 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388b1d39b62535c62803754ebef89808859558697366dbedd0299345887ba461" +checksum = "2942f2d8d3d4953c0e8d07cd1244879628d084948e48ce6b6d808c5938f7858d" dependencies = [ "base64 0.23.1", "bytes", @@ -3606,7 +3603,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "foreign-types", "libc", @@ -4060,7 +4057,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit 0.25.15+spec-1.1.0", ] [[package]] @@ -4252,7 +4249,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4261,7 +4258,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4281,7 +4278,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -4424,7 +4421,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4668,11 +4665,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -4693,9 +4690,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.44" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -4747,7 +4744,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.44", + "rustls 0.23.45", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", @@ -4889,7 +4886,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4959,7 +4956,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5055,7 +5052,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5195,9 +5192,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" @@ -5324,9 +5321,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -5353,6 +5350,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "syslog" version = "7.0.0" @@ -5371,7 +5379,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5442,7 +5450,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5507,18 +5515,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -5545,7 +5544,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -5564,7 +5563,7 @@ version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ - "rustls 0.23.44", + "rustls 0.23.45", "tokio", ] @@ -5674,9 +5673,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap 2.14.2", "toml_datetime 1.1.1+spec-1.1.0", @@ -5733,7 +5732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-core", "futures-util", @@ -5881,9 +5880,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -5936,9 +5935,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6019,7 +6018,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.44", + "rustls 0.23.45", "semver", "serde", "serde_json", @@ -6134,7 +6133,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "wasm-bindgen-shared", ] @@ -6612,14 +6611,14 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -6666,14 +6665,14 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -6726,14 +6725,14 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index d3a3d5e9..7dcae503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,11 +125,11 @@ libsqlite3-sys = { version = "0.38.2", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.44", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.45", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation -uuid = { version = "1.26.0", features = ["v4"] } +uuid = { version = "1.26.1", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } @@ -143,7 +143,7 @@ job_scheduler_ng = "2.5.0" data-encoding = "2.11.1" # JWT library -jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] } +jsonwebtoken = { version = "11.1.0", default-features = false, features = ["rust_crypto", "use_pem"] } # TOTP library totp-lite = "2.0.1" @@ -201,7 +201,7 @@ reqwest = { version = "0.13.5", default-features = false, features = [ "socks", "system-proxy", ] } -hickory-resolver = "0.26.2" +hickory-resolver = "0.26.3" # Favicon extraction libraries html5gum = "0.8.4" @@ -257,8 +257,8 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.59.1", default-features = false, features = ["services-fs"] } -opendal-http-transport-reqwest = { version = "0.59.1", default-features = false, features = ["rustls-no-provider"], optional = true } +opendal = { version = "0.59.2", default-features = false, features = ["services-fs"] } +opendal-http-transport-reqwest = { version = "0.59.2", default-features = false, features = ["rustls-no-provider"], optional = true } # For retrieving AWS credentials, including temporary SSO credentials aws-config = { version = "1.12.0", optional = true, default-features = false, features = [ @@ -268,7 +268,7 @@ aws-config = { version = "1.12.0", optional = true, default-features = false, fe "sso", ] } aws-credential-types = { version = "1.3.0", optional = true } -aws-smithy-runtime-api = { version = "1.16.0", optional = true } +aws-smithy-runtime-api = { version = "1.16.2", optional = true } http = { version = "1.5.0", optional = true } reqsign-aws-v4 = { version = "3.3.0", optional = true } reqsign-core = { version = "3.3.1", optional = true } diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 34cb913d..b397fbe1 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] quote = "1.0.47" -syn = "3.0.5" +syn = "3.0.6" [lints] workspace = true diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css index 48b7400c..3a32d7ed 100644 --- a/src/static/scripts/datatables.css +++ b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-3.0.3 + * https://datatables.net/download/#bs5/dt-3.0.4 * * Included libraries: - * DataTables 3.0.3 + * DataTables 3.0.4 */ /*! DataTables Bootstrap 5 integration @@ -636,6 +636,18 @@ table.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order { left: 0.25rem; } +table.dataTable.table-sm > thead > tr th.dt-left .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-head-left .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-left .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-head-left .dt-column-order { + left: auto; + right: 0.25rem; +} +table.dataTable.table-sm > thead > tr th.dt-right .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-head-right .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-right .dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-head-right .dt-column-order { + left: 0.25rem; + right: auto; +} div.dt-scroll-head table.table-bordered { border-bottom-width: 0; diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js index 1ae94cd2..3d4c727d 100644 --- a/src/static/scripts/datatables.js +++ b/src/static/scripts/datatables.js @@ -4,13 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-3.0.3 + * https://datatables.net/download/#bs5/dt-3.0.4 * * Included libraries: - * DataTables 3.0.3 + * DataTables 3.0.4 */ -/*! DataTables 3.0.3 +/*! DataTables 3.0.4 * Copyright (c) SpryMedia Ltd - datatables.net/license */ @@ -476,7 +476,7 @@ function assignDeep(out, ...inputs) { * Deep merge objects, but shallow copy arrays. The reason we need to do this, * is that we don't want to deep copy array init values (such as aaSorting) * since the dev wouldn't be able to override them, but we do want to deep copy - * arrays. + * objects. * * @param out Object to extend * @param extender Object from which the properties will be applied to out @@ -485,7 +485,6 @@ function assignDeep(out, ...inputs) { * present. This is so you can pass in a collection to DataTables and have * that used as your data source without breaking the references * @returns out Reference, just for convenience - out === the return. - * @todo This doesn't take account of arrays inside the deep copied objects. */ function assignDeepObjects(out, extender, breakRefs = false) { let val; @@ -496,7 +495,7 @@ function assignDeepObjects(out, extender, breakRefs = false) { if (!plainObject(out[prop])) { out[prop] = {}; } - assignDeep(out[prop], val); + assignDeepObjects(out[prop], val, breakRefs); } else if (breakRefs && prop !== 'data' && @@ -3241,5511 +3240,5542 @@ var pager = { numbers_length: 7 }; -const footer = (settings, cell, classes) => { - cell.classAdd(classes.tfoot.cell); -}; -const header = (settings, cell, classes) => { - cell.classAdd(classes.thead.cell); - if (!settings.features.ordering) { - cell.classAdd(classes.order.none); - } - var titleRow = settings.titleRow; - var headerRows = cell.closest('thead').find('tr'); - var rowIdx = cell.parent().index(); - // Conditions to not apply the ordering icons - if ( - // Cells and rows which have the attribute to disable the icons - cell.attr('data-dt-order') === 'disable' || - cell.parent().attr('data-dt-order') === 'disable' || - // titleRow support, for defining a specific row in the header - (titleRow === true && rowIdx !== 0) || - (titleRow === false && rowIdx !== headerRows.count() - 1) || - (typeof titleRow === 'number' && rowIdx !== titleRow)) { - return; - } - // No additional mark-up required. Attach a sort listener to update on sort - // - note that using the `DT` namespace will allow the event to be removed - // automatically on destroy, while the `dt` namespaced event is the one we - // are listening for - Dom.s(settings.table).on('order.dt.DT column-visibility.dt.DT', function (e, ctx, column) { - if (settings !== ctx) { - // need to check if this is the host - return; // table, not a nested one - } - var sorting = ctx.sortDetails; - if (!sorting) { - return; - } - var orderedColumns = pluck(sorting, 'col'); - // This handler is only needed on column visibility if the column is - // part of the ordering. If it isn't, then we can bail out to save - // performance. It could be a separate event handler, but this is a - // balance between code reuse / size and performance console.log(e, - // e.name, column, orderedColumns, orderedColumns.includes(column)) - if (e.type === 'column-visibility' && - !orderedColumns.includes(column)) { - return; - } - var i; - var orderClasses = classes.order; - var columns = ctx.api.columns(cell); - var col = settings.columns[columns.flatten()[0]]; - var orderable = columns.orderable().includes(true); - var ariaType = ''; - var indexes = columns.indexes(); - var sortDirs = columns.orderable(true).flatten(); - var tabIndex = settings.tabIndex; - var canOrder = ctx.orderHandler && orderable; - cell.classRemove(orderClasses.isAsc + ' ' + orderClasses.isDesc) - .classToggle(orderClasses.none, !orderable) - .classToggle(orderClasses.canAsc, canOrder && sortDirs.includes('asc')) - .classToggle(orderClasses.canDesc, canOrder && sortDirs.includes('desc')); - // Determine if all of the columns that this cell covers are - // included in the current ordering - var isOrdering = true; - for (i = 0; i < indexes.length; i++) { - if (!orderedColumns.includes(indexes[i])) { - isOrdering = false; - } - } - if (isOrdering) { - // Get the ordering direction for the columns under this cell - // Note that it is possible for a cell to be asc and desc - // sorting (column spanning cells) - var orderDirs = columns.order(); - cell.classAdd((orderDirs.includes('asc') ? orderClasses.isAsc : '') + - (orderDirs.includes('desc') ? orderClasses.isDesc : '')); - } - // Find the first visible column that has ordering applied to it - - // it get's the aria information, as the ARIA spec says that only - // one column should be marked with aria-sort - var firstVis = -1; // column index - for (i = 0; i < orderedColumns.length; i++) { - if (settings.columns[orderedColumns[i]].visible) { - firstVis = orderedColumns[i]; - break; - } - } - if (indexes[0] == firstVis) { - var firstSort = sorting[0]; - var sortOrder = col.orderSequence; - cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending'); - // Determine if the next click will remove sorting or change the - // sort - ariaType = - sortOrder && !sortOrder[firstSort.index + 1] - ? 'Remove' - : 'Reverse'; - } - else { - cell.attrRemove('aria-sort'); - } - // Make the headers tab-able for keyboard navigation - if (orderable) { - var orderSpan = cell.find('.dt-column-order'); - orderSpan - .attr('role', 'button') - .attr('aria-label', orderable - ? col.ariaTitle + - ctx.api.i18n('aria.orderable' + ariaType) - : col.ariaTitle); - if (tabIndex !== -1) { - orderSpan.attr('tabindex', tabIndex); - } - } - }); +const defaults$4 = { + addedClasses: [], + cells: [], + data: [], + details: undefined, + detailsShow: undefined, + displayData: null, + idx: -1, + orderCache: null, + searchCellCache: null, + searchRowCache: null, + src: 'dom', + tr: null }; -const layout = (settings, container, items) => { - let classes = settings.classes.layout; - let row = Dom - .c('div') - .attr('id', items.id || null) - .classAdd(items.className || classes.row) - .appendTo(container); - displayRowCells(items, function (key, val) { - var klass = ''; - if (val.table) { - row.classAdd(classes.tableRow); - klass += classes.tableCell + ' '; - } - if (key === 'start') { - klass += classes.start; - } - else if (key === 'end') { - klass += classes.end; - } - else { - klass += classes.full; - } - Dom.c('div') - .attr({ - id: val.id || null, - class: val.className - ? val.className - : classes.cell + ' ' + klass - }) - .append(val.contents) - .appendTo(row); +/** + * Create a new object that is a row model + * + * @param parts Values to assign, otherwise the defaults will be used + * @returns New object + */ +function create$2(parts = {}) { + return util.object.assignDeep({}, defaults$4, parts); +} + +/** + * Add a data array to the table, creating DOM node etc. This is the parallel to + * gatherData, but for adding rows from a JavaScript source, rather than a + * DOM source. + * + * @param settings DataTables settings object + * @param dataIn data array to be added + * @param tr TR element to add to the table - optional. If not given, DataTables + * will create a row automatically + * @param tds Array of TD|TH elements for the row - must be given if tr is. + * @returns >=0 if successful (index of new data entry), -1 if failed + */ +function addData(settings, dataIn, tr, tds) { + /* Create the object for storing information about this new row */ + var rowIdx = settings.data.length; + var row = create$2({ + src: tr ? 'dom' : 'data', + idx: rowIdx }); -}; -const pagingButton = (settings, buttonType, content, active, disabled) => { - var classes = settings.classes.paging; - var btnClasses = [classes.button]; - var btn; - if (active) { - btnClasses.push(classes.active); - } - if (disabled) { - btnClasses.push(classes.disabled); + row.data = dataIn; + settings.data.push(row); + var columns = settings.columns; + for (var i = 0, iLen = columns.length; i < iLen; i++) { + // Invalidate the column types as the new data needs to be revalidated + columns[i].type = null; } - if (buttonType === 'ellipsis') { - btn = Dom.c('span').classAdd('ellipsis').html(content).get(0); + /* Add to the display array */ + settings.displayMaster.push(rowIdx); + var id = settings.rowIdFn(dataIn); + if (id !== undefined) { + settings.ids[id] = row; } - else { - btn = Dom - .c('button') - .classAdd(btnClasses.join(' ')) - .attr('role', 'link') - .attr('type', 'button') - .html(content) - .get(0); + /* Create the DOM information, or register it if already present */ + if (tr || !settings.features.deferRender) { + createTr(settings, rowIdx, tr, tds); } - return { - display: btn, - clicker: btn - }; -}; -const pagingContainer = (settings, buttons) => { - // No wrapping element - just append directly to the host - return buttons; -}; -function displayRowCells(items, fn) { - if (items.start) { - fn('start', items.start); + return rowIdx; +} +/** + * Add one or more TR elements to the table. Generally we'd expect to + * use this for reading data from a DOM sourced table, but it could be + * used for an TR element. Note that if a TR is given, it is used (i.e. + * it is not cloned). + * + * @param settings DataTables settings object + * @param rows The TR element(s) to add to the table + * @returns Array of indexes for the added rows + */ +function addTr(settings, rows) { + return rows.mapTo(el => { + let row = getRowElementsFromNode(settings, el); + return addData(settings, row.data, el, row.cells); + }); +} +/** + * Get the data for a given cell from the internal cache, taking into account + * data mapping + * + * @param settings DataTables settings object + * @param rowIdx data row id + * @param colIdx Column index + * @param type data get type ('display', 'type' 'filter|search' 'sort|order') + * @returns Cell data + */ +function getCellData(settings, rowIdx, colIdx, type) { + if (type === 'search') { + type = 'filter'; } - if (items.end) { - fn('end', items.end); + else if (type === 'order') { + type = 'sort'; } - if (items.full) { - fn('full', items.full); + var row = settings.data[rowIdx]; + if (!row) { + return undefined; } -} - -const store = { - className: {}, - detect: [], - render: {}, - search: {}, - order: {} -}; -// Common function to remove new lines, strip HTML and diacritic control -function _filterString(stripHtml, normalize) { - return function (str) { - if (util.is.empty(str) || typeof str !== 'string') { - return str; - } - str = str.replace(util.regex.reNewLines, ' '); - if (stripHtml) { - str = util.stripHtml(str); - } - { - str = util.diacritics(str, false); + var draw = settings.drawCount; + var col = settings.columns[colIdx]; + var rowData = row.data; + var defaultContent = col.defaultContent; + var cellData = col.dataGet(rowData, type, { + settings: settings, + row: rowIdx, + col: colIdx + }); + // Allow for a node being returned for non-display types + if (type !== 'display' && + cellData && + typeof cellData === 'object' && + cellData.nodeName) { + cellData = cellData.innerHTML; + } + if (cellData === undefined) { + if (settings.drawError != draw && defaultContent === null) { + log(settings, 0, 'Requested unknown parameter ' + + (typeof col.data == 'function' + ? '{function}' + : "'" + col.data + "'") + + ' for row ' + + rowIdx + + ', column ' + + colIdx, 4); + settings.drawError = draw; } - return str; - }; -} -function __numericReplace(d, decimalPlace, re1, re2) { - if (d !== 0 && (!d || d === '-')) { - return -Infinity; + return defaultContent; } - if (typeof d === 'number' || typeof d === 'bigint') { - return d; + // When the data source is null and a specific data type is requested (i.e. + // not the original data), we can use default column data + if ((cellData === rowData || cellData === null) && + defaultContent !== null && + type !== undefined) { + cellData = defaultContent; } - // If a decimal place other than `.` is used, it needs to be given to the - // function so we can detect it and replace with a `.` which is the only - // decimal place JavaScript recognises - it is not locale aware. - if (decimalPlace) { - d = util.conv.numToDecimal(d, decimalPlace); + else if (typeof cellData === 'function') { + // If the data source is a function, then we run it and use the return, + // executing in the scope of the data object (for instances) + return cellData.call(rowData); } - if (typeof d === 'string') { - if (re1) { - d = d.replace(re1, ''); - } - if (re2) { - d = d.replace(re2, ''); + if (cellData === null && type === 'display') { + return ''; + } + if (type === 'filter') { + var formatters = ext.type.search; + if (col.type && formatters[col.type]) { + cellData = formatters[col.type](cellData); } } - return d * 1; + return cellData; } -function register$1(name, prop, val) { - if (!prop) { - return { - className: store.className[name], - detect: store.detect.find(function (fn) { - return fn._name === name; - }), - order: { - pre: store.order[name + '-pre'], - asc: store.order[name + '-asc'], - desc: store.order[name + '-desc'] - }, - render: store.render[name], - search: store.search[name] - }; - } - var setProp = function (prop2, propVal) { - store[prop2][name] = propVal; - }; - var setDetect = function (detect) { - // `detect` can be a function or an object - we set a name - // property for either - that is used for the detection - Object.defineProperty(detect, '_name', { value: name }); - var idx = store.detect.findIndex(function (item) { - return item._name === name; +/** + * Set the value for a specific cell, into the internal data cache + * + * @param settings DataTables settings object + * @param rowIdx data row id + * @param colIdx Column index + * @param val Value to set + */ +function setCellData(settings, rowIdx, colIdx, val) { + let row = settings.data[rowIdx]; + if (row) { + let col = settings.columns[colIdx]; + let rowData = row.data; + col.dataSet(rowData, val, { + settings: settings, + row: rowIdx, + col: colIdx }); - if (idx === -1) { - store.detect.unshift(detect); - } - else { - store.detect.splice(idx, 1, detect); - } - }; - var setOrder = function (obj) { - store.order[name + '-pre'] = obj.pre; // can be undefined - store.order[name + '-asc'] = obj.asc; // can be undefined - store.order[name + '-desc'] = obj.desc; // can be undefined - }; - // prop is optional - if (val === undefined) { - val = prop; - prop = undefined; - } - if (prop === 'className') { - setProp('className', val); } - else if (prop === 'detect') { - setDetect(val); +} +/** + * Write a value to a cell + * + * @param td Cell + * @param val Value + */ +function writeCell(td, val) { + let cell = Dom.s(td); + if (val && typeof val === 'object' && val.nodeName) { + cell.empty().append(val); } - else if (prop === 'order') { - setOrder(val); + else { + cell.html(val); } - else if (prop === 'render') { - setProp('render', val); +} +/** + * Return an array with the full table data + * + * @param settings DataTables settings object + * @returns array {array} aData Master data array + */ +function getDataMaster(settings) { + return util.array.pluck(settings.data, 'data'); +} +/** + * Nuke the table + * + * @param settings DataTables settings object + */ +function clearTable(settings) { + settings.data.length = 0; + settings.displayMaster.length = 0; + settings.display.length = 0; + settings.ids = {}; +} +/** + * Mark cached data as invalid such that a re-read of the data will occur when + * the cached data is next requested. Also update from the data source object. + * + * @param settings DataTables settings object + * @param rowIdx Row index to invalidate + * @param src Source to invalidate from: undefined, 'auto', 'dom' or 'data' + * @param colIdx Column index to invalidate. If undefined the whole row will be + * invalidated + */ +function invalidateRow(settings, rowIdx, src, colIdx) { + var row = settings.data[rowIdx]; + var i, iLen; + if (!row) { + return; } - else if (prop === 'search') { - setProp('search', val); + // Remove the cached data for the row + row.orderCache = null; + row.searchCellCache = null; + row.displayData = null; + // Are we reading last data from DOM or the data object? + if (src === 'dom' || ((!src || src === 'auto') && row.src === 'dom')) { + // Read the data from the DOM + row.data = getRowElementsFromModel(settings, row, colIdx).data; } - else if (!prop) { - if (val.className) { - setProp('className', val.className); - } - if (val.detect !== undefined) { - setDetect(val.detect); - } - if (val.order) { - setOrder(val.order); - } - if (val.render !== undefined) { - setProp('render', val.render); - } - if (val.search !== undefined) { - setProp('search', val.search); + else { + // Reading from data object, update the DOM + var cells = row.cells; + var display = getRowDisplay(settings, rowIdx); + if (cells.length) { + if (colIdx !== undefined) { + writeCell(cells[colIdx], display[colIdx]); + } + else { + for (i = 0, iLen = cells.length; i < iLen; i++) { + writeCell(cells[i], display[i]); + } + } } } + invalidColumn(settings, colIdx); + // Update DataTables special `DT_*` attributes for the row + rowAttributes(settings, row); + callbackFire(settings, null, 'rowInvalidate', [settings, rowIdx, colIdx], false); } -// Get a list of types -function types() { - return store.detect.map(function (detect) { - return detect._name; - }); -} -var __diacriticSort = function (a, b) { - a = a !== null && a !== undefined ? a.toString().toLowerCase() : ''; - b = b !== null && b !== undefined ? b.toString().toLowerCase() : ''; - // Checked for `navigator.languages` support in `oneOf` so this code can't execute in old - // Safari and thus can disable this check - // eslint-disable-next-line compat/compat - return a.localeCompare(b, navigator.languages[0] || navigator.language, { - numeric: true, - ignorePunctuation: true - }); -}; -var __diacriticHtmlSort = function (a, b) { - a = util.stripHtml(a); - b = util.stripHtml(b); - return __diacriticSort(a, b); -}; -// -// Built in data types -// -register$1('string', { - detect: function () { - return 'string'; - }, - order: { - pre: function (a) { - // This is a little complex, but faster than always calling toString, - // http://jsperf.com/tostring-v-check - return util.is.empty(a) && typeof a !== 'boolean' - ? '' - : typeof a === 'string' - ? a.toLowerCase() - : !a.toString - ? '' - : a.toString(); - } - }, - search: _filterString(false) -}); -register$1('string-utf8', { - detect: { - allOf: function () { - return true; - }, - oneOf: function (d) { - // At least one data point must contain a non-ASCII character - // This line will also check if navigator.languages is supported or not. If not (Safari 10.0-) - // this data type won't be supported. - // eslint-disable-next-line compat/compat - return (!util.is.empty(d) && - navigator.languages && - typeof d === 'string' && - !!d.match(/[^\x00-\x7F]/)); - } - }, - order: { - asc: __diacriticSort, - desc: function (a, b) { - return __diacriticSort(a, b) * -1; - } - }, - search: _filterString(false) -}); -register$1('html', { - detect: { - allOf: function (d) { - return (util.is.empty(d) || - (typeof d === 'string' && d.indexOf('<') !== -1)); - }, - oneOf: function (d) { - // At least one data point must contain a `<` - return (!util.is.empty(d) && - typeof d === 'string' && - d.indexOf('<') !== -1); - } - }, - order: { - pre: function (a) { - return util.is.empty(a) - ? '' - : a.replace - ? util.stripHtml(a).trim().toLowerCase() - : a + ''; - } - }, - search: _filterString(true) -}); -register$1('html-utf8', { - detect: { - allOf: function (d) { - return (util.is.empty(d) || - (typeof d === 'string' && d.indexOf('<') !== -1)); - }, - oneOf: function (d) { - // At least one data point must contain a `<` and a non-ASCII character - // eslint-disable-next-line compat/compat - return (navigator.languages && - !util.is.empty(d) && - typeof d === 'string' && - d.indexOf('<') !== -1 && - typeof d === 'string' && - !!d.match(/[^\x00-\x7F]/)); - } - }, - order: { - asc: __diacriticHtmlSort, - desc: function (a, b) { - return __diacriticHtmlSort(a, b) * -1; - } - }, - search: _filterString(true) -}); -register$1('date', { - className: 'dt-type-date', - detect: { - allOf: function (d) { - // V8 tries _very_ hard to make a string passed into `Date.parse()` - // valid, so we need to use a regex to restrict date formats. Use a - // plug-in for anything other than ISO8601 style strings - if (d && !(d instanceof Date) && !util.regex.reDate.test(d)) { - return null; - } - var parsed = Date.parse(d); - return (parsed !== null && !isNaN(parsed)) || util.is.empty(d); - }, - oneOf: function (d) { - // At least one entry must be a date or a string with a date - return (d instanceof Date || - (typeof d === 'string' && util.regex.reDate.test(d))); - } - }, - order: { - pre: function (d) { - var ts = Date.parse(d); - return isNaN(ts) ? -Infinity : ts; - } +/** + * Column specific invalidation + * + * @param settings DataTables settings object + * @param colIdx Column index to invalidate, or all columns if not given + */ +function invalidColumn(settings, colIdx) { + // Column specific invalidation + var cols = settings.columns; + if (colIdx !== undefined) { + // Type - the data might have changed + cols[colIdx].type = null; + // Max length string. Its a fairly cheep recalculation, so not worth + // something more complicated + cols[colIdx].wideStrings = null; } -}); -register$1('html-num-fmt', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, true, false); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, true, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reHtml, util.regex.reFormattedNumeric); - } - }, - search: _filterString(true) -}); -register$1('html-num', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, false, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.htmlNum(d, decimal, false, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reHtml); - } - }, - search: _filterString(true) -}); -register$1('num-fmt', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.num(d, decimal, true, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.num(d, decimal, true, false); - } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp, util.regex.reFormattedNumeric); + else { + for (let i = 0, iLen = cols.length; i < iLen; i++) { + cols[i].type = null; + cols[i].wideStrings = null; } } -}); -register$1('num', { - className: 'dt-type-numeric', - detect: { - allOf: function (d, settings) { - var decimal = settings.language.decimal; - return util.is.num(d, decimal, false, true); - }, - oneOf: function (d, settings) { - // At least one data point must contain a numeric value - var decimal = settings.language.decimal; - return util.is.num(d, decimal, false, false); + settings.containerWidth = -1; +} +/** + * Get the cells and data for a given row - from a element + * + * @param settings DataTables settings object + * @param row TR element from which to read data or existing row object from + * which to re-read the data from the cells + */ +function getRowElementsFromNode(settings, row) { + let data = settings.rowReadObject ? {} : []; + let cells = Dom.s(row).children('th, td'); + let id = row.getAttribute('id'); + cells.each((el, idx) => { + readCellData(settings, el, data, idx); + }); + if (id) { + util.set(settings.rowId)(data, id); + } + return { + data: data, + cells: cells.get() + }; +} +/** + * Get the cells and data for a given row - from an existing row model + * + * @param settings DataTables settings object + * @param row Existing row object from which to re-read the data from the cells + * @param colIdx Optional column index + */ +function getRowElementsFromModel(settings, row, colIdx) { + let tds = row.cells; + for (let i = 0; i < tds.length; i++) { + if (colIdx === undefined || colIdx === i) { + readCellData(settings, tds[i], row.data, i); } - }, - order: { - pre: function (d, s) { - var dp = s.language.decimal; - return __numericReplace(d, dp); + } + // Read the ID from the DOM if present + if (row.tr) { + let id = row.tr.getAttribute('id'); + if (id) { + util.set(settings.rowId)(row.data, id); } } -}); - + return { + data: row.data, + cells: tds + }; +} /** - * DataTables extensions - * - * This namespace acts as a collection area for plug-ins that can be used to - * extend DataTables capabilities. Indeed many of the build in methods - * use this method to provide their own capabilities (sorting methods for - * example). + * Read data from a cell into the data source object * - * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy - * reasons + * @param settings DataTables settings object + * @param cell The HTML cell element to read from + * @param data Data object / array to store data into + * @param colIdx The column index for the cell */ -const ext = { - /** - * DataTables build type (expanded by the download builder) - */ - builder: 'bs5/dt-3.0.3', - /** - * Buttons. For use with the Buttons extension for DataTables. This is - * defined here so other extensions can define buttons regardless of load - * order. It is _not_ used by DataTables core. - */ - buttons: {}, - /** - * ColumnControl buttons and content - */ - ccContent: {}, - /** - * Element class names - */ - classes: classes$1, - /** - * Error reporting. - * - * How should DataTables report an error. Can take the value 'alert', - * 'throw', 'none' or a function. - */ - errMode: 'alert', - /** HTML entity escaping */ - escape: { - /** When reading data-* attributes for initialisation options */ - attributes: false - }, - /** - * Legacy so v1 plug-ins don't throw js errors on load - */ - feature: legacy, - /** - * Feature plug-ins. - * - * This is an object of callbacks which provide the features for DataTables - * to be initialised via the `layout` option. - */ - features: features, - /** - * Row searching. - * - * This method of searching is complimentary to the default type based - * searching, and a lot more comprehensive as it allows you complete control - * over the searching logic. Each element in this array is a function - * (parameters described below) that is called for every row in the table, - * and your logic decides if it should be included in the searching data set - * or not. - */ - search: [], - /** - * Selector extensions - * - * The `selector` option can be used to extend the options available for the - * selector modifier options (`selector-modifier` object data type) that - * each of the three built in selector types offer (row, column and cell + - * their plural counterparts). For example the Select extension uses this - * mechanism to provide an option to select only rows, columns and cells - * that have been marked as selected by the end user (`{selected: true}`), - * which can be used in conjunction with the existing built in selector - * options. - */ - selector: { - cell: [], - column: [], - row: [] - }, - settings: [], - /** - * Legacy configuration options. Enable and disable legacy options that - * are available in DataTables. - * - * @type object - */ - legacy: { - /** - * Enable / disable DataTables 1.9 compatible server-side processing - * requests - */ - ajax: null - }, - /** - * Pagination plug-in methods. - * - * Each entry in this object is a function and defines which buttons should - * be shown by the pagination rendering method that is used for the table. - * The renderer addresses how the buttons are displayed in the document, - * while the functions here tell it what buttons to display. This is done by - * returning an array of button descriptions (what each button will do). - */ - pager: pager, - renderer: { - footer: { - _: footer - }, - header: { - _: header - }, - layout: { - _: layout - }, - pagingButton: { - _: pagingButton - }, - pagingContainer: { - _: pagingContainer +function readCellData(settings, cell, data, colIdx) { + let column = settings.columns[colIdx]; + let contents = cell.innerHTML.trim(); + if (column.attrSrc) { + // If we are working with attributes from the cell as values + let dataPoint = column.data; + let setter = util.set(dataPoint._); + let attr = function (str, cell) { + if (typeof str === 'string') { + let idx = str.indexOf('@'); + if (idx !== -1) { + let att = str.substring(idx + 1); + let setter = util.set(str); + setter(data, cell.getAttribute(att)); + } + } + }; + setter(data, contents); + attr(dataPoint.sort, cell); + attr(dataPoint.type, cell); + attr(dataPoint.filter, cell); + } + else { + if (!column.setter) { + // Cache the setter function + column.setter = util.set(column.data); } - }, - /** - * Rendering helper function exposed for use by the styling integrations. - */ - rendererDisplayRowCells: displayRowCells, - /** - * Ordering plug-ins - custom data source - * - * The extension options for ordering of data available here is - * complimentary to the default type based ordering that DataTables - * typically uses. It allows much greater control over the data that is - * being used to order a column, but is necessarily therefore more complex. - */ - order: {}, - /** - * Type based plug-ins. - * - * Each column in DataTables has a type assigned to it, either by automatic - * detection or by direct assignment using the `type` option for the column. - * The type of a column will effect how it is ordering and search (plug-ins - * can also make use of the column type if required). - */ - type: store, - /** - * Unique DataTables instance counter - * - * @type int - * @private - */ - _unique: 0, - // - // Depreciated - // The following properties are retained for backwards compatibility only. - // The should not be used in new projects and will be removed in a future - // version - // - /** - * Software version - * @type string - */ - version: '3.0.3' -}; -// -// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts -// -Object.assign(ext, { - afnFiltering: ext.search, - aTypes: ext.type.detect, - ofnSearch: ext.type.search, - oSort: ext.type.order, - afnSortData: ext.order, - aoFeatures: ext.feature, - oStdClasses: ext.classes, - oPagination: ext.pager, - sVersion: ext.version, - fnVersionCheck: check$1 -}); + column.setter(data, contents); + } +} /** - * Log an error message + * Generate the node required for the processing node + * + * @param ctx DataTables settings object + */ +function processingHtml(ctx) { + var table = ctx.table; + var scrolling = ctx.scroll.x !== '' || ctx.scroll.y !== ''; + if (ctx.features.processing) { + var n = Dom + .c('div') + .attr('id', ctx.tableId + '_processing') + .attr('role', 'status') + .classAdd(ctx.classes.processing.container) + .html(ctx.language.processing) + .append(Dom + .c('div') + .append(Dom.c('div')) + .append(Dom.c('div')) + .append(Dom.c('div')) + .append(Dom.c('div'))); + // Different positioning depending on if scrolling is enabled or not + if (scrolling) { + n.prependTo(Dom.s(ctx.tableWrapper).find('div.dt-scroll').get(0)); + } + else { + n.insertBefore(table); + } + Dom.s(table).on('processing.dt.DT', (e, s, show) => { + n.css('display', show ? 'block' : 'none'); + }); + } +} +/** + * Display or hide the processing indicator * * @param ctx DataTables settings object - * @param level log error messages, or display them to the user - * @param msg error message - * @param tn Technical note id to get more information about the error. + * @param show Show the processing indicator (true) or not (false) */ -function log(ctx, level, msg, tn) { - msg = - 'DataTables warning: ' + - (ctx ? 'table id=' + ctx.tableId + ' - ' : '') + - msg; - if (tn) { - msg += - '. For more information about this error, please see ' + - 'https://datatables.net/tn/' + - tn; +function processingDisplay(ctx, show) { + // Ignore cases when we are still redrawing + if (ctx.doingDraw && show === false) { + return; } - { - // Backwards compatibility pre 1.10 - var type = ext.sErrMode || ext.errMode; - if (ctx) { - callbackFire(ctx, null, 'dt-error', [ctx, tn, msg], true); - } - if (type == 'alert') { - alert(msg); - } - else if (type == 'throw') { - throw new Error(msg); - } - else if (typeof type == 'function') { - type(ctx, tn, msg); - } + callbackFire(ctx, null, 'processing', [ctx, show]); +} +/** + * Show the processing element if an action takes longer than a given time + * + * @param ctx DataTables settings object + * @param enable Do (true) or not (false) async processing (local feature enablement) + * @param run Function to run + */ +function processingRun(ctx, enable, run) { + if (!enable) { + // Immediate execution, synchronous + run(); + } + else { + processingDisplay(ctx, true); + // Allow the processing display to show if needed + setTimeout(function () { + run(); + processingDisplay(ctx, false); + }, 0); + } +} + +function renderer(ctx, type) { + var render = ctx.renderer; + var host = ext.renderer[type]; + if (plainObject(render) && render[type]) { + // Specific renderer for this type. If available use it, otherwise use + // the default. + return host[render[type]] || host._; + } + else if (typeof render === 'string') { + // Common renderer - if there is one available for this type use it, + // otherwise use the default + return host[render] || host._; + } + // Use the default + return host._; +} + +/** + * Recalculate the column widths, if needed (by a column having been + * invalidated) + * + * @param settings DataTables settings object + */ +function columnWidths(settings) { + if (settings.columns.map(c => c.wideStrings).includes(null)) { + calculateColumnWidths(settings); } } /** - * See if a property is defined on one object, if so assign it to the other - * object + * Calculate the width of columns for the table * - * @param ret target object - * @param src source object - * @param name property - * @param mappedName name to map too - optional, name used if not given + * @param settings DataTables settings object */ -function map(ret, src, name, mappedName) { - if (Array.isArray(name)) { - for (let i = 0; i < name.length; i++) { - let val = name[i]; - if (Array.isArray(val)) { - map(ret, src, val[0], val[1]); +function calculateColumnWidths(settings) { + // Not interested in doing column width calculation if auto-width is disabled + if (!settings.features.autoWidth) { + return; + } + var table = settings.table, columns = settings.columns, scroll = settings.scroll, scrollY = scroll.y, scrollX = scroll.x, visibleColumns = getColumns(settings, 'visible'), tableWidthAttr = table.getAttribute('width'), // from DOM element + tableContainer = table.parentElement, i, j, column, columnIdx; + var styleWidth = table.style.width; + var containerWidth = wrapperWidth(settings); + // Don't re-run for the same width as the last time + if (containerWidth === settings.containerWidth) { + return false; + } + settings.containerWidth = containerWidth; + // If there is no width applied as a CSS style or as an attribute, we assume that + // the width is intended to be 100%, which is usually is in CSS, but it is very + // difficult to correctly parse the rules to get the final result. + if (!styleWidth && !tableWidthAttr) { + table.style.width = '100%'; + styleWidth = '100%'; + } + if (styleWidth && styleWidth.indexOf('%') !== -1) { + tableWidthAttr = styleWidth; + } + // Let plug-ins know that we are doing a recalc, in case they have changed any of the + // visible columns their own way (e.g. Responsive uses display:none). + callbackFire(settings, null, 'column-calc', [{ visible: visibleColumns }], false); + // Construct a worst case table with the widest, assign any user defined + // widths, then insert it into the DOM and allow the browser to do all + // the hard work of calculating table widths + var tmpTable = Dom + .s(table.cloneNode()) + .css('visibility', 'hidden') + .css('margin', '0') + .attrRemove('id'); + // Clean up the table body + tmpTable.append(Dom.c('tbody')); + // Clone the table header and footer - we can't use the header / footer + // from the cloned table, since if scrolling is active, the table's + // real header and footer are contained in different table tags + tmpTable + .append(settings.thead.cloneNode(true)) + .append(settings.tfoot.cloneNode(true)); + // Remove any assigned widths from the footer (from scrolling) + tmpTable.find('tfoot th, tfoot td').css('width', ''); + // Apply custom sizing to the cloned header + tmpTable.find('thead th, thead td').each(cell => { + // Get the `width` from the header layout + var width = columnsSumWidth(settings, cell, true); + if (width) { + cell.style.width = width; + // For scrollX we need to force the column width otherwise the + // browser will collapse it. If this width is smaller than the + // width the column requires, then it will have no effect + if (scrollX) { + cell.style.minWidth = width; + Dom.s(cell).append(Dom.c('div').css({ + width: width, + margin: '0', + padding: '0', + border: '0', + height: '1px' + })); } - else { - map(ret, src, val); + } + else { + cell.style.width = ''; + } + }); + // Get the widest strings for each of the visible columns and add them to + // our table to create a "worst case" + var longestData = []; + for (i = 0; i < visibleColumns.length; i++) { + longestData.push(getWideStrings(settings, visibleColumns[i])); + } + if (longestData.length) { + for (i = 0; i < longestData[0].length; i++) { + var tr = Dom.c('tr').appendTo(tmpTable.find('tbody')); + for (j = 0; j < visibleColumns.length; j++) { + columnIdx = visibleColumns[j]; + column = columns[columnIdx]; + var longest = longestData[j][i] || ''; + var autoClass = ext.type.className[column.type]; + var padding = column.contentPadding || (scrollX ? '-' : ''); + var text = longest + padding; + var cell = Dom + .c('td') + .classAdd(autoClass) + .classAdd(column.className) + .appendTo(tr); + if (longest.indexOf('<') === -1 && + longest.indexOf('&') === -1) { + cell.text(text); + } + else { + cell.html(text); + } } } - return; } - if (mappedName === undefined) { - mappedName = name; + // Tidy the temporary table - remove name attributes so there aren't + // duplicated in the dom (radio elements for example) + tmpTable.find('[name]').attrRemove('name'); + // Table has been built, attach to the document so we can work with it. + // A holding element is used, positioned at the top of the container + // with minimal height, so it has no effect on if the container scrolls + // or not. Otherwise it might trigger scrolling when it actually isn't + // needed + var holder = Dom + .c('div') + .css(scrollX || scrollY + ? { + position: 'absolute', + top: '0', + left: '0', + height: '1px', + right: '0', + overflow: 'hidden' + } + : {}) + .append(tmpTable) + .appendTo(tableContainer); + // When scrolling (X or Y) we want to set the width of the table as + // appropriate. However, when not scrolling leave the table width as it + // is. This results in slightly different, but I think correct behaviour + if (scrollX) { + tmpTable.css('width', 'auto').attrRemove('width'); + // If there is no width attribute or style, then allow the table to + // collapse + if (tmpTable.width() < tableContainer.clientWidth && tableWidthAttr) { + tmpTable.width(tableContainer.clientWidth); + } } - if (src[name] !== undefined) { - ret[mappedName] = src[name]; + else if (scrollY) { + tmpTable.width(tableContainer.clientWidth); } -} -/** - * Bind an event handler to allow a click or return key to activate the callback. - * This is good for accessibility since a return on the keyboard will have the - * same effect as a click, if the element has focus. - * - * @param n Element to bind the action to - * @param selector Selector (for delegated events) - * @param fn Callback function for when the event is triggered - */ -function bindAction(n, selector, fn) { - Dom.s(n) - .on('click.DT', selector, function (e) { - fn(e); - }) - .on('keypress.DT', selector, function (e) { - if (e.which === 13) { - e.preventDefault(); - fn(e); - } - }) - .on('selectstart.DT', selector, function () { - // Don't want a double click resulting in text selection - return false; - }); -} -/** - * Register a callback function. Easily allows a callback function to be added - * to an array store of callback functions that can then all be called together. - * - * @param settings dataTables settings object - * @param store Name of the array storage for the callbacks in settings - * @param fn Function to be called back - */ -function callbackReg(ctx, store, fn) { - if (fn) { - ctx.callbacks[store].push(fn); + else if (tableWidthAttr) { + tmpTable.width(tableWidthAttr); } -} -/** - * Fire callback functions and trigger events. Note that the loop over the - * callback array store is done backwards! Further note that you do not want to - * fire off triggers in time sensitive applications (for example cell creation) - * as its slow. - * - * @param ctx DataTables settings object - * @param callbackArr Name of the array storage for the callbacks in the context - * @param eventName Name of the custom event to trigger. If null no trigger is - * fired - * @param args Array of arguments to pass to the callback function / trigger - * @param bubbles True if the event should bubble - */ -function callbackFire(ctx, callbackArr, eventName, args, bubbles = false) { - var ret = []; - if (callbackArr) { - ret = ctx.callbacks[callbackArr] - .slice() - .reverse() - .map(function (val) { - return val.apply(ctx.instance, args); - }); + // Get the width of each column in the constructed table + var total = 0; + var bodyCells = tmpTable.find('tbody tr').eq(0).children(); + for (i = 0; i < visibleColumns.length; i++) { + // Use getBounding for sub-pixel accuracy, which we then want to round + // up! + var bounding = bodyCells.get(i).getBoundingClientRect().width; + // Total is tracked to remove any sub-pixel errors as the outerWidth + // of the table might not equal the total given here + total += bounding; + // Width for each column to use + columns[visibleColumns[i]].width = stringToCss(bounding); } - if (eventName !== null) { - let table = Dom.s(ctx.table); - let result = table.trigger(eventName + '.dt', bubbles, args, { - dt: ctx.api + table.style.width = stringToCss(total); + // Finished with the table - ditch it + holder.remove(); + // If there is a width attr, we want to attach an event listener which + // allows the table sizing to automatically adjust when the window is + // resized. Use the width attr rather than CSS, since we can't know if the + // CSS is a relative value or absolute - DOM read is always px. + if (tableWidthAttr) { + table.style.width = stringToCss(tableWidthAttr); + } + if ((tableWidthAttr || scrollX) && !settings.reszEvt) { + var resize = util.throttle(function () { + var newWidth = wrapperWidth(settings); + // Don't do it if destroying or the container width is 0 + if (!settings.destroying && newWidth !== 0) { + adjustColumnSizing(settings); + } }); - // If not yet attached to the document, trigger the event - // on the body directly to sort of simulate the bubble - if (bubbles && table.closest('body').count() === 0) { - Dom.s('body').trigger(eventName + '.dt', bubbles, args, { - dt: ctx.api + // For browsers that support it (~2020 onwards for wide support) we can watch for the + // container changing width. + if (window.ResizeObserver) { + // This is a tricky beast - if the element is visible when `.observe()` is called, + // then the callback is immediately run. Which we don't want. If the element isn't + // visible, then it isn't run, but we want it to run when it is then made visible. + // This flag allows the above to be satisfied. + var first = Dom.s(settings.tableWrapper).isVisible(); + // Use an empty div to attach the observer so it isn't impacted by height changes + var resizer = Dom + .c('div') + .css({ + width: '100%', + height: '0' + }) + .classAdd('dt-autosize') + .appendTo(settings.tableWrapper); + settings.resizeObserver = new ResizeObserver(function (e) { + if (first) { + first = false; + } + else { + resize(); + } }); + settings.resizeObserver.observe(resizer.get(0)); } - ret.push(result[0]); - } - return ret; -} -function lengthOverflow(ctx) { - var start = ctx.displayStart, end = displayEnd(ctx), len = ctx.pageLength; - // If we have space to show extra rows (backing up from the end point - then - // do so - if (start >= end) { - start = end - len; - } - // Keep the start record on the current page - start -= start % len; - if (len === -1 || start < 0) { - start = 0; + else { + // For old browsers, the best we can do is listen for a window + // resize + window.addEventListener('resize', resize); + settings.windowResizeCb = resize; // For removal in `destroy` + } + settings.reszEvt = true; } - ctx.displayStart = start; } /** - * Detect the data source being used for the table. Used to simplify the code a - * little (ajax) and to make it compress a little smaller. + * Get the width of the DataTables wrapper element * - * @param ctx DataTables settings object - * @returns Data source + * @param settings DataTables settings object + * @returns Width */ -function dataSource(ctx) { - if (ctx.features.serverSide) { - return 'ssp'; - } - else if (ctx.ajax) { - return 'ajax'; - } - return 'dom'; +function wrapperWidth(settings) { + let wrapper = Dom.s(settings.tableWrapper); + return wrapper.isVisible() ? wrapper.width() : 0; } /** - * Common replacement for language strings + * Get the widest strings for each column. * - * @param ctx DataTables settings object - * @param str String with values to replace - * @param entries Plural number for _ENTRIES_ - can be undefined - * @returns String - */ -function macros(ctx, str, entries) { - // When infinite scrolling, we are always starting at 1. _iDisplayStart is - // used only internally - var formatter = ctx.formatNumber, start = ctx.displayStart + 1, len = ctx.pageLength, vis = recordsDisplay(ctx), max = recordsTotal(ctx), all = len === -1; - return str - .replace(/_START_/g, formatter(start, ctx)) - .replace(/_END_/g, formatter(displayEnd(ctx), ctx)) - .replace(/_MAX_/g, formatter(max, ctx)) - .replace(/_TOTAL_/g, formatter(vis, ctx)) - .replace(/_PAGE_/g, formatter(all ? 1 : Math.ceil(start / len), ctx)) - .replace(/_PAGES_/g, formatter(all ? 1 : Math.ceil(vis / len), ctx)) - .replace(/_ENTRIES_/g, ctx.api.i18n('entries', '', entries)) - .replace(/_ENTRIES-MAX_/g, ctx.api.i18n('entries', '', max)) - .replace(/_ENTRIES-TOTAL_/g, ctx.api.i18n('entries', '', vis)); -} -/** - * Add elements to an array as quickly as possible, but stack safe. + * It is very difficult to determine what the widest string actually is due to variable character + * width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance + * and this is a critical point, so we use two techniques to determine a collection of the longest + * strings from the column, which will likely contain the widest strings: * - * @param arr Array to add the data to - * @param data Data array that is to be added + * 1) Get the top three longest strings from the column + * 2) Get the top three widest words (i.e. an unbreakable phrase) + * + * @param settings DataTables settings object + * @param colIdx column of interest + * @returns Array of the longest strings */ -function arrayApply(arr, data) { - if (!data) { - return; - } - // Chrome can throw a max stack error if apply is called with - // too large an array, but apply is faster. - if (data.length < 10000) { - arr.push.apply(arr, data); - } - else { - for (var i = 0; i < data.length; i++) { - arr.push(data[i]); +function getWideStrings(settings, colIdx) { + var column = settings.columns[colIdx]; + // Do we need to recalculate (i.e. was invalidated), or just use the cached data? + if (!column.wideStrings) { + var allStrings = []; + var collection = []; + // Create an array with the string information for the column + for (var i = 0, iLen = settings.displayMaster.length; i < iLen; i++) { + var rowIdx = settings.displayMaster[i]; + var data = getRowDisplay(settings, rowIdx)[colIdx]; + var cellString = data && typeof data === 'object' && data.nodeType + ? data.innerHTML + : data + ''; + // Remove id / name attributes from elements so they + // don't interfere with existing elements + cellString = cellString + .replace(/id=".*?"/g, '') + .replace(/name=".*?"/g, ''); + // Don't want script, dialog or template tags in the width + // calculations as they are hidden content + cellString = cellString + .replace(/]*)?>/gi, ' ') + .replace(/]*)?>/gi, ' ') + .replace(/]*)?>/gi, ' '); + var noHtml = util.string + .stripHtml(cellString, ' ') + .replace(/ /g, ' '); + collection.push({ + str: cellString, + len: noHtml.length + }); + allStrings.push(noHtml); + } + // Order and then cut down to the size we need + collection + .sort(function (a, b) { + return b.len - a.len; + }) + .splice(3); + column.wideStrings = collection.map(function (item) { + return item.str; + }); + // Longest unbroken string + const parts = allStrings.join(' ').split(' '); + parts.sort(function (a, b) { + return b.length - a.length; + }); + if (parts.length) { + column.wideStrings.push(parts[0]); + } + if (parts.length > 1) { + column.wideStrings.push(parts[1]); + } + if (parts.length > 2) { + column.wideStrings.push(parts[3]); } } + return column.wideStrings; } /** - * Add one or more listeners to the table + * Append a CSS unit (only if required) to a string * - * @param that JQ for the table - * @param name Event name - * @param src Listener(s) + * @param s Value to css-ify + * @returns Value with css unit */ -function listener(that, name, src) { - let srcArr = Array.isArray(src) ? src : [src]; - for (var i = 0; i < srcArr.length; i++) { - that.on(name + '.dt.DT', srcArr[i]); +function stringToCss(s) { + if (s === null) { + return '0px'; } + if (typeof s == 'number') { + return s < 0 ? '0px' : s + 'px'; + } + // Check it has a unit character already + return s.match(/\d$/) ? s + 'px' : s; } /** - * Escape HTML entities in strings, in an object + * Re-insert the `col` elements for current visibility + * + * @param settings DT settings */ -function escapeObject(obj) { - if (ext.escape.attributes) { - each(obj, function (key, val) { - obj[key] = escapeHtml(val); - }); +function colGroup(settings) { + var cols = settings.columns; + settings.colgroup.empty(); + for (var i = 0; i < cols.length; i++) { + if (cols[i].visible) { + settings.colgroup.append(cols[i].colEl); + } } - return obj; } -/* - * Public helper functions. These aren't used internally by DataTables, or - * called by any of the options passed into DataTables, but they can be used - * externally by developers working with DataTables. They are helper functions - * to make working with DataTables a little bit easier. - */ /** - * Common logic for moment, luxon or a date action. + * Scrolling setup * - * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again + * @param settings DataTables settings object + * @returns Node to add to the DOM */ -function __mld(dtLib, momentFn, luxonFn, dateFn, arg1) { - if (__moment) { - return dtLib[momentFn](arg1); +function featureTable(settings) { + let table = Dom.s(settings.table); + let scroll = settings.scroll; + let scrollX = scroll.x; + let scrollY = scroll.y; + // No scrolling or x-scrolling only + if (scrollY === '' && scrollX === '') { + return table.get(0); } - else if (__luxon) { - return dtLib[luxonFn](arg1); + let classes = settings.classes.scrolling; + let caption = settings.captionNode; + let captionSide = caption + ? caption._captionSide + : null; + let tableCloneHeader = table.clone(false); + let tableCloneFooter = table.clone(false); + let footer = table.children('tfoot'); + let size = function (s) { + return !s ? '100%' : stringToCss(s); + }; + /* + * The HTML structure that we want to generate in this function is: + * div - scroller + * div - scroll head + * div - scroll head inner + * table - scroll head table + * thead - thead + * div - scroll body + * table - table (master table) + * thead - thead clone for sizing + * tbody - tbody + * div - scroll foot + * div - scroll foot inner + * table - scroll foot table + * tfoot - tfoot + */ + let scroller = Dom.c('div') + .classAdd(classes.container) + .attr('role', 'table') + .append(Dom.c('div') + .classAdd(classes.header.self) + .css({ + overflow: 'hidden', + position: 'relative', + border: '0', + width: scrollX ? size(scrollX) : '100%' + }) + .attr('role', 'none') + .append(Dom.c('div') + .classAdd(classes.header.inner) + .css({ + 'box-sizing': 'content-box', + width: scroll.xInner || '100%' + }) + .attr('role', 'none') + .append(tableCloneHeader + .attrRemove('id') + .css('margin-left', '0') + .append(captionSide === 'top' ? caption : null) + .append(table.children('thead'))))) + .append(Dom.c('div') + .classAdd(classes.body) + .css({ + position: 'relative', + overflow: 'auto', + width: size(scrollX) + }) + .attr('role', 'none') + .append(table)); + if (footer.count()) { + scroller.append(Dom.c('div') + .classAdd(classes.footer.self) + .css({ + overflow: 'hidden', + border: '0', + width: scrollX ? size(scrollX) : '100%' + }) + .attr('role', 'none') + .append(Dom.c('div') + .classAdd(classes.footer.inner) + .attr('role', 'none') + .append(tableCloneFooter + .attrRemove('id') + .css('margin-left', '0') + .append(captionSide === 'bottom' ? caption : null) + .append(table.children('tfoot'))))); } - return dateFn ? dtLib[dateFn](arg1) : dtLib; + let children = scroller.children(); + let scrollHead = children.eq(0); + let scrollBody = children.eq(1); + let scrollFoot = children.eq(2); + // When the body is scrolled, then we also want to scroll the header and + // footer. Note that each element has its own scroll listener, and that in + // turn sets the scroll for the other elements. However this doesn't lead to + // an infinite loop as `scroll` is only triggered if the value changes. + scrollBody.on('scroll.DT', () => { + let scrollLeft = scrollBody.scrollLeft(); + scrollHead.scrollLeft(scrollLeft); + scrollFoot.scrollLeft(scrollLeft); + }); + scrollHead.on('scroll.DT', () => { + let scrollLeft = scrollHead.scrollLeft(); + scrollBody.scrollLeft(scrollLeft); + scrollFoot.scrollLeft(scrollLeft); + }); + scrollFoot.on('scroll.DT', () => { + let scrollLeft = scrollFoot.scrollLeft(); + scrollHead.scrollLeft(scrollLeft); + scrollBody.scrollLeft(scrollLeft); + }); + scrollBody.css('max-height', size(scrollY)); + if (!scroll.collapse) { + scrollBody.css('height', size(scrollY)); + } + settings.scrollHead = scrollHead; + settings.scrollBody = scrollBody; + settings.scrollFoot = scrollFoot; + // On redraw - align columns + settings.callbacks.draw.push(scrollDraw); + // Aria roles - because we break the table up into parts we need to be very + // explicit with the roles to create the accessability tree for the table, + // otherwise browser's attempt to "fix" the tree by filling in what it + // thinks are gaps. The static elements that we can assign roles to are done + // here. Dynamic ones are done in the draw function below. + table.attr('role', 'none'); + table.find('tbody').attr('role', 'rowgroup'); + tableCloneHeader.attr('role', 'none'); + tableCloneFooter.attr('role', 'none'); + settings.colgroup.find('colgroup').attr('role', 'none'); + // Move the info feature's aria desc by to the new "table" + let describedBy = table.attr('aria-describedby'); + if (describedBy) { + scroller.attr('aria-describedby', describedBy); + table.attrRemove('aria-describedby'); + } + return scroller.get(0); } -var __mlWarning = false; -var __luxon; -var __moment; /** + * Update the header, footer and body tables for resizing - i.e. column + * alignment. + * + * Welcome to the most horrible function DataTables. The process that this + * function follows is basically: + * 1. Re-create the table inside the scrolling div + * 2. Correct colgroup > col values if needed + * 3. Copy colgroup > col over to header and footer + * 4. Clean up * + * @param settings DataTables settings object */ -function resolveWindowLibs() { - __luxon = util.external('luxon'); - __moment = util.external('moment'); -} -function __mldObj(d, format, locale) { - var dt; - resolveWindowLibs(); - if (__moment) { - dt = __moment(d, format, locale, true); - if (!dt.isValid()) { - return null; - } - } - else if (__luxon) { - dt = - format && typeof d === 'string' - ? __luxon.DateTime.fromFormat(d, format) - : __luxon.DateTime.fromISO(d); - if (!dt.isValid) { - return null; - } - dt = dt.setLocale(locale); - } - else if (!format) { - // No format given, must be ISO - dt = new Date(d); +function scrollDraw(settings) { + // Given that this is such a monster function, a lot of variables are use + // to try and keep the minimised size as small as possible + let scroll = settings.scroll, barWidth = scroll.barWidth, divHeader = settings.scrollHead, divHeaderInner = divHeader.children('div'), divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.scrollBody, divBody = divBodyEl, divFooter = settings.scrollFoot, divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = Dom.s(settings.thead), table = Dom.s(settings.table), footer = Dom.s(settings.tfoot), browser = settings.browser, headerCopy, footerCopy; + // If the scrollbar visibility has changed from the last draw, we need to + // adjust the column sizes as the table width will have changed to account + // for the scrollbar + let scrollBarVis = divBodyEl.get(0).scrollHeight > divBodyEl.get(0).clientHeight; + if (settings.scrollBarVis !== scrollBarVis && + settings.scrollBarVis !== undefined) { + settings.scrollBarVis = scrollBarVis; + adjustColumnSizing(settings); + return; // adjust column sizing will call this function again } else { - if (!__mlWarning) { - alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); - } - __mlWarning = true; + settings.scrollBarVis = scrollBarVis; } - return dt; -} -// Wrapper for date, datetime and time which all operate the same way with the -// exception of the output string for auto locale support -function __mlHelper(localeString) { - return function (from, to, locale, def) { - // Luxon and Moment support - // Argument shifting - if (arguments.length === 0) { - locale = 'en'; - to = null; // means toLocaleString - from = null; // means iso8601 - } - else if (arguments.length === 1) { - locale = 'en'; - to = from; - from = null; - } - else if (arguments.length === 2) { - locale = to; - to = from; - from = null; - } - var typeName = 'datetime' + (to ? '-' + to : ''); - // Add type detection and sorting specific to this date format - we need - // to be able to identify date type columns as such, rather than as - // numbers in extensions. Hence the need for this. - if (!store.order[typeName + '-pre']) { - register$1(typeName, { - detect: function (d) { - // The renderer will give the value to type detect as the - // type! - return d === typeName ? typeName : false; - }, - order: { - pre: function (d) { - // The renderer gives us Moment, Luxon or Date objects - // for the sorting, all of which have a `valueOf` which - // gives milliseconds epoch - return d.valueOf(); - } + header.find('thead').attr('role', 'rowgroup'); + footer.find('tfoot').attr('role', 'rowgroup'); + // 1. Re-create the table inside the scrolling div + // Remove the old minimised thead and tfoot elements in the inner table + table.children('thead, tfoot').remove(); + // Clone the current header and footer elements and then place it into the + // inner table + headerCopy = header.clone(true).prependTo(table); + headerCopy.find('th, td').attrRemove('tabindex'); + headerCopy.find('[id]').attrRemove('id'); + if (footer.count()) { + footerCopy = footer.clone(true).prependTo(table); + footerCopy.find('[id]').attrRemove('id'); + } + // 2. Correct colgroup > col values if needed + // It is possible that the cell sizes are smaller than the content, so we need to + // correct colgroup>col for such cases. This can happen if the auto width detection + // uses a cell which has a longer string, but isn't the widest! For example + // "Chief Executive Officer (CEO)" is the longest string in the demo, but + // "Systems Administrator" is actually the widest string since it doesn't collapse. + // Note the use of translating into a column index to get the `col` element. This + // is because of Responsive which might remove `col` elements, knocking the alignment + // of the indexes out. + if (settings.display.length) { + // Get the column sizes from the first row in the table. This should really be a + // [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year + // browser support + let firstTr = null; + let start = dataSource(settings) !== 'ssp' ? settings.displayStart : 0; + for (let i = start; i < start + settings.display.length; i++) { + let idx = settings.display[i]; + let row = settings.data[idx]; + if (row) { + let tr = row.tr; + if (tr) { + firstTr = tr; + break; } - }); - } - if (!store.className[typeName]) { - store.className[typeName] = 'dt-right'; + } } - return function (d, type) { - // Allow for a default value - if (d === null || d === undefined) { - if (def === '--now') { - // We treat everything as UTC further down, so no changes - // are made, as such need to get the local date / time as if - // it were UTC - var local = new Date(); - d = new Date(Date.UTC(local.getFullYear(), local.getMonth(), local.getDate(), local.getHours(), local.getMinutes(), local.getSeconds())); - } - else { - d = ''; + if (firstTr) { + let colSizes = Dom.s(firstTr) + .children('th, td') + .mapTo(function (cell, idx) { + return { + idx: visibleToColumnIndex(settings, idx), + width: Dom.s(cell).width('outer') + }; + }); + // Check against what the colgroup > col is set to and correct if needed + for (let i = 0; i < colSizes.length; i++) { + let colEl = settings.columns[colSizes[i].idx].colEl; + colEl.css('width', colSizes[i].width + 'px'); + if (scroll.x) { + colEl.css('minWidth', colSizes[i].width + 'px'); } } - if (type === 'type') { - // Typing uses the type name for fast matching - return typeName; - } - if (d === '') { - return type !== 'sort' - ? '' - : __mldObj('0000-01-01 00:00:00', null, locale); - } - // Shortcut. If `from` and `to` are the same, we are using the - // renderer to format for ordering, not display - its already in the - // display format. - if (to !== null && - from === to && - type !== 'sort' && - type !== 'type' && - !(d instanceof Date)) { - return d; - } - // Determine if there is a timezone. If there is, we want to reuse - // it for the output, so the timezone doesn't change between the - // input and output. - let options = {}; - let tzMatch = typeof d === 'string' ? d.match(util.regex.isoTimezone) : null; - if (tzMatch) { - options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1]; - } - // Get a Date object (Luxon, moment or Date) - var dt = __mldObj(d, from, locale); - if (dt === null) { - return d; - } - if (type === 'sort') { - return dt; - } - var formatted = to === null - ? __mld(dt, 'toDate', 'toJSDate', '')[localeString](navigator.language, options) - : __mld(dt, 'format', 'toFormat', 'toISOString', to); - // XSS protection - return type === 'display' ? util.escapeHtml(formatted) : formatted; - }; - }; -} -// Based on locale, determine standard number formatting -// Fallback for legacy browsers is US English -var __thousands = ','; -var __decimal = '.'; -if (window.Intl !== undefined) { - try { - var num = new Intl.NumberFormat().formatToParts(100000.1); - for (var i = 0; i < num.length; i++) { - if (num[i].type === 'group') { - __thousands = num[i].value; - } - else if (num[i].type === 'decimal') { - __decimal = num[i].value; - } } } - catch (e) { - // noop - } -} -/** - * Register a date / time format for DataTables to use. - * - * @param format The date / time format to detect data in. Please refer to the - * Moment.js or Luxon document for the full list of tokens, depending on which - * of the two libraries you are using. - * @param locale The locale to pass to Moment.js / Luxon. - */ -function datetime(format, locale) { - var typeName = 'datetime-' + format; - if (!locale) { - locale = 'en'; + // 3. Copy the colgroup over to the header and footer + divHeaderTable.find('colgroup').remove(); + divHeaderTable.append(settings.colgroup.clone(true)); + if (footer) { + divFooterTable.find('colgroup').remove(); + divFooterTable.append(settings.colgroup.clone(true)); } - if (!store.order[typeName]) { - register$1(typeName, { - detect: function (d) { - var dt = __mldObj(d, format, locale); - return d === '' || dt ? typeName : false; - }, - order: { - pre: function (d) { - return __mldObj(d, format, locale) || 0; - } - } + // "Hide" the header and footer that we used for the sizing. We need to keep + // the content of the cell so that the width applied to the header and body + // both match, but we want to hide it completely. + headerCopy.find('th, td').each(function (el) { + Dom.c('div') + .classAdd('dt-scroll-sizing') + .append(Array.from(el.childNodes)) + .appendTo(el); + }); + if (footerCopy) { + footerCopy.find('th, td').each(function (el) { + Dom.c('div') + .classAdd('dt-scroll-sizing') + .append(Array.from(el.childNodes)) + .appendTo(el); }); } - if (!store.className[typeName]) { - store.className[typeName] = 'dt-right'; + // 4. Clean up + // Figure out if there are scrollbar present - if so then we need the header and footer to + // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) + let isScrolling = Math.floor(table.height()) > divBodyEl.get(0).clientHeight || + divBody.css('overflow-y') == 'scroll'; + let paddingSide = 'padding' + (browser.scrollbarLeft ? 'Left' : 'Right'); + // Set the width's of the header and footer tables + let outerWidth = table.width('withPadding'); + divHeaderTable.css('width', stringToCss(outerWidth)); + divHeaderInner + .css('width', stringToCss(outerWidth)) + .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + if (footer.count()) { + divFooterTable.css('width', stringToCss(outerWidth)); + divFooterInner + .css('width', stringToCss(outerWidth)) + .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + } + // Correct DOM ordering for colgroup - comes before the thead + table.children('colgroup').prependTo(table); + // Remove tabindex from the hidden row elements + table.find('thead, tfoot').find('[tabindex]').attrRemove('tabindex'); + // Dynamic ARIA roles - see setup for details on why this is needed + table + .find('thead, tfoot') + .attr('role', 'none') + .find('[role]') + .attrRemove('role'); + table.find('tbody tr:not([role])').attr('role', 'row'); + table.find('tbody td:not([role]), tbody th:not([role])').attr('role', 'cell'); + scrollAria(headerCopy); + scrollAria(footerCopy); + // Adjust the position of the header in case we loose the y-scrollbar + divBody.trigger('scroll'); + // If sorting or filtering has occurred, jump the scrolling back to the top + // only if we aren't holding the position + if ((settings.wasOrdered || settings.wasFiltered) && !settings.drawHold) { + divBodyEl.scrollTop(0); } } /** - * Helpers for `columns.render`. - */ -var helpers = { - date: __mlHelper('toLocaleDateString'), - datetime: __mlHelper('toLocaleString'), - time: __mlHelper('toLocaleTimeString'), - number: function (thousands, decimal, precision, prefix, postfix) { - // Auto locale detection - if (thousands === null || thousands === undefined) { - thousands = __thousands; - } - if (decimal === null || decimal === undefined) { - decimal = __decimal; - } - return { - display: function (d) { - if (typeof d !== 'number' && typeof d !== 'string') { - return d; - } - if (d === '' || d === null) { - return d; - } - var flo = typeof d === 'number' ? d : parseFloat(d); - var negative = flo < 0 ? '-' : ''; - var abs = Math.abs(flo); - // Scientific notation for large and small numbers - if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0)) { - var exp = flo.toExponential(precision).split(/e\+?/); - return exp[0] + ' x 10' + exp[1] + ''; - } - // If NaN then there isn't much formatting that we can do - just - // return immediately, escaping any HTML (this was supposed to - // be a number after all) - if (isNaN(flo)) { - return util.escapeHtml(d); - } - flo = flo.toFixed(precision); - var absPart = Math.abs(flo); - var intPart = Math.abs(parseInt(flo, 10)); - var floatPart = precision - ? decimal + - (absPart - intPart).toFixed(precision).substring(2) - : ''; - // If zero, then can't have a negative prefix - if (intPart === 0 && parseFloat(floatPart) === 0) { - negative = ''; - } - return (negative + - (prefix || '') + - intPart - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + - floatPart + - (postfix || '')); - } - }; - }, - text: function () { - return { - display: util.escapeHtml, - filter: util.escapeHtml - }; - } -}; - -/** - * Column options that can be given to DataTables at initialisation time. - */ -const defaults$4 = { - ariaTitle: '', - cellType: 'td', - className: '', - contentPadding: '', - createdCell: null, - data: null, - defaultContent: null, - footer: null, - name: '', - orderable: true, - orderData: null, - orderDataType: 'std', - orderSequence: ['asc', 'desc', ''], - render: null, - search: null, - searchable: true, - title: null, - type: null, - visible: true, - width: null -}; - -/** - * Internal settings object used for individual columns. Instances are held in - * the setting object's `columns` array and contains all the information that - * DataTables needs about each individual column. - * - * Note that this object is related to the column defaults but this one is the - * internal data store for DataTables's cache of columns. It should NOT be - * manipulated outside of DataTables. Any configuration should be done through - * the initialisation options. + * Apply ARIA roles for the header / footer of a scrolling table + * @param element */ -class Settings { - constructor() { - /** - * Flag to indicate if HTML5 data attributes should be used as the data - * source for filtering or sorting. True is either are. - */ - this.attrSrc = false; - this.ariaTitle = ''; - /** - * The class to apply to all cells in the table's `tbody`` for the column - */ - this.className = null; - /** - * When DataTables calculates the column widths to assign to each column, it - * finds the longest string in each column and then constructs a temporary - * table and reads the widths from that. The problem with this is that "mmm" - * is much wider then "iiii", but the latter is a longer string - thus the - * calculation can go wrong (doing it properly and putting it into an DOM - * object and measuring that is horribly(!) slow). Thus as a "work around" - * we provide this option. It will append its value to the text that is - * found to be the longest string for the column - i.e. padding. - */ - this.contentPadding = null; - /** - * Property to read the value for the cells in the column from the data - * source array / object. If null, then the default content is used, if a - * function is given then the return from the function is used. - */ - this.data = null; - /** - * Allows a default value to be given for a column's data, and will be used - * whenever a null data source is encountered (this can be because mData is - * set to null, or because the data source itself is null). - */ - this.defaultContent = null; - /** - * Name for the column, allowing reference to the column by name as well as - * by index (needs a lookup to work by name). - */ - this.name = null; - /** - * A list of the columns that sorting should occur on when this column is - * sorted. That this property is an array allows multi-column sorting to be - * defined for a column (for example first name / last name columns would - * benefit from this). The values are integers pointing to the columns to be - * sorted on (typically it will be a single integer pointing at itself, but - * that doesn't need to be the case). - */ - this.orderData = []; - /** - * Custom sorting data type - defines which of the available plug-ins in - * afnSortData the custom sorting will use - if any is defined. - */ - this.orderDataType = 'std'; - /** - * Class to be applied to the header element when sorting on this column - */ - this.orderingClass = null; - /** - * Define the sorting directions that are applied to the column, in sequence - * as the column is repeatedly sorted upon - i.e. the first value is used as - * the sorting direction when the column if first sorted (clicked on). Sort - * it again (click again) and it will move on to the next index. Repeat - * until loop. - */ - this.orderSequence = []; - /** - * Partner property to mData which is used (only when defined) to get the - * data - i.e. it is basically the same as mData, but without the 'set' - * option, and also the data fed to it is the result from mData. This is the - * rendering method to match the data method of mData. - */ - this.render = null; - /** - * Title of the column - what is seen in the TH element (nTh). - */ - this.title = null; - /** - * Store for manual type assignment using the `column.type` option. This - * is held in store so we can manipulate the column's `type` property. - */ - this.typeManual = null; - /** Cached longest strings from a column */ - this.wideStrings = null; - /** - * Width of the column - */ - this.width = null; - /** - * Width of the column when it was first "encountered" - */ - this.widthOrig = null; +function scrollAria(element) { + if (element) { + element.find('tfoot:not([role])').attr('role', 'rowgroup'); + element.find('tr:not([role])').attr('role', 'row'); + element.find('th:not([role])').attr('role', 'columnheader'); + element.find('td:not([role])').attr('role', 'cell'); } } -const defaults$3 = { - boundary: false, - caseInsensitive: true, - columns: null, - exact: false, - regex: false, - return: false, - search: '', - smart: true -}; /** - * Create a new search options object + * Add the options to the page HTML for the table * - * @param parts Values to assign, otherwise the defaults will be used - * @returns New object + * @param ctx DataTables context */ -function create$2(parts = {}) { - return util.object.assignDeep({}, defaults$3, parts); +function createLayout(ctx) { + var classes = ctx.classes; + // Wrapper div around everything DataTables controls + var insert = Dom + .c('div') + .attr('id', ctx.tableId + '_wrapper') + .classAdd(classes.container) + .insertBefore(ctx.table); + ctx.tableWrapper = insert.get(0); + if (ctx.dom) { + // Legacy + legacyDom(ctx, ctx.dom, insert); + } + else { + var top = convert(ctx, ctx.layout, 'top'); + var bottom = convert(ctx, ctx.layout, 'bottom'); + var render = renderer(ctx, 'layout'); + // Everything above - the renderer will actually insert the contents into the document + top.forEach(function (item) { + render(ctx, insert, item); + }); + // The table - always the center of attention + render(ctx, insert, { + full: { + contents: [featureTable(ctx)], + items: [], + table: true + } + }); + // Everything below + bottom.forEach(function (item) { + render(ctx, insert, item); + }); + } + // Processing floats on top, so it isn't an inserted feature + processingHtml(ctx); } - -const browser = { - barWidth: -1, - scrollbarLeft: false -}; -const hungarianToCamelRe = /^(a|aa|ai|ao|as|b|fn|i|m|o|s)([A-Z])([a-z].*$)/; /** - * Take an object which has hungarian notation parameters and convert them to - * camelCase style. This is to allow compatibility with DataTables 1.9 and - * earlier which only used hungarian notation, and also with DataTables 1.10 - 2 - * which allowed it to be used. + * Expand the layout items into an object for the rendering function */ -function hungarianToCamel(user) { - if (!user) { - return user; +function layoutItems(row, align, items) { + if (Array.isArray(items)) { + for (var i = 0; i < items.length; i++) { + layoutItems(row, align, items[i]); + } + return; } - let userKeys = Object.keys(user); - let userAny = user; - for (let i = 0; i < userKeys.length; i++) { - let userKey = userKeys[i]; - let match = userKey.match(hungarianToCamelRe); - // Is the key in hungarian notation? - if (match) { - // If so map it down - user[match[2].toLowerCase() + match[3]] = userAny[userKey]; + var rowCell = row[align]; // can't be undefined - will have been created by getRow + // If it is an object, then there can be multiple features contained in it + if (util.is.plainObject(items)) { + // Is it an cell object already, with rowId, etc. A feature plugin cannot + // be named "features" due to this check + if (items.features) { + if (items.rowId) { + row.id = items.rowId; + } + if (items.rowClass) { + row.className = items.rowClass; + } + rowCell.id = items.id; + rowCell.className = items.className; + layoutItems(row, align, items.features); } - // Recurse down through the object - if (util.is.plainObject(userAny[userKey])) { - hungarianToCamel(userAny[userKey]); + else { + // An object of features and configuration options - e.g. `{paging: {startEnd: false}}` + util.object.each(items, (key, val) => { + rowCell.items.push({ + feature: key, + opts: val + }); + }); } } - return user; + else { + // Otherwise, it is a function, node or Dom / jQuery instance and can just get added + rowCell.items.push(items); + } } /** - * Map one parameter onto another + * Find, or create a layout row and setup a target cell in it * - * @param o Object to map - * @param newKey The new parameter name - * @param oldKey The old parameter name + * @param rows Rows array to search for the target row. Is mutated when a row is + * added if not found. + * @param rowNum Row index to get + * @param align Where the cell position is + * @returns The row */ -function compatMap(o, newKey, oldKey) { - if (o[oldKey] !== undefined) { - o[newKey] = o[oldKey]; +function getRow(rows, rowNum, align) { + var row; + // Find existing rows + for (var i = 0; i < rows.length; i++) { + row = rows[i]; + if (row.rowNum === rowNum) { + // full is on its own, but start and end share a row + if ((align === 'full' && row.full) || + ((align === 'start' || align === 'end') && + (row.start || row.end))) { + if (!row[align]) { + row[align] = { + contents: [], + items: [] + }; + } + return row; + } + } } + // If we get this far, then there was no match, create a new row + row = { + rowNum: rowNum + }; + row[align] = { + contents: [], + items: [] + }; + rows.push(row); + return row; } /** - * Provide backwards compatibility for the main DT options. Note that the new - * options are mapped onto the old parameters, so this is an external interface - * change only. + * Convert a `layout` object given by a user to the object structure needed + * for the renderer. This is done twice, once for above and once for below + * the table. Ordering must also be considered. * - * @param init Object to map + * @param settings DataTables settings object + * @param layout Layout object to convert + * @param side `top` or `bottom` + * @returns Converted array structure - one item for each row. */ -function compatOpts(init) { - // Convert any old style parameters to camelCase - hungarianToCamel(init); - // Map old parameter names to new - compatMap(init, 'ordering', 'sort'); - compatMap(init, 'orderMulti', 'sortMulti'); - compatMap(init, 'orderClasses', 'sortClasses'); - compatMap(init, 'orderCellsTop', 'sortCellsTop'); - compatMap(init, 'order', 'sorting'); - compatMap(init, 'orderFixed', 'sortingFixed'); - compatMap(init, 'paging', 'paginate'); - compatMap(init, 'pagingType', 'paginationType'); - compatMap(init, 'pageLength', 'displayLength'); - compatMap(init, 'searching', 'filter'); - compatMap(init, 'stateDuration', 'cookieDuration'); - // Boolean initialisation of x-scrolling - if (typeof init.scrollX === 'boolean') { - init.scrollX = init.scrollX ? '100%' : ''; - } - // Objects for ordering - if (typeof init.ordering === 'object') { - init.orderIndicators = - init.ordering.indicators !== undefined - ? init.ordering.indicators - : true; - init.orderHandler = - init.ordering.handler !== undefined ? init.ordering.handler : true; - init.ordering = true; - } - else if (init.ordering === false) { - init.orderIndicators = false; - init.orderHandler = false; - } - else if (init.ordering === true) { - init.orderIndicators = true; - init.orderHandler = true; - } - // Which cells are the title cells? - if (typeof init.orderCellsTop === 'boolean') { - init.titleRow = init.orderCellsTop; - } - // Column search objects are in an array, so it needs to be converted - // element by element - var searchCols = init.searchCols; - if (searchCols) { - for (var i = 0, iLen = searchCols.length; i < iLen; i++) { - if (searchCols[i]) { - hungarianToCamel(searchCols[i]); - } +function convert(settings, layout, side) { + var rows = []; + // Split out into an array + util.object.each(layout, function (pos, items) { + var parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/); + if (items === null || !parts) { + return; } + var rowNum = parts[2] ? parseInt(parts[2]) : 0; + var align = parts[3] ? parts[3].toLowerCase() : 'full'; + // Filter out the side we aren't interested in + if (parts[1] !== side) { + return; + } + // Only really a type check + if (align !== 'full' && align !== 'start' && align !== 'end') { + return; + } + // Get or create the row we should attach to + var row = getRow(rows, rowNum, align); + layoutItems(row, align, items); + }); + // Order by item identifier + rows.sort(function (a, b) { + var order1 = a.rowNum || 0; + var order2 = b.rowNum || 0; + // If both in the same row, then the row with `full` comes first + if (order1 === order2) { + var ret = a.full && !b.full ? -1 : 1; + return side === 'bottom' ? ret * -1 : ret; + } + return order2 - order1; + }); + // Invert for below the table + if (side === 'bottom') { + rows.reverse(); } - // Enable search delay if server-side processing is enabled - if (init.serverSide && !init.searchDelay) { - init.searchDelay = 400; - } - // Language - if (init.language && init.language.url && !init.language.ajax) { - init.language.ajax = init.language.url; + for (var row = 0; row < rows.length; row++) { + delete rows[row].rowNum; + resolve(settings, rows[row]); } + return rows; +} +/** + * Convert the contents of a row's layout object to nodes that can be inserted + * into the document by a renderer. Execute functions, look up plug-ins, etc. + * + * @param settings DataTables settings object + * @param row Layout object for this row + */ +function resolve(settings, row) { + var getFeature = function (feature, opts) { + if (!ext.features[feature]) { + log(settings, 0, 'Unknown feature: ' + feature); + } + return ext.features[feature].apply(this, [settings, opts]); + }; + // Resolve items in the `contents` array from being an identifier, such as + // the name of a feature, into the node to display. + var resolve = function (item) { + if (!row[item]) { + return; + } + row[item].contents = row[item].items + .filter(item => !!item) + .map(item => { + if (typeof item === 'string') { + return getFeature(item, null); + } + else if (util.is.plainObject(item)) { + // If it's an object, it just has feature and opts properties from + // the transform in _layoutArray + return getFeature(item.feature, item.opts); + } + else if (typeof item.node === 'function') { + return item.node(settings); + } + else if (typeof item === 'function') { + var inst = item(settings); + return typeof inst.node === 'function' ? inst.node() : inst; + } + else if (item.nodeName) { + // An HTML element + return item; + } + else if (item instanceof Dom) { + return item.get(0); + } + else if (item.length) { + // Possibly jQuery + return item[0]; + } + }); + }; + resolve('start'); + resolve('end'); + resolve('full'); } /** - * Provide backwards compatibility for column options. Note that the new options - * are mapped onto the old parameters, so this is an external interface change - * only. + * Draw the table with the legacy DOM property * - * @param init Object to map + * @param settings DT settings instance + * @param layout DOM string + * @param insert Insert point */ -function compatCols(init) { - // Convert any old style parameters to camelCase - hungarianToCamel(init); - // typeof columnDefaults - compatMap(init, 'orderable', 'sortable'); - compatMap(init, 'orderData', 'dataSort'); - compatMap(init, 'orderSequence', 'sorting'); - compatMap(init, 'orderDataType', 'sortDataType'); - compatMap(init, 'className', 'class'); - // orderData can be given as an integer - var dataSort = init.aDataSort; - var orderData = init.orderData; - if (typeof dataSort === 'number') { - init.orderData = [dataSort]; - } - if (typeof orderData === 'number') { - init.orderData = [orderData]; - } - // Backwards compatibility for mDataProp from 1.9- - if (init.dataProp !== undefined && !init.data) { - init.data = init.dataProp; +function legacyDom(settings, layout, insert) { + let parts = layout.match(/(".*?")|('.*?')|./g); + let featureNode, option, newNode, next, attr; + if (!parts) { + return; } -} -/** - * Browser feature detection for capabilities, quirks - * - * @param ctx DataTables settings object - */ -function browserDetect(ctx) { - // We don't need to do this every time DataTables is constructed, the values - // calculated are specific to the browser and OS configuration which we - // don't expect to change between initialisations - if (browser.barWidth === -1) { - // Scrolling feature / quirks detection - var n = Dom - .c('div') - .css({ - position: 'fixed', - top: '0', - left: -1 * window.pageXOffset + 'px', // allow for scrolling - height: '1px', - width: '1px', - overflow: 'hidden' - }) - .append(Dom - .c('div') - .css({ - position: 'absolute', - top: '1px', - left: '1px', - width: '100px', - overflow: 'scroll' - }) - .append(Dom.c('div').css({ - width: '100%', - height: '10px' - }))) - .appendTo('body'); - var outer = n.children(); - var inner = outer.children(); - browser.barWidth = outer.get(0).offsetWidth - outer.get(0).clientWidth; - browser.scrollbarLeft = Math.round(inner.offset().left) !== 1; - n.remove(); + for (let i = 0; i < parts.length; i++) { + featureNode = null; + option = parts[i]; + if (option == '<') { + // New container div + newNode = Dom.c('div'); + // Check to see if we should append an id and/or a class name to the container + next = parts[i + 1]; + if (next[0] == "'" || next[0] == '"') { + attr = next.replace(/['"]/g, ''); + let id = '', className; + /* The attribute can be in the format of "#id.class", "#id" or "class" This logic + * breaks the string into parts and applies them as needed + */ + if (attr.indexOf('.') != -1) { + let split = attr.split('.'); + id = split[0]; + className = split[1]; + } + else if (attr[0] == '#') { + id = attr; + } + else { + className = attr; + } + newNode.attr('id', id.substring(1)).classAdd(className); + i++; // Move along the position array + } + insert.append(newNode.get()); // TODO + insert = newNode; + } + else if (option == '>') { + // End container div + insert = insert.parent(); + } + else if (option == 't') { + // Table + featureNode = featureTable(settings); + } + else { + ext.feature.forEach(function (feature) { + if (option == feature.cFeature) { + featureNode = feature.fnInit(settings); + } + }); + } + // Add to the display + if (featureNode) { + // TODO when doing the full dom update, won't need this check + insert.append(featureNode instanceof Dom ? featureNode.get() : featureNode); + } } - Object.assign(ctx.browser, browser); - ctx.scroll.barWidth = browser.barWidth; } -const defaults$2 = { - addedClasses: [], - cells: [], - data: [], - details: undefined, - detailsShow: undefined, - displayData: null, - idx: -1, - orderCache: null, - searchCellCache: null, - searchRowCache: null, - src: 'dom', - tr: null -}; -/** - * Create a new object that is a row model - * - * @param parts Values to assign, otherwise the defaults will be used - * @returns New object - */ -function create$1(parts = {}) { - return util.object.assignDeep({}, defaults$2, parts); +function sortInit(settings) { + var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; + if (settings.orderHandler) { + columnOrderingCells(settings, notSelector) + .each(el => { + sortAttachListener(settings, el, ''); + }); + } + // Need to resolve the user input array into our internal structure + var order = []; + sortResolve(settings, order, settings.order); + settings.order = order; } - /** - * Add a data array to the table, creating DOM node etc. This is the parallel to - * gatherData, but for adding rows from a JavaScript source, rather than a - * DOM source. + * Attach event listeners to a node that will trigger ordering on a column * - * @param settings DataTables settings object - * @param dataIn data array to be added - * @param tr TR element to add to the table - optional. If not given, DataTables - * will create a row automatically - * @param tds Array of TD|TH elements for the row - must be given if tr is. - * @returns >=0 if successful (index of new data entry), -1 if failed + * @param settings DataTables context + * @param node Node to attach to + * @param selector Delegate selector + * @param column Column index to target + * @param callback Callback for when done */ -function addData(settings, dataIn, tr, tds) { - /* Create the object for storing information about this new row */ - var rowIdx = settings.data.length; - var row = create$1({ - src: tr ? 'dom' : 'data', - idx: rowIdx +function sortAttachListener(settings, node, selector, column, callback) { + bindAction(node, selector, function (e) { + var run = false; + var columns = column === undefined + ? columnsFromHeader(e.target) + : typeof column === 'function' + ? column() + : Array.isArray(column) + ? column + : [column]; + if (columns.length) { + for (var i = 0, iLen = columns.length; i < iLen; i++) { + var ret = sortAdd(settings, columns[i], i, e.shiftKey); + if (ret !== false) { + run = true; + } + // If the first entry is no sort, then subsequent + // sort columns are ignored + if (settings.order.length === 1 && + settings.order[0][1] === '') { + break; + } + } + if (run) { + processingRun(settings, true, function () { + sort(settings); + sortDisplay(settings, settings.display); + reDraw(settings, false, false); + if (callback) { + callback(); + } + }); + } + } }); - row.data = dataIn; - settings.data.push(row); - var columns = settings.columns; - for (var i = 0, iLen = columns.length; i < iLen; i++) { - // Invalidate the column types as the new data needs to be revalidated - columns[i].type = null; - } - /* Add to the display array */ - settings.displayMaster.push(rowIdx); - var id = settings.rowIdFn(dataIn); - if (id !== undefined) { - settings.ids[id] = row; - } - /* Create the DOM information, or register it if already present */ - if (tr || !settings.features.deferRender) { - createTr(settings, rowIdx, tr, tds); - } - return rowIdx; } /** - * Add one or more TR elements to the table. Generally we'd expect to - * use this for reading data from a DOM sourced table, but it could be - * used for an TR element. Note that if a TR is given, it is used (i.e. - * it is not cloned). + * Sort the display array to match the master's order * - * @param settings DataTables settings object - * @param rows The TR element(s) to add to the table - * @returns Array of indexes for the added rows + * @param settings DataTables context + * @param display The display array */ -function addTr(settings, rows) { - return rows.mapTo(el => { - let row = getRowElementsFromNode(settings, el); - return addData(settings, row.data, el, row.cells); +function sortDisplay(settings, display) { + if (display.length < 2) { + return; + } + var master = settings.displayMaster; + var masterMap = {}; + var map = {}; + var i; + // Rather than needing an `indexOf` on master array, we can create a map + for (i = 0; i < master.length; i++) { + masterMap[master[i]] = i; + } + // And then cache what would be the indexOf from the display + for (i = 0; i < display.length; i++) { + map[display[i]] = masterMap[display[i]]; + } + display.sort(function (a, b) { + // Short version of this function is simply `master.indexOf(a) - master.indexOf(b);` + return map[a] - map[b]; }); } /** - * Get the data for a given cell from the internal cache, taking into account - * data mapping + * Convert the API variants that can be used for defining the order into our + * internal OrderColumn array. * - * @param settings DataTables settings object - * @param rowIdx data row id - * @param colIdx Column index - * @param type data get type ('display', 'type' 'filter|search' 'sort|order') - * @returns Cell data + * @param settings DataTable context object + * @param nestedSort Array to write the resolve values to + * @param sortItem Source object / array from user (It is really an `Order` + * but due to `aaSorting` being used for input and the internal structure + * it is currently any). + * @todo Split aaSorting into unresolved and resolved parameters (in state.ts as + * well) */ -function getCellData(settings, rowIdx, colIdx, type) { - if (type === 'search') { - type = 'filter'; - } - else if (type === 'order') { - type = 'sort'; - } - var row = settings.data[rowIdx]; - if (!row) { - return undefined; +function sortResolve(settings, nestedSort, sortItem // TODO typing +) { + var push = function (a) { + if (plainObject(a)) { + let orderIdx = a; + let orderName = a; + if (orderIdx.idx !== undefined) { + // Index based ordering + nestedSort.push([orderIdx.idx, orderIdx.dir]); + } + else if (orderName.name) { + // Name based ordering + var cols = pluck(settings.columns, 'name'); + var idx = cols.indexOf(orderName.name); + if (idx !== -1) { + nestedSort.push([idx, orderName.dir]); + } + } + } + else { + // Plain column index and direction pair + nestedSort.push(a); + } + }; + if (plainObject(sortItem)) { + // Object + push(sortItem); } - var draw = settings.drawCount; - var col = settings.columns[colIdx]; - var rowData = row.data; - var defaultContent = col.defaultContent; - var cellData = col.dataGet(rowData, type, { - settings: settings, - row: rowIdx, - col: colIdx - }); - // Allow for a node being returned for non-display types - if (type !== 'display' && - cellData && - typeof cellData === 'object' && - cellData.nodeName) { - cellData = cellData.innerHTML; + else if (Array.isArray(sortItem) && typeof sortItem[0] === 'number') { + // 1D array + push(sortItem); } - if (cellData === undefined) { - if (settings.drawError != draw && defaultContent === null) { - log(settings, 0, 'Requested unknown parameter ' + - (typeof col.data == 'function' - ? '{function}' - : "'" + col.data + "'") + - ' for row ' + - rowIdx + - ', column ' + - colIdx, 4); - settings.drawError = draw; + else if (Array.isArray(sortItem)) { + // 2D array + for (var z = 0; z < sortItem.length; z++) { + push(sortItem[z]); // Object or array } - return defaultContent; } - // When the data source is null and a specific data type is requested (i.e. - // not the original data), we can use default column data - if ((cellData === rowData || cellData === null) && - defaultContent !== null && - type !== undefined) { - cellData = defaultContent; +} +function sortFlatten(settings) { + var i, k, kLen, aSort = [], extSort = ext.type.order, aoColumns = settings.columns, dataSort, colIdx, type, srcCol, fixed = settings.orderFixed, fixedObj = plainObject(fixed), nestedSort = []; + if (!settings.features.ordering) { + return aSort; } - else if (typeof cellData === 'function') { - // If the data source is a function, then we run it and use the return, - // executing in the scope of the data object (for instances) - return cellData.call(rowData); + // Build the sort array, with pre-fix and post-fix options if they have been + // specified + if (Array.isArray(fixed)) { + sortResolve(settings, nestedSort, fixed); } - if (cellData === null && type === 'display') { - return ''; + if (fixedObj && fixed.pre) { + sortResolve(settings, nestedSort, fixed.pre); } - if (type === 'filter') { - var formatters = ext.type.search; - if (col.type && formatters[col.type]) { - cellData = formatters[col.type](cellData); + sortResolve(settings, nestedSort, settings.order); + if (fixedObj && fixed.post) { + sortResolve(settings, nestedSort, fixed.post); + } + for (i = 0; i < nestedSort.length; i++) { + srcCol = nestedSort[i][0]; + if (aoColumns[srcCol]) { + dataSort = aoColumns[srcCol].orderData; + for (k = 0, kLen = dataSort.length; k < kLen; k++) { + colIdx = dataSort[k]; + type = aoColumns[colIdx].type || 'string'; + if (nestedSort[i]._idx === undefined) { + nestedSort[i]._idx = aoColumns[colIdx].orderSequence.indexOf(nestedSort[i][1]); + } + if (nestedSort[i][1]) { + aSort.push({ + src: srcCol, + col: colIdx, + dir: nestedSort[i][1], + index: nestedSort[i]._idx, + type: type, + formatter: extSort[type + '-pre'], + sorter: extSort[type + '-' + nestedSort[i][1]] + }); + } + } } } - return cellData; + return aSort; } /** - * Set the value for a specific cell, into the internal data cache + * Change the order of the table * - * @param settings DataTables settings object - * @param rowIdx data row id - * @param colIdx Column index - * @param val Value to set + * @param ctx DataTables settings object + * @param col Column to perform sort on + * @param dir Direction to sort on */ -function setCellData(settings, rowIdx, colIdx, val) { - let row = settings.data[rowIdx]; - if (row) { - let col = settings.columns[colIdx]; - let rowData = row.data; - col.dataSet(rowData, val, { - settings: settings, - row: rowIdx, - col: colIdx +function sort(ctx, col, dir) { + var i, iLen, aiOrig = [], extSort = ext.type.order, data = ctx.data, sortCol, displayMaster = ctx.displayMaster, aSort; + // Make sure the columns all have types defined + columnTypes(ctx); + // Allow a specific column to be sorted, which will _not_ alter the display + // master + if (col !== undefined) { + var srcCol = ctx.columns[col]; + aSort = [ + { + src: col, + col: col, + dir: dir || '', + index: 0, + type: srcCol.type, + formatter: extSort[srcCol.type + '-pre'], + sorter: extSort[srcCol.type + '-' + dir] + } + ]; + displayMaster = displayMaster.slice(); + } + else { + aSort = sortFlatten(ctx); + } + for (i = 0, iLen = aSort.length; i < iLen; i++) { + sortCol = aSort[i]; + // Load the data needed for the sort, for each cell + sortData(ctx, sortCol.col); + } + /* No sorting required if server-side or no sorting array */ + if (dataSource(ctx) != 'ssp' && aSort.length !== 0) { + // Reset the initial positions on each pass so we get a stable sort + for (i = 0, iLen = displayMaster.length; i < iLen; i++) { + aiOrig[i] = i; + } + // If the first sort is desc, then reverse the array to preserve original + // order, just in reverse + if (aSort.length && aSort[0].dir === 'desc' && ctx.orderDescReverse) { + aiOrig.reverse(); + } + /* Do the sort - here we want multi-column sorting based on a given data source (column) + * and sorting function (from oSort) in a certain direction. It's reasonably complex to + * follow on its own, but this is what we want (example two column sorting): + * fnLocalSorting = function(a,b){ + * var test; + * test = oSort['string-asc']('data11', 'data12'); + * if (test !== 0) + * return test; + * test = oSort['numeric-desc']('data21', 'data22'); + * if (test !== 0) + * return test; + * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); + * } + * Basically we have a test for each sorting column, if the data in that column is equal, + * test the next column. If all columns match, then we use a numeric sort on the row + * positions in the original data array to provide a stable sort. + */ + displayMaster.sort(function (a, b) { + var _a, _b; + var x, y, k, test, sortItem, len = aSort.length, dataA = (_a = data[a]) === null || _a === void 0 ? void 0 : _a.orderCache, dataB = (_b = data[b]) === null || _b === void 0 ? void 0 : _b.orderCache; + for (k = 0; k < len; k++) { + sortItem = aSort[k]; + // Data, which may have already been through a `-pre` function + x = dataA[sortItem.col]; + y = dataB[sortItem.col]; + if (sortItem.sorter) { + // If there is a custom sorter (`-asc` or `-desc`) for this + // data type, use it + test = sortItem.sorter(x, y); + if (test !== 0) { + return test; + } + } + else { + // Otherwise, use generic sorting + test = x < y ? -1 : x > y ? 1 : 0; + if (test !== 0) { + return sortItem.dir === 'asc' ? test : -test; + } + } + } + x = aiOrig[a]; + y = aiOrig[b]; + return x < y ? -1 : x > y ? 1 : 0; }); } -} -/** - * Write a value to a cell - * - * @param td Cell - * @param val Value - */ -function writeCell(td, val) { - let cell = Dom.s(td); - if (val && typeof val === 'object' && val.nodeName) { - cell.empty().append(val); + else if (aSort.length === 0) { + // Apply index order + displayMaster.sort(function (x, y) { + return x < y ? -1 : x > y ? 1 : 0; + }); } - else { - cell.html(val); + if (col === undefined) { + // Tell the draw function that we have sorted the data + ctx.wasOrdered = true; + ctx.sortDetails = aSort; + callbackFire(ctx, null, 'order', [ctx, aSort]); } + return displayMaster; } /** - * Return an array with the full table data - * - * @param settings DataTables settings object - * @returns array {array} aData Master data array - */ -function getDataMaster(settings) { - return util.array.pluck(settings.data, 'data'); -} -/** - * Nuke the table - * - * @param settings DataTables settings object - */ -function clearTable(settings) { - settings.data.length = 0; - settings.displayMaster.length = 0; - settings.display.length = 0; - settings.ids = {}; -} -/** - * Mark cached data as invalid such that a re-read of the data will occur when - * the cached data is next requested. Also update from the data source object. + * Function to run on user sort request * - * @param settings DataTables settings object - * @param rowIdx Row index to invalidate - * @param src Source to invalidate from: undefined, 'auto', 'dom' or 'data' - * @param colIdx Column index to invalidate. If undefined the whole row will be - * invalidated + * @param settings dataTables settings object + * @param colIdx column sorting index + * @param addIndex Counter + * @param shift Shift click add */ -function invalidateRow(settings, rowIdx, src, colIdx) { - var row = settings.data[rowIdx]; - var i, iLen; - if (!row) { - return; +function sortAdd(settings, colIdx, addIndex, shift) { + var col = settings.columns[colIdx]; + var sorting = settings.order; + var asSorting = col.orderSequence; + var nextSortIdx; + var next = function (a, overflow) { + var idx = a._idx; + if (idx === undefined) { + idx = asSorting.indexOf(a[1]); + } + return idx + 1 < asSorting.length ? idx + 1 : overflow ? null : 0; + }; + if (!col.orderable) { + return false; } - // Remove the cached data for the row - row.orderCache = null; - row.searchCellCache = null; - row.displayData = null; - // Are we reading last data from DOM or the data object? - if (src === 'dom' || ((!src || src === 'auto') && row.src === 'dom')) { - // Read the data from the DOM - row.data = getRowElementsFromModel(settings, row, colIdx).data; + // Convert to 2D array if needed + if (typeof sorting[0] === 'number') { + sorting = settings.order = [sorting]; } - else { - // Reading from data object, update the DOM - var cells = row.cells; - var display = getRowDisplay(settings, rowIdx); - if (cells.length) { - if (colIdx !== undefined) { - writeCell(cells[colIdx], display[colIdx]); + // If appending the sort then we are multi-column sorting + if ((shift || addIndex) && settings.features.orderMulti) { + // Are we already doing some kind of sort on this column? + var sortIdx = pluck(sorting, '0').indexOf(colIdx); + if (sortIdx !== -1) { + // Yes, modify the sort + nextSortIdx = next(sorting[sortIdx], true); + if (nextSortIdx === null && sorting.length === 1) { + nextSortIdx = 0; // can't remove sorting completely + } + if (nextSortIdx === null || asSorting[nextSortIdx] === '') { + sorting.splice(sortIdx, 1); } else { - for (i = 0, iLen = cells.length; i < iLen; i++) { - writeCell(cells[i], display[i]); - } + sorting[sortIdx][1] = asSorting[nextSortIdx]; + sorting[sortIdx]._idx = nextSortIdx; } } + else if (shift) { + // No sort on this column yet, being added by shift click + // add it as itself + sorting.push([colIdx, asSorting[0], 0]); + sorting[sorting.length - 1]._idx = 0; + } + else { + // No sort on this column yet, being added from a colspan + // so add with same direction as first column + sorting.push([colIdx, sorting[0][1], 0]); + sorting[sorting.length - 1]._idx = 0; + } } - invalidColumn(settings, colIdx); - // Update DataTables special `DT_*` attributes for the row - rowAttributes(settings, row); - callbackFire(settings, null, 'rowInvalidate', [settings, rowIdx, colIdx], false); -} -/** - * Column specific invalidation - * - * @param settings DataTables settings object - * @param colIdx Column index to invalidate, or all columns if not given - */ -function invalidColumn(settings, colIdx) { - // Column specific invalidation - var cols = settings.columns; - if (colIdx !== undefined) { - // Type - the data might have changed - cols[colIdx].type = null; - // Max length string. Its a fairly cheep recalculation, so not worth - // something more complicated - cols[colIdx].wideStrings = null; - } - else { - for (let i = 0, iLen = cols.length; i < iLen; i++) { - cols[i].type = null; - cols[i].wideStrings = null; + else if (sorting.length && sorting[0][0] == colIdx) { + // Single column - already sorting on this column, modify the sort + nextSortIdx = next(sorting[0]); + if (nextSortIdx) { + sorting.length = 1; + sorting[0][1] = asSorting[nextSortIdx]; + sorting[0]._idx = nextSortIdx; + } + else { + sorting.length = 1; + sorting[0][1] = asSorting[0]; + sorting[0]._idx = 0; } } - settings.containerWidth = -1; -} -/** - * Get the cells and data for a given row - from a element - * - * @param settings DataTables settings object - * @param row TR element from which to read data or existing row object from - * which to re-read the data from the cells - */ -function getRowElementsFromNode(settings, row) { - let data = settings.rowReadObject ? {} : []; - let cells = Dom.s(row).children('th, td'); - let id = row.getAttribute('id'); - cells.each((el, idx) => { - readCellData(settings, el, data, idx); - }); - if (id) { - util.set(settings.rowId)(data, id); + else { + // Single column - sort only on this column + sorting.length = 0; + sorting.push([colIdx, asSorting[0]]); + sorting[0]._idx = 0; } - return { - data: data, - cells: cells.get() - }; } /** - * Get the cells and data for a given row - from an existing row model + * Set the sorting classes on table's body, Note: it is safe to call this function + * when bSort and bSortClasses are false * * @param settings DataTables settings object - * @param row Existing row object from which to re-read the data from the cells - * @param colIdx Optional column index */ -function getRowElementsFromModel(settings, row, colIdx) { - let tds = row.cells; - for (let i = 0; i < tds.length; i++) { - if (colIdx === undefined || colIdx === i) { - readCellData(settings, tds[i], row.data, i); +function sortingClasses(settings) { + var oldSort = settings.lastOrder; + var sortClass = settings.classes.order.position; + var sortFlat = sortFlatten(settings); + var features = settings.features; + var i, iLen, colIdx; + if (features.ordering && features.orderClasses) { + // Remove old sorting classes + for (i = 0, iLen = oldSort.length; i < iLen; i++) { + colIdx = oldSort[i].src; + // Remove column sorting + Dom.s(pluck(settings.data, 'cells', colIdx)).classRemove(sortClass + (i < 2 ? i + 1 : 3)); } - } - // Read the ID from the DOM if present - if (row.tr) { - let id = row.tr.getAttribute('id'); - if (id) { - util.set(settings.rowId)(row.data, id); + // Add new column sorting + for (i = 0, iLen = sortFlat.length; i < iLen; i++) { + colIdx = sortFlat[i].src; + Dom.s(pluck(settings.data, 'cells', colIdx)).classAdd(sortClass + (i < 2 ? i + 1 : 3)); } } - return { - data: row.data, - cells: tds - }; + settings.lastOrder = sortFlat; } /** - * Read data from a cell into the data source object + * Get the data to sort a column, be it from cache, fresh (populating the + * cache), or from a sort formatter * * @param settings DataTables settings object - * @param cell The HTML cell element to read from - * @param data Data object / array to store data into - * @param colIdx The column index for the cell + * @param colIdx Column index */ -function readCellData(settings, cell, data, colIdx) { - let column = settings.columns[colIdx]; - let contents = cell.innerHTML.trim(); - if (column.attrSrc) { - // If we are working with attributes from the cell as values - let dataPoint = column.data; - let setter = util.set(dataPoint._); - let attr = function (str, cell) { - if (typeof str === 'string') { - let idx = str.indexOf('@'); - if (idx !== -1) { - let att = str.substring(idx + 1); - let setter = util.set(str); - setter(data, cell.getAttribute(att)); - } - } - }; - setter(data, contents); - attr(dataPoint.sort, cell); - attr(dataPoint.type, cell); - attr(dataPoint.filter, cell); +function sortData(settings, colIdx) { + // Custom sorting function - provided by the sort data type + var column = settings.columns[colIdx]; + var customSort = ext.order[column.orderDataType]; + var customData; + if (customSort) { + customData = customSort.call(settings.instance, settings, colIdx, columnIndexToVisible(settings, colIdx)); } - else { - if (!column.setter) { - // Cache the setter function - column.setter = util.set(column.data); + // Use / populate cache + var row, cellData; + var formatter = ext.type.order[column.type + '-pre']; + var data = settings.data; + for (var rowIdx = 0; rowIdx < data.length; rowIdx++) { + // Sparse array + if (!data[rowIdx]) { + continue; + } + row = data[rowIdx]; + if (row && !row.orderCache) { + row.orderCache = []; + } + if (row && (!row.orderCache[colIdx] || customSort)) { + cellData = customSort + ? customData[rowIdx] // If there was a custom sort function, use data from there + : getCellData(settings, rowIdx, colIdx, 'sort'); + row.orderCache[colIdx] = formatter + ? formatter(cellData, settings) + : cellData; } - column.setter(data, contents); } } +const defaults$3 = { + boundary: false, + caseInsensitive: true, + columns: null, + exact: false, + regex: false, + return: false, + search: '', + smart: true +}; /** - * Recalculate the column widths, if needed (by a column having been - * invalidated) + * Create a new search options object * - * @param settings DataTables settings object + * @param parts Values to assign, otherwise the defaults will be used + * @returns New object */ -function columnWidths(settings) { - if (settings.columns.map(c => c.wideStrings).includes(null)) { - calculateColumnWidths(settings); - } +function create$1(parts = {}) { + return util.object.assignDeep({}, defaults$3, parts); } + /** - * Calculate the width of columns for the table + * Alter the display settings to change the page * * @param settings DataTables settings object + * @param action Paging action to take: "first", "previous", "next" or "last" or + * page number to jump to (integer) + * @param redraw Automatically draw the update or not + * @returns true page has changed, false - no change */ -function calculateColumnWidths(settings) { - // Not interested in doing column width calculation if auto-width is disabled - if (!settings.features.autoWidth) { - return; - } - var table = settings.table, columns = settings.columns, scroll = settings.scroll, scrollY = scroll.y, scrollX = scroll.x, visibleColumns = getColumns(settings, 'visible'), tableWidthAttr = table.getAttribute('width'), // from DOM element - tableContainer = table.parentElement, i, j, column, columnIdx; - var styleWidth = table.style.width; - var containerWidth = wrapperWidth(settings); - // Don't re-run for the same width as the last time - if (containerWidth === settings.containerWidth) { - return false; - } - settings.containerWidth = containerWidth; - // If there is no width applied as a CSS style or as an attribute, we assume that - // the width is intended to be 100%, which is usually is in CSS, but it is very - // difficult to correctly parse the rules to get the final result. - if (!styleWidth && !tableWidthAttr) { - table.style.width = '100%'; - styleWidth = '100%'; - } - if (styleWidth && styleWidth.indexOf('%') !== -1) { - tableWidthAttr = styleWidth; - } - // Let plug-ins know that we are doing a recalc, in case they have changed any of the - // visible columns their own way (e.g. Responsive uses display:none). - callbackFire(settings, null, 'column-calc', [{ visible: visibleColumns }], false); - // Construct a worst case table with the widest, assign any user defined - // widths, then insert it into the DOM and allow the browser to do all - // the hard work of calculating table widths - var tmpTable = Dom - .s(table.cloneNode()) - .css('visibility', 'hidden') - .css('margin', '0') - .attrRemove('id'); - // Clean up the table body - tmpTable.append(Dom.c('tbody')); - // Clone the table header and footer - we can't use the header / footer - // from the cloned table, since if scrolling is active, the table's - // real header and footer are contained in different table tags - tmpTable - .append(settings.thead.cloneNode(true)) - .append(settings.tfoot.cloneNode(true)); - // Remove any assigned widths from the footer (from scrolling) - tmpTable.find('tfoot th, tfoot td').css('width', ''); - // Apply custom sizing to the cloned header - tmpTable.find('thead th, thead td').each(cell => { - // Get the `width` from the header layout - var width = columnsSumWidth(settings, cell, true); - if (width) { - cell.style.width = width; - // For scrollX we need to force the column width otherwise the - // browser will collapse it. If this width is smaller than the - // width the column requires, then it will have no effect - if (scrollX) { - cell.style.minWidth = width; - Dom.s(cell).append(Dom.c('div').css({ - width: width, - margin: '0', - padding: '0', - border: '0', - height: '1px' - })); - } - } - else { - cell.style.width = ''; - } - }); - // Get the widest strings for each of the visible columns and add them to - // our table to create a "worst case" - var longestData = []; - for (i = 0; i < visibleColumns.length; i++) { - longestData.push(getWideStrings(settings, visibleColumns[i])); - } - if (longestData.length) { - for (i = 0; i < longestData[0].length; i++) { - var tr = Dom.c('tr').appendTo(tmpTable.find('tbody')); - for (j = 0; j < visibleColumns.length; j++) { - columnIdx = visibleColumns[j]; - column = columns[columnIdx]; - var longest = longestData[j][i] || ''; - var autoClass = ext.type.className[column.type]; - var padding = column.contentPadding || (scrollX ? '-' : ''); - var text = longest + padding; - var cell = Dom - .c('td') - .classAdd(autoClass) - .classAdd(column.className) - .appendTo(tr); - if (longest.indexOf('<') === -1 && - longest.indexOf('&') === -1) { - cell.text(text); - } - else { - cell.html(text); - } - } - } - } - // Tidy the temporary table - remove name attributes so there aren't - // duplicated in the dom (radio elements for example) - tmpTable.find('[name]').attrRemove('name'); - // Table has been built, attach to the document so we can work with it. - // A holding element is used, positioned at the top of the container - // with minimal height, so it has no effect on if the container scrolls - // or not. Otherwise it might trigger scrolling when it actually isn't - // needed - var holder = Dom - .c('div') - .css(scrollX || scrollY - ? { - position: 'absolute', - top: '0', - left: '0', - height: '1px', - right: '0', - overflow: 'hidden' - } - : {}) - .append(tmpTable) - .appendTo(tableContainer); - // When scrolling (X or Y) we want to set the width of the table as - // appropriate. However, when not scrolling leave the table width as it - // is. This results in slightly different, but I think correct behaviour - if (scrollX) { - tmpTable.css('width', 'auto').attrRemove('width'); - // If there is no width attribute or style, then allow the table to - // collapse - if (tmpTable.width() < tableContainer.clientWidth && tableWidthAttr) { - tmpTable.width(tableContainer.clientWidth); - } - } - else if (scrollY) { - tmpTable.width(tableContainer.clientWidth); - } - else if (tableWidthAttr) { - tmpTable.width(tableWidthAttr); - } - // Get the width of each column in the constructed table - var total = 0; - var bodyCells = tmpTable.find('tbody tr').eq(0).children(); - for (i = 0; i < visibleColumns.length; i++) { - // Use getBounding for sub-pixel accuracy, which we then want to round - // up! - var bounding = bodyCells.get(i).getBoundingClientRect().width; - // Total is tracked to remove any sub-pixel errors as the outerWidth - // of the table might not equal the total given here - total += bounding; - // Width for each column to use - columns[visibleColumns[i]].width = stringToCss(bounding); - } - table.style.width = stringToCss(total); - // Finished with the table - ditch it - holder.remove(); - // If there is a width attr, we want to attach an event listener which - // allows the table sizing to automatically adjust when the window is - // resized. Use the width attr rather than CSS, since we can't know if the - // CSS is a relative value or absolute - DOM read is always px. - if (tableWidthAttr) { - table.style.width = stringToCss(tableWidthAttr); - } - if ((tableWidthAttr || scrollX) && !settings.reszEvt) { - var resize = util.throttle(function () { - var newWidth = wrapperWidth(settings); - // Don't do it if destroying or the container width is 0 - if (!settings.destroying && newWidth !== 0) { - adjustColumnSizing(settings); - } - }); - // For browsers that support it (~2020 onwards for wide support) we can watch for the - // container changing width. - if (window.ResizeObserver) { - // This is a tricky beast - if the element is visible when `.observe()` is called, - // then the callback is immediately run. Which we don't want. If the element isn't - // visible, then it isn't run, but we want it to run when it is then made visible. - // This flag allows the above to be satisfied. - var first = Dom.s(settings.tableWrapper).isVisible(); - // Use an empty div to attach the observer so it isn't impacted by height changes - var resizer = Dom - .c('div') - .css({ - width: '100%', - height: '0' - }) - .classAdd('dt-autosize') - .appendTo(settings.tableWrapper); - settings.resizeObserver = new ResizeObserver(function (e) { - if (first) { - first = false; - } - else { - resize(); - } - }); - settings.resizeObserver.observe(resizer.get(0)); +function pageChange(settings, action, redraw) { + var start = settings.displayStart, len = settings.pageLength, records = recordsDisplay(settings); + if (records === 0 || len === -1) { + start = 0; + } + else if (typeof action === 'number') { + start = action * len; + if (start > records) { + start = 0; } - else { - // For old browsers, the best we can do is listen for a window - // resize - window.addEventListener('resize', resize); - settings.windowResizeCb = resize; // For removal in `destroy` + } + else if (action == 'first') { + start = 0; + } + else if (action == 'previous') { + start = len >= 0 ? start - len : 0; + if (start < 0) { + start = 0; } - settings.reszEvt = true; } + else if (action == 'next') { + if (start + len < records) { + start += len; + } + } + else if (action == 'last') { + start = Math.floor((records - 1) / len) * len; + } + else if (action === 'ellipsis') { + return; + } + else { + log(settings, 0, 'Unknown paging action: ' + action, 5); + } + var changed = settings.displayStart !== start; + settings.displayStart = start; + callbackFire(settings, null, changed ? 'page' : 'page-nc', [settings]); + if (changed && redraw) { + draw(settings); + } + return changed; } + /** - * Get the width of the DataTables wrapper element + * State information for a table * * @param settings DataTables settings object - * @returns Width */ -function wrapperWidth(settings) { - let wrapper = Dom.s(settings.tableWrapper); - return wrapper.isVisible() ? wrapper.width() : 0; +function saveState(settings) { + if (settings.loadingState) { + return; + } + // Sort state saving uses [[idx, order]] structure. + var sorting = []; + sortResolve(settings, sorting, settings.order); + /* Store the interesting variables */ + var columns = settings.columns; + var state = { + columns: settings.columns.map(function (col, i) { + return { + name: col.name, + visible: col.visible, + search: Object.assign({}, settings.searches[i]) + }; + }), + length: settings.pageLength, + order: sorting.map(function (sort) { + // If a column name is available, use it + return columns[sort[0]] && columns[sort[0]].name + ? [columns[sort[0]].name, sort[1]] + : sort.slice(); + }), + search: Object.assign({}, settings.searches['*']), + searchGroups: Object.keys(settings.searches) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => Object.assign({}, settings.searches[c])), + start: settings.displayStart, + time: +new Date() + }; + settings.stateSaved = state; + callbackFire(settings, 'stateSaveParams', 'stateSaveParams', [ + settings, + state + ]); + if (settings.features.stateSave && !settings.destroying) { + settings.stateSaveCallback.call(settings.instance, settings, state); + } } /** - * Get the widest strings for each column. - * - * It is very difficult to determine what the widest string actually is due to variable character - * width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance - * and this is a critical point, so we use two techniques to determine a collection of the longest - * strings from the column, which will likely contain the widest strings: - * - * 1) Get the top three longest strings from the column - * 2) Get the top three widest words (i.e. an unbreakable phrase) + * Attempt to load a saved table state * - * @param settings DataTables settings object - * @param colIdx column of interest - * @returns Array of the longest strings + * @param settings dataTables settings object + * @param callback Callback to execute when the state has been loaded */ -function getWideStrings(settings, colIdx) { - var column = settings.columns[colIdx]; - // Do we need to recalculate (i.e. was invalidated), or just use the cached data? - if (!column.wideStrings) { - var allStrings = []; - var collection = []; - // Create an array with the string information for the column - for (var i = 0, iLen = settings.displayMaster.length; i < iLen; i++) { - var rowIdx = settings.displayMaster[i]; - var data = getRowDisplay(settings, rowIdx)[colIdx]; - var cellString = data && typeof data === 'object' && data.nodeType - ? data.innerHTML - : data + ''; - // Remove id / name attributes from elements so they - // don't interfere with existing elements - cellString = cellString - .replace(/id=".*?"/g, '') - .replace(/name=".*?"/g, ''); - // Don't want script, dialog or template tags in the width - // calculations as they are hidden content - cellString = cellString - .replace(/]*)?>/gi, ' ') - .replace(/]*)?>/gi, ' ') - .replace(/]*)?>/gi, ' '); - var noHtml = util.string - .stripHtml(cellString, ' ') - .replace(/ /g, ' '); - collection.push({ - str: cellString, - len: noHtml.length - }); - allStrings.push(noHtml); +function loadState(settings, callback) { + if (!settings.features.stateSave) { + callback(); + return; + } + var loaded = function (state, ignoreTime = false) { + implementState(settings, state, ignoreTime, callback); + }; + var state = settings.stateLoadCallback.call(settings.instance, settings, loaded); + if (state !== undefined) { + implementState(settings, state, false, callback); + } + // otherwise, wait for the loaded callback to be executed + return true; +} +function implementState(settings, s, ignoreTime, callback) { + var i, iLen; + var columns = settings.columns; + var currentNames = pluck(settings.columns, 'name'); + settings.loadingState = true; + // When StateRestore was introduced the state could now be implemented at + // any time Not just initialisation. To do this an api instance is required + // in some places + var api = settings.initDone ? new Api(settings) : null; + if (!ignoreTime) { + if (!s || !s.time) { + settings.loadingState = false; + callback(); + return; } - // Order and then cut down to the size we need - collection - .sort(function (a, b) { - return b.len - a.len; - }) - .splice(3); - column.wideStrings = collection.map(function (item) { - return item.str; - }); - // Longest unbroken string - const parts = allStrings.join(' ').split(' '); - parts.sort(function (a, b) { - return b.length - a.length; - }); - if (parts.length) { - column.wideStrings.push(parts[0]); + // Reject old data + var duration = settings.stateDuration; + if (duration > 0 && s.time < +new Date() - duration * 1000) { + settings.loadingState = false; + callback(); + return; } - if (parts.length > 1) { - column.wideStrings.push(parts[1]); + } + // Allow custom and plug-in manipulation functions to alter the saved data + // set and cancelling of loading by returning false + var abStateLoad = callbackFire(settings, 'stateLoadParams', 'stateLoadParams', [settings, s]); + if (abStateLoad.indexOf(false) !== -1) { + settings.loadingState = false; + callback(); + return; + } + // Store the saved state so it might be accessed at any time + settings.stateLoaded = assignDeep({}, s); + // This is needed for ColReorder, which has to happen first to allow all + // the stored indexes to be usable. It is not publicly documented. + callbackFire(settings, null, 'stateLoadInit', [settings, s], true); + // Page Length + if (s.length !== undefined) { + // If already initialised just set the value directly so that the select + // element is also updated + if (api) { + api.page.len(s.length); } - if (parts.length > 2) { - column.wideStrings.push(parts[3]); + else { + settings.pageLength = s.length; } } - return column.wideStrings; -} -/** - * Append a CSS unit (only if required) to a string - * - * @param s Value to css-ify - * @returns Value with css unit - */ -function stringToCss(s) { - if (s === null) { - return '0px'; + // Restore key features + if (s.start !== undefined) { + if (api === null) { + settings.displayStart = s.start; + settings.displayStartInit = s.start; + } + else { + pageChange(settings, s.start / settings.pageLength); + } + } + // Order + if (s.order !== undefined) { + settings.order = []; + for (let i = 0; i < s.order.length; i++) { + let col = s.order[i]; + let set = [col[0], col[1]]; + // A column name was stored and should be used for restore + if (typeof col[0] === 'string') { + // Find the name from the current list of column names + let idx = currentNames.indexOf(col[0]); + if (idx < 0) { + // If the column was not found ignore it and continue + continue; + } + set[0] = idx; + } + else if (set[0] >= columns.length) { + // If the column index is out of bounds ignore it and continue + continue; + } + settings.order.push(set); + } } - if (typeof s == 'number') { - return s < 0 ? '0px' : s + 'px'; + // Search + if (s.search !== undefined) { + Object.assign(settings.searches['*'], s.search); } - // Check it has a unit character already - return s.match(/\d$/) ? s + 'px' : s; -} -/** - * Re-insert the `col` elements for current visibility - * - * @param settings DT settings - */ -function colGroup(settings) { - var cols = settings.columns; - settings.colgroup.empty(); - for (var i = 0; i < cols.length; i++) { - if (cols[i].visible) { - settings.colgroup.append(cols[i].colEl); + if (s.searchGroups) { + s.searchGroups.forEach(group => { + if (group.columns) { + let index = group.columns.join(','); + settings.searches[index] = create$1(group); + } + }); + } + // Columns + if (s.columns) { + var set = s.columns; + var incoming = pluck(s.columns, 'name'); + // Check if it is a 2.2 style state object with a `name` property for + // the columns, and if the name was defined. If so, then create a new + // array that will map the state object given, to the current columns + // (don't bother if they are already matching tho). + if (incoming.join('').length && + incoming.join('') !== currentNames.join('')) { + set = []; + // For each column, try to find the name in the incoming array + for (i = 0; i < currentNames.length; i++) { + if (currentNames[i] != '') { + var idx = incoming.indexOf(currentNames[i]); + if (idx >= 0) { + set.push(s.columns[idx]); + } + else { + // No matching column name in the state's columns, so + // this might be a new column and thus can't have a + // state already. + set.push({}); + } + } + else { + // If no name, but other columns did have a name, then there + // is no knowing where this one came from originally so it + // can't be restored. + set.push({}); + } + } + } + // If the number of columns to restore is different from current, then + // all bets are off. + if (set.length === columns.length) { + for (i = 0, iLen = set.length; i < iLen; i++) { + var col = set[i]; + // Visibility + if (col.visible !== undefined) { + // If the api is defined, the table has been initialised so + // we need to use it rather than internal settings + if (api) { + // Don't redraw the columns on every iteration of this + // loop, we will do this at the end instead + api.column(i).visible(col.visible, false); + } + else { + columns[i].visible = col.visible; + } + } + // Search + if (col.search !== undefined) { + Object.assign(settings.searches[i], col.search); + // If out of order due to a change in order from named + // columns we need to make sure the index is correct + settings.searches[i].columns = [i]; + } + } + // If the api is defined then we need to adjust the columns once the + // visibility has been changed + if (api) { + api.one('draw', function () { + api.columns.adjust(); + }); + } } } + settings.loadingState = false; + callbackFire(settings, 'stateLoaded', 'stateLoaded', [settings, s]); + callback(); } /** - * Scrolling setup + * Draw the table for the first time, adding all required features * * @param settings DataTables settings object - * @returns Node to add to the DOM */ -function featureTable(settings) { - let table = Dom.s(settings.table); - let scroll = settings.scroll; - let scrollX = scroll.x; - let scrollY = scroll.y; - // No scrolling or x-scrolling only - if (scrollY === '' && scrollX === '') { - return table.get(0); - } - let classes = settings.classes.scrolling; - let caption = settings.captionNode; - let captionSide = caption - ? caption._captionSide - : null; - let tableCloneHeader = table.clone(false); - let tableCloneFooter = table.clone(false); - let footer = table.children('tfoot'); - let size = function (s) { - return !s ? '100%' : stringToCss(s); - }; - /* - * The HTML structure that we want to generate in this function is: - * div - scroller - * div - scroll head - * div - scroll head inner - * table - scroll head table - * thead - thead - * div - scroll body - * table - table (master table) - * thead - thead clone for sizing - * tbody - tbody - * div - scroll foot - * div - scroll foot inner - * table - scroll foot table - * tfoot - tfoot - */ - let scroller = Dom.c('div') - .classAdd(classes.container) - .attr('role', 'table') - .append(Dom.c('div') - .classAdd(classes.header.self) - .css({ - overflow: 'hidden', - position: 'relative', - border: '0', - width: scrollX ? size(scrollX) : '100%' - }) - .attr('role', 'none') - .append(Dom.c('div') - .classAdd(classes.header.inner) - .css({ - 'box-sizing': 'content-box', - width: scroll.xInner || '100%' - }) - .attr('role', 'none') - .append(tableCloneHeader - .attrRemove('id') - .css('margin-left', '0') - .append(captionSide === 'top' ? caption : null) - .append(table.children('thead'))))) - .append(Dom.c('div') - .classAdd(classes.body) - .css({ - position: 'relative', - overflow: 'auto', - width: size(scrollX) - }) - .attr('role', 'none') - .append(table)); - if (footer.count()) { - scroller.append(Dom.c('div') - .classAdd(classes.footer.self) - .css({ - overflow: 'hidden', - border: '0', - width: scrollX ? size(scrollX) : '100%' - }) - .attr('role', 'none') - .append(Dom.c('div') - .classAdd(classes.footer.inner) - .attr('role', 'none') - .append(tableCloneFooter - .attrRemove('id') - .css('margin-left', '0') - .append(captionSide === 'bottom' ? caption : null) - .append(table.children('tfoot'))))); +function initialise(settings) { + var i; + var init = settings.init; + var deferLoading = settings.deferLoading; + var dataSrc = dataSource(settings); + // Ensure that the table data is fully initialised + if (!settings.initialised) { + setTimeout(function () { + initialise(settings); + }, 200); + return; } - let children = scroller.children(); - let scrollHead = children.eq(0); - let scrollBody = children.eq(1); - let scrollFoot = children.eq(2); - // When the body is scrolled, then we also want to scroll the header and - // footer. Note that each element has its own scroll listener, and that in - // turn sets the scroll for the other elements. However this doesn't lead to - // an infinite loop as `scroll` is only triggered if the value changes. - scrollBody.on('scroll.DT', () => { - let scrollLeft = scrollBody.scrollLeft(); - scrollHead.scrollLeft(scrollLeft); - scrollFoot.scrollLeft(scrollLeft); - }); - scrollHead.on('scroll.DT', () => { - let scrollLeft = scrollHead.scrollLeft(); - scrollBody.scrollLeft(scrollLeft); - scrollFoot.scrollLeft(scrollLeft); - }); - scrollFoot.on('scroll.DT', () => { - let scrollLeft = scrollFoot.scrollLeft(); - scrollHead.scrollLeft(scrollLeft); - scrollBody.scrollLeft(scrollLeft); + // Build the header / footer for the table + buildHead(settings, 'header'); + buildHead(settings, 'footer'); + // Load the table's state (if needed) and then render around it and draw + loadState(settings, function () { + // Then draw the header / footer + drawHead(settings, settings.header); + drawHead(settings, settings.footer); + // Cache the paging start point, as the first redraw will reset it + var iAjaxStart = settings.displayStartInit; + // Local data load + // Check if there is data passing into the constructor + if (init && init.data) { + for (i = 0; i < init.data.length; i++) { + addData(settings, init.data[i]); + } + } + else if (deferLoading || dataSrc == 'dom') { + // Grab the data from the page + addTr(settings, Dom.s(settings.tbody).children('tr')); + } + // Filter not yet applied - copy the display master + settings.display = settings.displayMaster.slice(); + // Enable features + createLayout(settings); + sortInit(settings); + colGroup(settings); + /* Okay to show that something is going on now */ + processingDisplay(settings, true); + callbackFire(settings, null, 'preInit', [settings], true); + // If there is default sorting required - let's do it. The sort function + // will do the drawing for us. Otherwise we draw the table regardless of + // the Ajax source - this allows the table to look initialised for Ajax + // sourcing data (show 'loading' message possibly) + reDraw(settings); + // Server-side processing init complete is done by _fnAjaxUpdateDraw + if (dataSrc != 'ssp' || deferLoading) { + // if there is an ajax source load the data + if (dataSrc == 'ajax') { + buildAjax(settings, {}, function (json) { + var aData = ajaxDataSrc(settings, json, false); + // Got the data - add it to the table + for (i = 0; i < aData.length; i++) { + addData(settings, aData[i]); + } + // Reset the init display for cookie saving. We've already + // done a filter, and therefore cleared it before. So we + // need to make it appear 'fresh' + settings.displayStartInit = iAjaxStart; + reDraw(settings); + processingDisplay(settings, false); + initComplete(settings); + }); + } + else { + initComplete(settings); + processingDisplay(settings, false); + } + } }); - scrollBody.css('max-height', size(scrollY)); - if (!scroll.collapse) { - scrollBody.css('height', size(scrollY)); - } - settings.scrollHead = scrollHead; - settings.scrollBody = scrollBody; - settings.scrollFoot = scrollFoot; - // On redraw - align columns - settings.callbacks.draw.push(scrollDraw); - // Aria roles - because we break the table up into parts we need to be very - // explicit with the roles to create the accessability tree for the table, - // otherwise browser's attempt to "fix" the tree by filling in what it - // thinks are gaps. The static elements that we can assign roles to are done - // here. Dynamic ones are done in the draw function below. - table.attr('role', 'none'); - table.find('tbody').attr('role', 'rowgroup'); - tableCloneHeader.attr('role', 'none'); - tableCloneFooter.attr('role', 'none'); - settings.colgroup.find('colgroup').attr('role', 'none'); - // Move the info feature's aria desc by to the new "table" - let describedBy = table.attr('aria-describedby'); - if (describedBy) { - scroller.attr('aria-describedby', describedBy); - table.attrRemove('aria-describedby'); - } - return scroller.get(0); } /** - * Update the header, footer and body tables for resizing - i.e. column - * alignment. - * - * Welcome to the most horrible function DataTables. The process that this - * function follows is basically: - * 1. Re-create the table inside the scrolling div - * 2. Correct colgroup > col values if needed - * 3. Copy colgroup > col over to header and footer - * 4. Clean up + * Draw the table for the first time, adding all required features * * @param settings DataTables settings object */ -function scrollDraw(settings) { - // Given that this is such a monster function, a lot of variables are use - // to try and keep the minimised size as small as possible - let scroll = settings.scroll, barWidth = scroll.barWidth, divHeader = settings.scrollHead, divHeaderInner = divHeader.children('div'), divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.scrollBody, divBody = divBodyEl, divFooter = settings.scrollFoot, divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = Dom.s(settings.thead), table = Dom.s(settings.table), footer = Dom.s(settings.tfoot), browser = settings.browser, headerCopy, footerCopy; - // If the scrollbar visibility has changed from the last draw, we need to - // adjust the column sizes as the table width will have changed to account - // for the scrollbar - let scrollBarVis = divBodyEl.get(0).scrollHeight > divBodyEl.get(0).clientHeight; - if (settings.scrollBarVis !== scrollBarVis && - settings.scrollBarVis !== undefined) { - settings.scrollBarVis = scrollBarVis; - adjustColumnSizing(settings); - return; // adjust column sizing will call this function again - } - else { - settings.scrollBarVis = scrollBarVis; +function initComplete(settings) { + if (settings.initDone) { + return; } - header.find('thead').attr('role', 'rowgroup'); - footer.find('tfoot').attr('role', 'rowgroup'); - // 1. Re-create the table inside the scrolling div - // Remove the old minimised thead and tfoot elements in the inner table - table.children('thead, tfoot').remove(); - // Clone the current header and footer elements and then place it into the - // inner table - headerCopy = header.clone(true).prependTo(table); - headerCopy.find('th, td').attrRemove('tabindex'); - headerCopy.find('[id]').attrRemove('id'); - if (footer.count()) { - footerCopy = footer.clone(true).prependTo(table); - footerCopy.find('[id]').attrRemove('id'); + var args = [settings, settings.json]; + settings.initDone = true; + // If the footer element is empty after initialisation, then remove it + let tfoot = Dom.s(settings.tfoot); + if (tfoot.children().count() === 0) { + tfoot.remove(); } - // 2. Correct colgroup > col values if needed - // It is possible that the cell sizes are smaller than the content, so we need to - // correct colgroup>col for such cases. This can happen if the auto width detection - // uses a cell which has a longer string, but isn't the widest! For example - // "Chief Executive Officer (CEO)" is the longest string in the demo, but - // "Systems Administrator" is actually the widest string since it doesn't collapse. - // Note the use of translating into a column index to get the `col` element. This - // is because of Responsive which might remove `col` elements, knocking the alignment - // of the indexes out. - if (settings.display.length) { - // Get the column sizes from the first row in the table. This should really be a - // [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year - // browser support - let firstTr = null; - let start = dataSource(settings) !== 'ssp' ? settings.displayStart : 0; - for (let i = start; i < start + settings.display.length; i++) { - let idx = settings.display[i]; - let row = settings.data[idx]; - if (row) { - let tr = row.tr; - if (tr) { - firstTr = tr; - break; - } + // Table is fully set up and we have data, so calculate the + // column widths + adjustColumnSizing(settings); + callbackFire(settings, null, 'plugin-init', args, true); + callbackFire(settings, 'init', 'init', args, true); +} + +/** + * Create an Ajax call based on the table's settings, taking into account that + * parameters can have multiple forms, and backwards compatibility. + * + * @param settings DataTables settings object + * @param data Data to send to the server, required by DataTables - may be + * augmented by developer callbacks + * @param fn Callback function to run when data is obtained + */ +function buildAjax(settings, data, fn) { + var ajaxData; + var ajaxConfig = settings.ajax; + var instance = settings.instance; + var callback = function (json) { + var status = settings.jqXHR ? settings.jqXHR.status : null; + if (json === null || (typeof status === 'number' && status == 204)) { + json = {}; + ajaxDataSrc(settings, json, []); + } + var error = json.error || json.sError; + if (error) { + log(settings, 0, error); + } + // Microsoft often wrap JSON as a string in another JSON object Let's + // handle that automatically + if (json.d && typeof json.d === 'string') { + try { + json = JSON.parse(json.d); + } + catch (e) { + // noop } } - if (firstTr) { - let colSizes = Dom.s(firstTr) - .children('th, td') - .mapTo(function (cell, idx) { - return { - idx: visibleToColumnIndex(settings, idx), - width: Dom.s(cell).width('outer') - }; - }); - // Check against what the colgroup > col is set to and correct if needed - for (let i = 0; i < colSizes.length; i++) { - let colEl = settings.columns[colSizes[i].idx].colEl; - colEl.css('width', colSizes[i].width + 'px'); - if (scroll.x) { - colEl.css('minWidth', colSizes[i].width + 'px'); + settings.json = json; + invalidColumn(settings); + callbackFire(settings, null, 'xhr', [settings, json, settings.jqXHR], true); + fn(json); + }; + if (util.is.plainObject(ajaxConfig) && ajaxConfig.data) { + ajaxData = ajaxConfig.data; + var newData = typeof ajaxData === 'function' + ? ajaxData(data, settings) // fn can manipulate data or return + : ajaxData; // an object or array to merge + // If the function returned something, use that alone + data = + typeof ajaxData === 'function' && newData + ? newData + : util.object.assignDeep(data, newData); + // Remove the data property as we've resolved it already and don't want + // jQuery to do it again (it is restored at the end of the function) + delete ajaxConfig.data; + } + var baseAjax = { + url: typeof ajaxConfig === 'string' ? ajaxConfig : '', + data: data, + success: callback, + dataType: 'json', + cache: false, + type: settings.serverMethod, + error: function (xhr, error) { + var ret = callbackFire(settings, null, 'xhr', [settings, null, settings.jqXHR], true); + if (ret.indexOf(false) === -1) { + if (error == 'parsererror') { + log(settings, 0, 'Invalid JSON response', 1); + } + else if (xhr.readyState === 4) { + log(settings, 0, 'Ajax error', 7); } } + processingDisplay(settings, false); } + }; + // If `ajax` option is an object, extend and override our default base + if (util.is.plainObject(ajaxConfig)) { + util.object.assign(baseAjax, ajaxConfig); } - // 3. Copy the colgroup over to the header and footer - divHeaderTable.find('colgroup').remove(); - divHeaderTable.append(settings.colgroup.clone(true)); - if (footer) { - divFooterTable.find('colgroup').remove(); - divFooterTable.append(settings.colgroup.clone(true)); - } - // "Hide" the header and footer that we used for the sizing. We need to keep - // the content of the cell so that the width applied to the header and body - // both match, but we want to hide it completely. - headerCopy.find('th, td').each(function (el) { - Dom.c('div') - .classAdd('dt-scroll-sizing') - .append(Array.from(el.childNodes)) - .appendTo(el); - }); - if (footerCopy) { - footerCopy.find('th, td').each(function (el) { - Dom.c('div') - .classAdd('dt-scroll-sizing') - .append(Array.from(el.childNodes)) - .appendTo(el); - }); - } - // 4. Clean up - // Figure out if there are scrollbar present - if so then we need the header and footer to - // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) - let isScrolling = Math.floor(table.height()) > divBodyEl.get(0).clientHeight || - divBody.css('overflow-y') == 'scroll'; - let paddingSide = 'padding' + (browser.scrollbarLeft ? 'Left' : 'Right'); - // Set the width's of the header and footer tables - let outerWidth = table.width('withPadding'); - divHeaderTable.css('width', stringToCss(outerWidth)); - divHeaderInner - .css('width', stringToCss(outerWidth)) - .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); - if (footer.count()) { - divFooterTable.css('width', stringToCss(outerWidth)); - divFooterInner - .css('width', stringToCss(outerWidth)) - .css(paddingSide, isScrolling ? barWidth + 'px' : '0px'); + // Store the data submitted for the API + settings.ajaxData = data; + // Allow plug-ins and external processes to modify the data + callbackFire(settings, null, 'preXhr', [settings, data, baseAjax], true); + if (typeof ajaxConfig === 'function') { + // Is a function - let the caller define what needs to be done + settings.jqXHR = ajaxConfig.call(instance, data, callback, settings); } - // Correct DOM ordering for colgroup - comes before the thead - table.children('colgroup').prependTo(table); - // Remove tabindex from the hidden row elements - table.find('thead, tfoot').find('[tabindex]').attrRemove('tabindex'); - // Dynamic ARIA roles - see setup for details on why this is needed - table - .find('thead, tfoot') - .attr('role', 'none') - .find('[role]') - .attrRemove('role'); - table.find('tbody tr:not([role])').attr('role', 'row'); - table.find('tbody td:not([role]), tbody th:not([role])').attr('role', 'cell'); - scrollAria(headerCopy); - scrollAria(footerCopy); - // Adjust the position of the header in case we loose the y-scrollbar - divBody.trigger('scroll'); - // If sorting or filtering has occurred, jump the scrolling back to the top - // only if we aren't holding the position - if ((settings.wasOrdered || settings.wasFiltered) && !settings.drawHold) { - divBodyEl.scrollTop(0); + else if (ajaxConfig && + typeof ajaxConfig !== 'string' && + ajaxConfig.url === '') { + // No url, so don't load any data. Just apply an empty data array + // to the object for the callback. + var empty = {}; + ajaxDataSrc(settings, empty, []); + callback(empty); } -} -/** - * Apply ARIA roles for the header / footer of a scrolling table - * @param element - */ -function scrollAria(element) { - if (element) { - element.find('tfoot:not([role])').attr('role', 'rowgroup'); - element.find('tr:not([role])').attr('role', 'row'); - element.find('th:not([role])').attr('role', 'columnheader'); - element.find('td:not([role])').attr('role', 'cell'); + else { + // Object to extend the base settings + settings.jqXHR = util.ajax(baseAjax); + } + // Restore for next time around + if (ajaxData) { + ajaxConfig.data = ajaxData; } } - /** - * Add a column to the list used for the table with default values + * Update the table using an Ajax call * * @param settings DataTables settings object + * @returns Block the table drawing or not */ -function addColumn(settings) { - // Add column to aoColumns array - let columnIdx = settings.columns.length; - let column = util.object.assign({}, new Settings(), defaults$4, { - orderData: defaults$4.orderData - ? defaults$4.orderData - : [columnIdx], - data: defaults$4.data ? defaults$4.data : columnIdx, - idx: columnIdx, - searchFixed: {}, - colEl: Dom - .c('col') - .attr('data-dt-column', columnIdx) +function ajaxUpdate(settings) { + settings.drawCount++; + processingDisplay(settings, true); + buildAjax(settings, ajaxParameters(settings), function (json) { + ajaxUpdateDraw(settings, json); }); - settings.columns.push(column); - // Legacy support for `searchCols` property. If set, and there is a value - // for this column, then it should be applied to the search. The new, column - // specific `search` option is applied in `columnOptions`, but we always - // want the search object for the column to exist. - let searchCols = settings.searchCols; - settings.searches[columnIdx] = create$2(searchCols[columnIdx] - ? hungarianToCamel(searchCols[columnIdx]) - : {}); - settings.searches[columnIdx].columns = [columnIdx]; +} +function functionOrValue(val) { + return typeof val === 'function' ? 'function' : val.toString(); } /** - * Apply options for a column + * Build up the parameters in an object needed for a server-side processing + * request. * * @param settings DataTables settings object - * @param colIdx column index to consider - * @param options Column configuration options + * @returns Block the table drawing or not */ -function columnOptions(settings, colIdx, options) { - var column = settings.columns[colIdx]; - /* User specified column options */ - if (options !== undefined && options !== null) { - // Backwards compatibility - compatCols(options); - if (options.type) { - column.typeManual = options.type; - } - // `class` is a reserved word in JavaScript, so we need to provide - // the ability to use a valid name for the camel case input - if (options.className && !options.className) { - options.className = options.className; - } - var origClass = column.className; - util.object.assign(column, options); - map(column, options, 'width', 'widthOrig'); - // Merge class from previously defined classes with this one, rather - // than just overwriting it in the extend above - if (origClass !== column.className) { - column.className = origClass + ' ' + column.className; +function ajaxParameters(settings) { + var columns = settings.columns, features = settings.features, searches = settings.searches, searchesFixed = settings.searchesFixed, colData = function (idx, prop) { + return typeof columns[idx][prop] === 'function' + ? 'function' + : columns[idx][prop]; + }; + return { + draw: settings.drawCount, + columns: columns.map(function (column, i) { + return { + data: colData(i, 'data'), + name: column.name, + searchable: column.searchable, + orderable: column.orderable, + search: { + value: searches[i] + ? functionOrValue(searches[i].search) + : '', + regex: searches[i] ? searches[i].regex : false, + fixed: searchesFixed[i] + ? Object.keys(searchesFixed[i]).map(name => ({ + name: name, + term: functionOrValue(searchesFixed[i][name].search) + })) + : [] + } + }; + }), + order: sortFlatten(settings).map(function (val) { + return { + column: val.col, + dir: val.dir, + name: colData(val.col, 'name') + }; + }), + start: settings.displayStart, + length: features.paging ? settings.pageLength : -1, + search: { + value: functionOrValue(searches['*'].search), + regex: searches['*'].regex, + fixed: Object.keys(settings.searchesFixed['*']).map(name => ({ + name: name, + term: functionOrValue(settings.searchesFixed['*'][name].search) + })), + groups: Object.keys(settings.searches) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => ({ + columns: settings.searches[c].columns || [], + term: functionOrValue(settings.searches[c].search) + })), + groupsFixed: Object.keys(settings.searchesFixed) + .filter(c => c.includes(',')) // Limit to only multi-column subsets + .map(c => { + let searches = settings.searchesFixed[c]; + return Object.keys(searches).map(n => ({ + columns: searches[n].columns || [], + name: n, + term: functionOrValue(searches[n].search) + })); + }) + .flat() } - map(column, options, 'orderData'); - // Search term specifically for this column - if (options.search) { - util.object.assign(settings.searches[colIdx], options.search); + }; +} +/** + * Data the data from the server (nuking the old) and redraw the table + * + * @param settings DataTables settings object + * @param json json data return from the server. + */ +function ajaxUpdateDraw(settings, json) { + var data = ajaxDataSrc(settings, json, false); + var drawUnique = ajaxDataSrcParam(settings, 'draw', json); + var recordsTotal = ajaxDataSrcParam(settings, 'recordsTotal', json); + var recordsFiltered = ajaxDataSrcParam(settings, 'recordsFiltered', json); + var existingTypes = settings.columns.map(c => c.type).join(','); + if (drawUnique !== undefined) { + // Protect against out of sequence returns + if (drawUnique * 1 < settings.drawCount) { + return; } + settings.drawCount = drawUnique * 1; } - /* Cache the data get and set functions for speed */ - var dataSrc = column.data; - var dataFn = util.get(dataSrc); - // The `render` option can be given as an array to access the helper - // rendering methods. The first element is the rendering method to use, the - // rest are the parameters to pass - if (column.render && Array.isArray(column.render)) { - var copy = column.render.slice(); - var name = copy.shift(); - column.render = helpers[name].apply(window, copy); + // No data in returned object, so rather than an array, we show an empty + // table + if (!data) { + data = []; } - column.renderer = column.render ? util.get(column.render) : null; - var attrTest = function (src) { - return typeof src === 'string' && src.indexOf('@') !== -1; - }; - column.attrSrc = - !!dataSrc && - util.is.plainObject(dataSrc) && - (attrTest(dataSrc.sort) || - attrTest(dataSrc.type) || - attrTest(dataSrc.filter)); - column.setter = null; - column.dataGet = function (rowData, type, meta) { - var innerData = dataFn(rowData, type, undefined, meta); - return column.renderer && type - ? column.renderer(innerData, type, rowData, meta) - : innerData; - }; - column.dataSet = function (rowData, val, meta) { - return util.set(dataSrc)(rowData, val, meta); - }; - // Indicate if DataTables should read DOM data as an object or array - // Used in _fnGetRowElements - if (typeof dataSrc !== 'number' && !column._isArrayHost) { - settings.rowReadObject = true; + clearTable(settings); + settings.recordsTotal = parseInt(recordsTotal, 10); + settings.recordsDisplay = parseInt(recordsFiltered, 10); + for (var i = 0, iLen = data.length; i < iLen; i++) { + addData(settings, data[i]); + } + settings.display = settings.displayMaster.slice(); + columnTypes(settings, existingTypes); + draw(settings, true); + initComplete(settings); + processingDisplay(settings, false); +} +/** + * Get the data from the JSON data source to use for drawing a table. + * + * @param settings DataTables settings object + * @param json Data source object / array from the server + * @param write Array or object to write the data to + * @return Array of data to use + */ +function ajaxDataSrc(settings, json, write) { + var dataProp = 'data'; + if (util.is.plainObject(settings.ajax) && + settings.ajax.dataSrc !== undefined) { + // Could in inside a `dataSrc` object, or not! + var dataSrc = settings.ajax.dataSrc; + // string, function and object are valid types + if (typeof dataSrc === 'string' || typeof dataSrc === 'function') { + dataProp = dataSrc; + } + else if (dataSrc.data !== undefined) { + dataProp = dataSrc.data; + } } - // Feature sorting overrides column specific when off - if (!settings.features.ordering) { - column.orderable = false; + if (!write) { + if (dataProp === 'data') { + // If the default, then we still want to support the old style, and + // safely ignore it if possible + return json.aaData || json[dataProp]; + } + return dataProp !== '' ? util.get(dataProp)(json) : json; } + // set + util.set(dataProp)(json, write); } /** - * Adjust the table column widths for new data. Note: you would probably want to - * do a redraw after calling this function! + * Very similar to ajaxDataSrc, but for the other SSP properties * * @param settings DataTables settings object + * @param param Target parameter + * @param json JSON data + * @returns Resolved value */ -function adjustColumnSizing(settings) { - calculateColumnWidths(settings); - columnSizes(settings); - let scroll = settings.scroll; - if (scroll.y !== '' || scroll.x !== '') { - scrollDraw(settings); +function ajaxDataSrcParam(settings, param, json) { + var dataSrc = util.is.plainObject(settings.ajax) + ? settings.ajax.dataSrc // TODO + : null; + if (dataSrc && dataSrc[param]) { + // Get from custom location + return util.data.get(dataSrc[param])(json); } - callbackFire(settings, null, 'column-sizing', [settings]); + // else - Default behaviour + var old = ''; + // Legacy support + if (param === 'draw') { + old = 'sEcho'; + } + else if (param === 'recordsTotal') { + old = 'iTotalRecords'; + } + else if (param === 'recordsFiltered') { + old = 'iTotalDisplayRecords'; + } + return json[old] !== undefined ? json[old] : json[param]; } + +const __filter_div = Dom.c('div').get(0); +const __filter_div_textContent = __filter_div.textContent !== undefined; /** - * Apply column sizes + * Filter the table using both the global filter and column based filtering * * @param settings DataTables settings object */ -function columnSizes(settings) { - let cols = settings.columns; - for (let i = 0; i < cols.length; i++) { - let width = columnsSumWidth(settings, [i], false); - if (width) { - cols[i].colEl.css('width', width); - if (settings.scroll.x) { - cols[i].colEl.css('min-width', width); - } - } +function filterComplete(settings) { + settings.columns; + // In server-side processing all filtering is done by the server, so no + // point hanging around here + if (dataSource(settings) != 'ssp') { + // Check if any of the rows were invalidated + filterData(settings); + // Start from the full data set + settings.display = settings.displayMaster.slice(); + // Column set filters first + util.object.each(settings.searches, (key, s) => { + filter(settings.display, settings, s.search, s); + }); + // Fixed (named) filters next + util.object.each(settings.searchesFixed, function (columns) { + util.object.each(settings.searchesFixed[columns], function (name, s) { + filter(settings.display, settings, s.search, s); + }); + }); + // And finally legacy global filtering + filterCustom(settings); } + // Tell the draw function we have been filtering + settings.wasFiltered = true; + callbackFire(settings, null, 'search', [settings]); } /** - * Convert the index of a visible column to the index in the data array (take - * account of hidden columns) + * Apply custom filtering functions + * + * This is legacy now that we have named functions, but it is widely used + * from 1.x, so it is not yet deprecated. * * @param settings DataTables settings object - * @param visIdx Visible column index to lookup - * @returns i the data index */ -function visibleToColumnIndex(settings, visIdx) { - let aiVis = getColumns(settings, 'visible'); - return typeof aiVis[visIdx] === 'number' ? aiVis[visIdx] : null; +function filterCustom(settings) { + let filters = ext.search; + let displayRows = settings.display; + let row, rowIdx; + for (let i = 0, iLen = filters.length; i < iLen; i++) { + let rows = []; + // Loop over each row and see if it should be included + for (let j = 0, jen = displayRows.length; j < jen; j++) { + rowIdx = displayRows[j]; + row = settings.data[rowIdx]; + if (row && + filters[i](settings, row.searchCellCache, rowIdx, row.data, j)) { + rows.push(rowIdx); + } + } + // So the array reference doesn't break set the results into the + // existing array + displayRows.length = 0; + arrayApply(displayRows, rows); + } } /** - * Convert the index of an index in the data array and convert it to the visible - * column index (take account of hidden columns) + * Filter the data table based on user input and draw the table * - * @param settings DataTables settings object - * @param match Column index to lookup - * @returns The data index + * @param searchRows + * @param settings + * @param input + * @param options + * @returns */ -function columnIndexToVisible(settings, match) { - let aiVis = getColumns(settings, 'visible'); - let iPos = aiVis.indexOf(match); - return iPos !== -1 ? iPos : null; +function filter(searchRows, settings, input, options) { + if (input === '') { + return; + } + let i = 0; + let matched = []; + // Search term can be a function, regex or string - if a string we apply our + // smart filtering regex (assuming the options require that) + let searchFunc = typeof input === 'function' ? input : null; + let rpSearch = input instanceof RegExp + ? input + : searchFunc + ? null + : filterCreateSearch(input, options); + let columns = options.columns + ? options.columns + : util.array.range(settings.columns.length); + // Then for each row, does the test pass. If not, lop the row from the array + for (i = 0; i < searchRows.length; i++) { + let row = settings.data[searchRows[i]]; + if (row) { + // Get the data array based on the columns to include in the search + let data = util.array.selectiveJoin(row.searchCellCache, columns); + // Run the search action + if ((searchFunc && + searchFunc(data, row.data, searchRows[i], columns.length === 1 ? columns[0] : columns // compat + )) || + (rpSearch && typeof data === 'string' && rpSearch.test(data))) { + matched.push(searchRows[i]); + } + } + } + // Mutate the searchRows array + searchRows.length = matched.length; + for (i = 0; i < matched.length; i++) { + searchRows[i] = matched[i]; + } } /** - * Get the number of visible columns - * - * @param settings DataTables settings object - * @returns i the number of visible columns + * Build a regular expression object suitable for searching a table */ -function visibleColumns(settings) { - let layout = settings.header; +function filterCreateSearch(searchIn, inOpts) { + let not = []; + let options = Object.assign({}, { + boundary: false, + caseInsensitive: true, + exact: false, + regex: false, + smart: true + }, inOpts); + let search = typeof searchIn !== 'string' ? searchIn.toString() : searchIn; + // Remove diacritics if normalize is set up to do so + search = util.diacritics(search); + if (options.exact) { + return new RegExp('^' + util.escapeRegex(search) + '$', options.caseInsensitive ? 'i' : ''); + } + search = options.regex ? search : util.escapeRegex(search); + if (options.smart) { + /* For smart filtering we want to allow the search to work regardless of + * word order. We also want double quoted text to be preserved, so word + * order is important - a la google. And a negative look around for + * finding rows which don't contain a given string. + * + * So this is the sort of thing we want to generate: + * + * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ + */ + let parts = search.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g) || [ + '' + ]; + let a = parts.map(function (word) { + let negative = false; + let m; + // Determine if it is a "does not include" + if (word.charAt(0) === '!') { + negative = true; + word = word.substring(1); + } + // Strip the quotes from around matched phrases + if (word.charAt(0) === '"') { + m = word.match(/^"(.*)"$/); + word = m ? m[1] : word; + } + else if (word.charAt(0) === '\u201C') { + // Smart quote match (iPhone users) + m = word.match(/^\u201C(.*)\u201D$/); + word = m ? m[1] : word; + } + // For our "not" case, we need to modify the string that is + // allowed to match at the end of the expression. + if (negative) { + if (word.length > 1) { + not.push('(?!' + word + ')'); + } + word = ''; + } + return word.replace(/"/g, ''); + }); + let match = not.length ? not.join('') : ''; + let boundary = options.boundary ? '\\b' : ''; + search = + '^(?=.*?' + + boundary + + a.join(')(?=.*?' + boundary) + + ')(' + + match + + '.)*$'; + } + return new RegExp(search, options.caseInsensitive ? 'i' : ''); +} +// Update the filtering data for each row if needed (by invalidation or first +// run) +function filterData(settings) { let columns = settings.columns; - let vis = 0; - if (layout.length) { - for (let i = 0, iLen = layout[0].length; i < iLen; i++) { - if (columns[i].visible && - Dom.s(layout[0][i].cell).css('display') !== 'none') { - vis++; + let data = settings.data; + let column; + let j, jen, cellData, row; + let wasInvalidated = false; + for (let rowIdx = 0; rowIdx < data.length; rowIdx++) { + if (!data[rowIdx]) { + continue; + } + row = data[rowIdx]; + if (row && !row.searchCellCache) { + const rowFilterData = []; + for (j = 0, jen = columns.length; j < jen; j++) { + column = columns[j]; + if (column.searchable) { + cellData = getCellData(settings, rowIdx, j, 'filter'); + // Search in DataTables is string based + if (cellData === null) { + cellData = ''; + } + if (typeof cellData !== 'string' && cellData.toString) { + cellData = cellData.toString(); + } + } + else { + cellData = ''; + } + // If it looks like there is an HTML entity in the string, + // attempt to decode it so sorting works as expected. Note that + // we could use a single line of jQuery to do this, but the DOM + // method used here is much faster + // https://jsperf.com/html-decode + if (cellData.indexOf && cellData.indexOf('&') !== -1) { + __filter_div.innerHTML = cellData; + cellData = __filter_div_textContent + ? __filter_div.textContent + : __filter_div.innerText; + } + if (cellData.replace) { + cellData = cellData.replace(/[\r\n\u2028]/g, ''); + } + rowFilterData.push(cellData); } + row.searchCellCache = rowFilterData; + row.searchRowCache = rowFilterData.join(' '); + wasInvalidated = true; } } - return vis; + return wasInvalidated; } + /** - * Get an array of column indexes that match a given property + * Render and cache a row's display data for the columns, if required * * @param settings DataTables settings object - * @param param Parameter in the columns array to look for - * @returns Array of indexes with matched properties + * @param rowIdx Row index + * @returns Array with display information */ -function getColumns(settings, param) { - let a = []; - settings.columns.map(function (val, i) { - if (val[param]) { - a.push(i); +function getRowDisplay(settings, rowIdx) { + var rowModal = settings.data[rowIdx]; + var columns = settings.columns; + if (!rowModal) { + return []; + } + if (!rowModal.displayData) { + // Need to render and cache + rowModal.displayData = []; + for (var colIdx = 0, len = columns.length; colIdx < len; colIdx++) { + rowModal.displayData.push(getCellData(settings, rowIdx, colIdx, 'display')); } - }); - return a; + } + return rowModal.displayData; } /** - * Allow the result from a type detection function to be `true` while - * translating that into a string. Old type detection functions will return the - * type name if it passes. An object store would be better, but not backwards - * compatible. + * Create a new TR element (and it's TD children) for a row * - * @param typeDetect Object or function for type detection - * @param res Result from the type detection function - * @returns Type name or false - */ -function _typeResult(typeDetect, res) { - return res === true ? typeDetect._name : res; -} -/** - * Calculate the 'type' of a column * @param settings DataTables settings object + * @param rowIdx Row to consider + * @param trIn TR element to add to the table - optional. If not given, + * DataTables will create a row automatically + * @param tds Array of TD|TH elements for the row - must be given if trIn is. */ -function columnTypes(settings, originalTypes = '') { - var columns = settings.columns; - var data = settings.data; - var types = ext.type.detect; - var i, iLen, j, jen, k, ken; - var col, detectedType, cache; - if (!originalTypes) { - originalTypes = columns.map(c => c.type).join(','); - } - // For each column, spin over the data type detection functions, seeing if - // one matches - for (i = 0, iLen = columns.length; i < iLen; i++) { - col = columns[i]; - cache = []; - if (!col.type && col.typeManual) { - col.type = col.typeManual; - } - else if (!col.type) { - // With SSP type detection can be unreliable and error prone, so we - // provide a way to turn it off. - if (!settings.typeDetect) { - return; +function createTr(settings, rowIdx, trIn, tds) { + var row = settings.data[rowIdx], cells = [], tr, td, column, i, iLen, create, trClass = settings.classes.tbody.row; + if (row && row.tr === null) { + let rowData = row.data; + tr = trIn || document.createElement('tr'); + row.tr = tr; + row.cells = cells; + Dom.s(tr).classAdd(trClass); + // Use a private property on the node to allow reserve mapping from the node + // to the aoData array for fast look up + tr._DT_RowIndex = rowIdx; + // Special parameters can be given by the data source to be used on the + // row + rowAttributes(settings, row); + /* Process each column */ + for (i = 0, iLen = settings.columns.length; i < iLen; i++) { + column = settings.columns[i]; + create = trIn && tds && tds[i] ? false : true; + td = create + ? document.createElement(column.cellType) + : tds[i]; + if (!td) { + log(settings, 0, 'Incorrect column count', 18); } - for (j = 0, jen = types.length; j < jen; j++) { - let typeDetect = types[j]; - let oneOf; - let allOf; - let init; - let one = false; - // There can be either one, or three type detection functions - if (typeof typeDetect === 'function') { - allOf = typeDetect; - } - else { - oneOf = typeDetect.oneOf; - allOf = typeDetect.allOf; - init = typeDetect.init; - } - detectedType = null; - // Fast detect based on column assignment - if (init) { - detectedType = _typeResult(typeDetect, init(settings, col, i)); - if (detectedType) { - col.type = detectedType; - break; - } - } - for (k = 0, ken = data.length; k < ken; k++) { - if (!data[k]) { - continue; - } - // Use a cache array so we only need to get the type data - // from the formatter once (when using multiple detectors) - if (cache[k] === undefined) { - cache[k] = getCellData(settings, k, i, 'type'); - } - // Only one data point in the column needs to match this - // function - if (oneOf && !one) { - one = _typeResult(typeDetect, oneOf(cache[k], settings)); - } - // All data points need to match this function - detectedType = _typeResult(typeDetect, allOf(cache[k], settings)); - // If null, then this type can't apply to this column, so - // rather than testing all cells, break out. There is an - // exception for the last type which is `html`. We need to - // scan all rows since it is possible to mix string and HTML - // types - if (!detectedType && j !== types.length - 3) { - break; - } - // Only a single match is needed for html type since it is - // bottom of the pile and very similar to string - but it - // must not be empty - if (detectedType === 'html' && !util.is.empty(cache[k])) { - break; - } - } - // Type is valid for all data points in the column - use this - // type - if ((oneOf && one && detectedType) || - (!oneOf && detectedType)) { - col.type = detectedType; - break; - } + td._DT_CellIndex = { + row: rowIdx, + column: i + }; + cells.push(td); + var display = getRowDisplay(settings, rowIdx); + // Need to create the HTML if new, or if a rendering function is + // defined + if (create || + ((column.render || column.data !== i) && + (!util.is.plainObject(column.data) || + (column.data && + column.data._ !== i + '.display')))) { + writeCell(td, display[i]); } - // Fall back - if no type was detected, always use string - if (!col.type) { - col.type = 'string'; + // column class + Dom.s(td).classAdd(column.className); + // Visibility - add or remove as required + if (column.visible && create) { + tr.appendChild(td); + } + else if (!column.visible && !create) { + td.parentNode.removeChild(td); + } + if (column.createdCell) { + column.createdCell.call(settings.instance, td, getCellData(settings, rowIdx, i), rowData, rowIdx, i); } } - // Set class names for header / footer for auto type classes - var autoClass = ext.type.className[col.type]; - if (autoClass) { - _columnAutoClass(settings.header, i, autoClass); - _columnAutoClass(settings.footer, i, autoClass); - } - var renderer = ext.type.render[col.type]; - // This can only happen once! There is no way to remove - // a renderer. After the first time the renderer has - // already been set so createTr will run the renderer itself. - if (renderer && !col.renderer) { - col.renderer = util.get(renderer); - _columnAutoRender(settings, i); - } - } - var newTypes = columns.map(c => c.type).join(','); - if (newTypes !== originalTypes) { - callbackFire(settings, null, 'columnTypes', [settings], false); - } -} -/** - * Apply an auto detected renderer to data which doesn't yet have a renderer - */ -function _columnAutoRender(settings, colIdx) { - let data = settings.data; - for (let i = 0; i < data.length; i++) { - let d = data[i]; - if (d && d.tr) { - // We have to update the display here since there is no invalidation - // check for the data - let display = getCellData(settings, i, colIdx, 'display'); - d.displayData[colIdx] = display; - writeCell(d.cells[colIdx], display); - // No need to update sort / filter data since it has been - // invalidated and will be re-read with the renderer now applied - } + callbackFire(settings, 'rowCreated', 'row-created', [ + tr, + rowData, + rowIdx, + cells + ]); + } + else if (row) { + Dom.s(row.tr).classAdd(trClass); } } /** - * Apply a class name to a column's header cells + * Add attributes to a row based on the special `DT_*` parameters in a data + * source object. * - * @param container The header / footer structure array - * @param colIdx Column index - * @param className Class name to apply + * @param settings DataTables settings object + * @param row Row object for the row to be modified */ -function _columnAutoClass(container, colIdx, className) { - container.forEach(function (row) { - if (row[colIdx] && row[colIdx].unique) { - Dom.s(row[colIdx].cell).classAdd(className); +function rowAttributes(settings, row) { + var tr = row.tr; + var data = row.data; + if (tr) { + var id = settings.rowIdFn(data); + if (id) { + tr.id = id; } - }); + if (data.DT_RowClass) { + // Remove any classes added by DT_RowClass before + var a = data.DT_RowClass.split(' '); + row.addedClasses = row.addedClasses + ? util.unique(row.addedClasses.concat(a)) + : a; + Dom.s(tr) + .classRemove(row.addedClasses.join(' ')) + .classAdd(data.DT_RowClass); + } + if (data.DT_RowAttr) { + Dom.s(tr).attr(data.DT_RowAttr); + } + if (data.DT_RowData) { + Dom.s(tr).data(data.DT_RowData); + } + } } /** - * Take the column definitions and static columns arrays and calculate how they - * relate to column indexes. The callback function will then apply the - * definition found for a column to a suitable configuration object. + * Create the HTML header for the table * - * @param settings DataTables settings object - * @param aoColDefs The aoColumnDefs array that is to be applied - * @param aoCols The aoColumns array that defines columns individually - * @param headerLayout Layout for header as it was loaded - * @param fn Callback function - takes two parameters, the calculated column - * index and the definition for that column. + * @param settings DataTable instance + * @param side If the header or footer should be used + * @returns */ -function applyColumnDefs(settings, aoColDefs, aoCols, headerLayout, fn) { - var i, iLen, j, jLen, k, kLen; - var columns = settings.columns; - if (aoCols) { - for (i = 0, iLen = aoCols.length; i < iLen; i++) { - // Compat - if (aoCols[i] && aoCols[i].name) { - columns[i].name = aoCols[i].name; - } - } +function buildHead(settings, side) { + let classes = settings.classes; + let columns = settings.columns; + let i, iLen, row; + let target = Dom.s(side === 'header' ? settings.thead : settings.tfoot); + let titleProp = side === 'header' ? 'title' : side; + // Footer might be defined + if (!target) { + return; } - // Column definitions with aTargets - if (aoColDefs) { - // Loop over the definitions array - loop in reverse so first instance - // has priority - for (i = aoColDefs.length - 1; i >= 0; i--) { - let def = aoColDefs[i]; - /* Each definition can target multiple columns, as it is an array */ - let aTargets = def.target !== undefined - ? def.target - : def.targets !== undefined - ? def.targets - : def.aTargets; // legacy - if (!Array.isArray(aTargets)) { - aTargets = [aTargets]; - } - for (j = 0, jLen = aTargets.length; j < jLen; j++) { - var target = aTargets[j]; - if (typeof target === 'number' && target >= 0) { - /* Add columns that we don't yet know about */ - while (columns.length <= target) { - addColumn(settings); - } - /* Integer, basic index */ - fn(target, def); - } - else if (typeof target === 'number' && target < 0) { - /* Negative integer, right to left column counting */ - fn(columns.length + target, def); - } - else if (typeof target === 'string') { - for (k = 0, kLen = columns.length; k < kLen; k++) { - if (target === '_all') { - // Apply to all columns - fn(k, def); - } - else if (target.indexOf(':name') !== -1) { - // Column selector - if (columns[k].name === target.replace(':name', '')) { - fn(k, def); - } - } - else { - // Cell selector - headerLayout.forEach(function (row) { - if (row[k]) { - var cell = row[k].cell; - // Legacy support. Note that it means that - // we don't support an element name selector - // only, since they are treated as class - // names for 1.x compat. - if (target.match(/^[a-z][\w-]*$/i)) { - target = '.' + target; - } - if (cell.matches(target)) { - fn(k, def); - } - } - }); - } - } - } + // If no cells yet and we have content for them, then create + if (side === 'header' || + util.array.pluck(settings.columns, titleProp).join('')) { + row = target.find('tr'); + // Add a row if needed + if (!row.count()) { + row = Dom.c('tr').appendTo(target); + } + // Add the number of cells needed to make up to the number of columns + if (row.count() === 1) { + let cellCount = 0; + row.find('td, th').each(el => { + cellCount += el.colSpan; + }); + for (i = cellCount, iLen = columns.length; i < iLen; i++) { + Dom.c('th') + .html(columns[i][titleProp] || '') + .appendTo(row); } } } - // Statically defined columns array - if (aoCols) { - for (i = 0, iLen = aoCols.length; i < iLen; i++) { - fn(i, aoCols[i]); - } + let detected = detectHeader(settings, target.get(0), true); + if (side === 'header') { + settings.header = detected; + target.find('tr').classAdd(classes.thead.row); + } + else { + settings.footer = detected; + target.find('tr').classAdd(classes.tfoot.row); } + // Every cell needs to be passed through the renderer + target + .children('tr') + .children('th, td') + .each(el => { + // Should just be able to do `renderer(settings, side)` here but + // Typescript doesn't like it, despite it already being constrained! + let runner = side === 'header' + ? renderer(settings, 'header') + : renderer(settings, 'footer'); + runner(settings, Dom.s(el), classes); + }); } /** - * Get the width for a given set of columns + * Build a layout structure for a header or footer * - * @param settings DataTables settings object - * @param targets Columns - comma separated string or array of numbers - * @param original Use the original width (true) or calculated (false) - * @param incVisible Include visible columns (true) or not (false) - * @returns Combined CSS value + * @param settings DataTables settings + * @param source Source layout array + * @param incColumns What columns should be included + * @returns Layout array in column index order */ -function columnsSumWidth(settings, targets, original, incVisible) { - if (!Array.isArray(targets)) { - targets = columnsFromHeader(targets); +function headerLayout(settings, source, incColumns) { + var row, column, cell; + var local = []; + var structure = []; + var columns = settings.columns; + var columnCount = columns.length; + var rowspan, colspan; + if (!source) { + return; } - let sum = 0; - let unit = 'px'; - let columns = settings.columns; - for (let i = 0, iLen = targets.length; i < iLen; i++) { - let column = columns[targets[i]]; - let definedWidth = original ? column.widthOrig : column.width; - if (column.visible === false) { - continue; - } - if (definedWidth === null || definedWidth === undefined) { - return null; // can't determine a defined width - browser defined - } - else if (typeof definedWidth === 'number') { - sum += definedWidth; - } - else { - let matched = definedWidth.match(/([\d\.]+)([^\d]*)/); - if (matched) { - sum += parseFloat(matched[1]); - unit = matched.length === 3 ? matched[2] : 'px'; + // Default is to work on only visible columns + if (!incColumns) { + incColumns = util.array.range(columnCount).filter(function (idx) { + return columns[idx].visible; + }); + } + // Make a copy of the master layout array, but with only the columns we want + for (row = 0; row < source.length; row++) { + // Remove any columns we haven't selected + local[row] = source[row].slice().filter(function (c, i) { + return incColumns.includes(i); + }); + // Prep the structure array - it needs an element for each row + structure.push([]); + } + for (row = 0; row < local.length; row++) { + for (column = 0; column < local[row].length; column++) { + rowspan = 1; + colspan = 1; + // Check to see if there is already a cell (row/colspan) covering + // our target insert point. If there is, then there is nothing to + // do. + if (structure[row][column] === undefined) { + cell = local[row][column].cell; + // Expand for rowspan + while (local[row + rowspan] !== undefined && + local[row][column].cell == local[row + rowspan][column].cell) { + structure[row + rowspan][column] = null; + rowspan++; + } + // And for colspan + while (local[row][column + colspan] !== undefined && + local[row][column].cell == local[row][column + colspan].cell) { + // Which also needs to go over rows + for (var k = 0; k < rowspan; k++) { + structure[row + k][column + colspan] = null; + } + colspan++; + } + var titleSpan = Dom.s(cell).find('.dt-column-title'); + structure[row][column] = { + cell: cell, + colspan: colspan, + rowspan: rowspan, + title: titleSpan.count() + ? titleSpan.html() + : Dom.s(cell).html() + }; } } } - return sum + unit; + return structure; } /** - * Determine what columns a header cell covers (can be multiple for colspan - * cases). + * Draw the header (or footer) element based on the column visibility states. * - * @param cell The header cell in question - * @returns An array of column indexes + * @param settings DataTables settings object + * @param source Layout array from detectHeader */ -function columnsFromHeader(cell) { - let attr = Dom.s(cell).closest('[data-dt-column]').attr('data-dt-column'); - if (!attr) { - return []; +function drawHead(settings, source) { + let layout = headerLayout(settings, source); + let tr; + if (!layout) { + return; } - return attr.split(',').map(function (val) { - return parseInt(val); - }); -} - -/** - * Generate the node required for the processing node - * - * @param ctx DataTables settings object - */ -function processingHtml(ctx) { - var table = ctx.table; - var scrolling = ctx.scroll.x !== '' || ctx.scroll.y !== ''; - if (ctx.features.processing) { - var n = Dom - .c('div') - .attr('id', ctx.tableId + '_processing') - .attr('role', 'status') - .classAdd(ctx.classes.processing.container) - .html(ctx.language.processing) - .append(Dom - .c('div') - .append(Dom.c('div')) - .append(Dom.c('div')) - .append(Dom.c('div')) - .append(Dom.c('div'))); - // Different positioning depending on if scrolling is enabled or not - if (scrolling) { - n.prependTo(Dom.s(ctx.tableWrapper).find('div.dt-scroll').get(0)); + for (let row = 0; row < source.length; row++) { + tr = source[row].row; + // All cells are going to be replaced, so empty out the row + if (tr) { + Dom.s(tr).detachChildren(); } - else { - n.insertBefore(table); + for (let column = 0; column < layout[row].length; column++) { + let point = layout[row][column]; + if (point) { + Dom.s(point.cell) + .appendTo(tr) + .attr('rowspan', point.rowspan) + .attr('colspan', point.colspan); + } } - Dom.s(table).on('processing.dt.DT', (e, s, show) => { - n.css('display', show ? 'block' : 'none'); - }); } } /** - * Display or hide the processing indicator + * Insert the required TR nodes into the table for display * - * @param ctx DataTables settings object - * @param show Show the processing indicator (true) or not (false) + * @param settings DataTables settings object + * @param ajaxComplete true after ajax call to complete rendering */ -function processingDisplay(ctx, show) { - // Ignore cases when we are still redrawing - if (ctx.doingDraw && show === false) { +function draw(settings, ajaxComplete) { + // Allow for state saving and a custom start position + setStartPosition(settings); + // Provide a pre-callback function which can be used to cancel the draw is + // false is returned + var aPreDraw = callbackFire(settings, 'preDraw', 'preDraw', [settings]); + if (aPreDraw.indexOf(false) !== -1) { + processingDisplay(settings, false); return; } - callbackFire(ctx, null, 'processing', [ctx, show]); -} -/** - * Show the processing element if an action takes longer than a given time - * - * @param ctx DataTables settings object - * @param enable Do (true) or not (false) async processing (local feature enablement) - * @param run Function to run - */ -function processingRun(ctx, enable, run) { - if (!enable) { - // Immediate execution, synchronous - run(); - } - else { - processingDisplay(ctx, true); - // Allow the processing display to show if needed - setTimeout(function () { - run(); - processingDisplay(ctx, false); - }, 0); + var rowEls = []; + var rowCount = 0; + var isServerSide = dataSource(settings) == 'ssp'; + var display = settings.display; + var start = settings.displayStart; + var end = displayEnd(settings); + var columns = settings.columns; + var body = Dom.s(settings.tbody); + settings.doingDraw = true; + /* Server-side processing draw intercept */ + if (settings.deferLoading) { + settings.deferLoading = false; + settings.drawCount++; + processingDisplay(settings, false); } -} - -function renderer(ctx, type) { - var render = ctx.renderer; - var host = ext.renderer[type]; - if (plainObject(render) && render[type]) { - // Specific renderer for this type. If available use it, otherwise use - // the default. - return host[render[type]] || host._; + else if (!isServerSide) { + settings.drawCount++; } - else if (typeof render === 'string') { - // Common renderer - if there is one available for this type use it, - // otherwise use the default - return host[render] || host._; + else if (!settings.destroying && !ajaxComplete) { + // Show loading message for server-side processing + if (settings.drawCount === 0) { + body.empty().append(_emptyRow(settings)); + } + ajaxUpdate(settings); + return; } - // Use the default - return host._; -} - -/** - * Add the options to the page HTML for the table - * - * @param ctx DataTables context - */ -function createLayout(ctx) { - var classes = ctx.classes; - // Wrapper div around everything DataTables controls - var insert = Dom - .c('div') - .attr('id', ctx.tableId + '_wrapper') - .classAdd(classes.container) - .insertBefore(ctx.table); - ctx.tableWrapper = insert.get(0); - if (ctx.dom) { - // Legacy - legacyDom(ctx, ctx.dom, insert); + if (display.length !== 0) { + var iStart = isServerSide ? 0 : start; + var iEnd = isServerSide ? settings.data.length : end; + for (var j = iStart; j < iEnd; j++) { + var dataIdx = display[j]; + var data = settings.data[dataIdx]; + // Row has been deleted - can't be displayed + if (data === null) { + continue; + } + // Row node hasn't been created yet + if (data.tr === null) { + createTr(settings, dataIdx); + } + var nRow = data.tr; + // Add various classes as needed + for (var i = 0; i < columns.length; i++) { + var col = columns[i]; + var td = data.cells[i]; + Dom.s(td) + .classAdd(col.type ? ext.type.className[col.type] : null) // auto class + .classAdd(settings.classes.tbody.cell); // all cells + } + // Row callback functions - might want to manipulate the row + // rowCount and j are not currently documented. Are they at all + // useful? + callbackFire(settings, 'row', null, [ + nRow, + data.data, + rowCount, + j, + dataIdx + ]); + rowEls.push(nRow); + rowCount++; + } } else { - var top = convert(ctx, ctx.layout, 'top'); - var bottom = convert(ctx, ctx.layout, 'bottom'); - var render = renderer(ctx, 'layout'); - // Everything above - the renderer will actually insert the contents into the document - top.forEach(function (item) { - render(ctx, insert, item); - }); - // The table - always the center of attention - render(ctx, insert, { - full: { - contents: [featureTable(ctx)], - items: [], - table: true - } - }); - // Everything below - bottom.forEach(function (item) { - render(ctx, insert, item); - }); + rowEls[0] = _emptyRow(settings); } - // Processing floats on top, so it isn't an inserted feature - processingHtml(ctx); + /* Header and footer callbacks */ + callbackFire(settings, 'header', 'header', [ + Dom.s(settings.thead).children('tr').get(0), + getDataMaster(settings), + start, + end, + display + ]); + callbackFire(settings, 'footer', 'footer', [ + Dom.s(settings.tfoot).children('tr').get(0), + getDataMaster(settings), + start, + end, + display + ]); + body.detachChildren().append(rowEls); + // Empty table needs a specific class + Dom.s(settings.tableWrapper).classToggle('dt-empty-footer', Dom.s(settings.tfoot).find('tr').count() === 0); + // Call all required callback functions for the end of a draw + callbackFire(settings, 'draw', 'draw', [settings], true); + // Draw is complete, sorting and filtering must be as well + settings.wasOrdered = false; + settings.wasFiltered = false; + settings.doingDraw = false; } /** - * Expand the layout items into an object for the rendering function + * Redraw the table - taking account of the various features which are enabled + * + * @param settings DataTables settings object + * @param holdPosition Keep the current paging position. By default the paging + * is reset to the first page + * @param recompute Indicate if a rebuild of sort and filter should happen */ -function layoutItems(row, align, items) { - if (Array.isArray(items)) { - for (var i = 0; i < items.length; i++) { - layoutItems(row, align, items[i]); +function reDraw(settings, holdPosition, recompute) { + let features = settings.features, doSort = features.ordering, doFilter = features.searching; + if (recompute === undefined || recompute === true) { + // Resolve any column types that are unknown due to addition or + // invalidation + columnTypes(settings); + columnWidths(settings); + if (doSort) { + sort(settings); } - return; - } - var rowCell = row[align]; // can't be undefined - will have been created by getRow - // If it is an object, then there can be multiple features contained in it - if (util.is.plainObject(items)) { - // Is it an cell object already, with rowId, etc. A feature plugin cannot - // be named "features" due to this check - if (items.features) { - if (items.rowId) { - row.id = items.rowId; - } - if (items.rowClass) { - row.className = items.rowClass; - } - rowCell.id = items.id; - rowCell.className = items.className; - layoutItems(row, align, items.features); + if (doFilter) { + filterComplete(settings); } else { - // An object of features and configuration options - e.g. `{paging: {startEnd: false}}` - util.object.each(items, (key, val) => { - rowCell.items.push({ - feature: key, - opts: val - }); - }); + // No filtering, so we want to just use the display master + settings.display = settings.displayMaster.slice(); } } + if (holdPosition !== true) { + settings.displayStart = 0; + } else { - // Otherwise, it is a function, node or Dom / jQuery instance and can just get added - rowCell.items.push(items); + // Keep position, but make sure that there is actually data to display, + // otherwise we need to rewind a bit (e.g. if rows were deleted) + lengthOverflow(settings); } + // Let any modules know about the draw hold position state (used by + // scrolling internally) + settings.drawHold = holdPosition; + draw(settings); + settings.api.one('draw', function () { + settings.drawHold = false; + }); } /** - * Find, or create a layout row and setup a target cell in it + * Table is empty - create a row with an empty message in it * - * @param rows Rows array to search for the target row. Is mutated when a row is - * added if not found. - * @param rowNum Row index to get - * @param align Where the cell position is - * @returns The row + * @param settings DataTables context */ -function getRow(rows, rowNum, align) { - var row; - // Find existing rows - for (var i = 0; i < rows.length; i++) { - row = rows[i]; - if (row.rowNum === rowNum) { - // full is on its own, but start and end share a row - if ((align === 'full' && row.full) || - ((align === 'start' || align === 'end') && - (row.start || row.end))) { - if (!row[align]) { - row[align] = { - contents: [], - items: [] - }; +function _emptyRow(settings) { + let lang = settings.language; + let zero = lang.zeroRecords; + let dataSrc = dataSource(settings); + // Make use of the fact that settings.json is only set once the initial data + // has been loaded. Show loading when that isn't the case + if ((dataSrc === 'ssp' || dataSrc === 'ajax') && !settings.json) { + zero = lang.loadingRecords; + } + else if (lang.emptyTable && recordsTotal(settings) === 0) { + zero = lang.emptyTable; + } + return Dom + .c('tr') + .append(Dom + .c('td') + .attr('colSpan', visibleColumns(settings)) + .classAdd(settings.classes.empty.row) + .html(zero)) + .get(0); +} +/** + * Use the DOM source to create up an array of header cells. The idea here is to + * create a layout grid (array) of rows x columns, which contains a reference to + * the cell at that point in the grid (regardless of col/rowspan), such that any + * column / row could be removed and the new grid constructed. + * + * @param settings DataTables context + * @param thead thead / tbody element + * @param write If cells should be written (if required) + * @returns Calculated layout array + */ +function detectHeader(settings, thead, write) { + let columns = settings.columns; + let rows = Dom.s(thead).children('tr'); + let row, loopCell; + let i, k, l, len, shifted, column, colspan, rowspan; + let titleRow = settings.titleRow; + let isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; + let layout = []; + let isUnique; + let shift = function (a, b, j) { + let d = a[b]; + while (d[j]) { + j++; + } + return j; + }; + // We know how many rows there are in the layout - so prep it + for (i = 0, len = rows.count(); i < len; i++) { + layout.push([]); + } + for (i = 0, len = rows.count(); i < len; i++) { + row = rows.get(i); + column = 0; + // For every cell in the row.. + loopCell = row.firstChild; + while (loopCell) { + if (loopCell.nodeName.toUpperCase() == 'TD' || + loopCell.nodeName.toUpperCase() == 'TH') { + let cell = Dom.s(loopCell); + let cols = []; + // Get the col and rowspan attributes from the DOM and sanitise + // them + colspan = parseInt(cell.attr('colspan') || '1') || 1; + rowspan = parseInt(cell.attr('rowspan') || '1') || 1; + colspan = + !colspan || colspan === 0 || colspan === 1 ? 1 : colspan; + rowspan = + !rowspan || rowspan === 0 || rowspan === 1 ? 1 : rowspan; + // There might be colspan cells already in this row, so shift + // our target accordingly + shifted = shift(layout, i, column); + // Cache calculation for unique columns + isUnique = colspan === 1 ? true : false; + // Perform header setup + if (write) { + if (isUnique) { + // Allow column options to be set from HTML attributes + columnOptions(settings, shifted, escapeObject(cell.data())); + // Get the width for the column. This can be defined + // from the width attribute, style attribute or + // `columns.width` option + let columnDef = columns[shifted]; + let width = cell.attr('width') || null; + let t = cell + .get(0) + .style.width.match(/width:\s*(\d+[pxem%]+)/); + if (t) { + width = t[1]; + } + columnDef.widthOrig = columnDef.width || width; + if (isHeader) { + // Column title handling - can be user set, or read + // from the DOM This happens before the render, so + // the original is still in place + if (columnDef.title !== null && + !columnDef.autoTitle) { + if ((titleRow === true && i === 0) || // top row + (titleRow === false && + i === rows.count() - 1) || // bottom row + titleRow === i || // specific row + titleRow === null) { + cell.html(columnDef.title); + } + } + if (!columnDef.title && isUnique) { + columnDef.title = util.string.stripHtml(cell.html()); + columnDef.autoTitle = true; + } + } + else { + // Footer specific operations + if (columnDef.footer) { + cell.html(columnDef.footer); + } + } + // Fall back to the aria-label attribute on the table + // header if no ariaTitle is provided. + if (!columnDef.ariaTitle) { + columnDef.ariaTitle = + cell.attr('aria-label') || columnDef.title; + } + // Column specific class names + if (columnDef.className) { + cell.classAdd(columnDef.className); + } + } + // Wrap the column title so we can write to it in future + if (cell.find('div.dt-column-title').count() === 0) { + Dom.c('div') + .classAdd('dt-column-title') + .append(Array.from(cell.get(0).childNodes)) + .appendTo(cell); + } + if (settings.orderIndicators && + isHeader && + cell.filter(':not([data-dt-order=disable])').count() !== + 0 && + cell.parent(':not([data-dt-order=disable])').count() !== + 0 && + cell.find('div.dt-column-order').count() === 0) { + Dom.c('div') + .classAdd('dt-column-order') + .appendTo(cell); + } + // We need to wrap the elements in the header in another + // element to use flexbox layout for those elements + var headerFooter = isHeader ? 'header' : 'footer'; + if (cell.find('div.dt-column-' + headerFooter).count() === + 0) { + Dom.c('div') + .classAdd('dt-column-' + headerFooter) + .append(Array.from(cell.get(0).childNodes)) + .appendTo(cell); + } + } + // If there is col / rowspan, copy the information into the + // layout grid + for (l = 0; l < colspan; l++) { + for (k = 0; k < rowspan; k++) { + layout[i + k][shifted + l] = { + cell: cell.get(0), + unique: isUnique + }; + layout[i + k].row = row; + } + cols.push(shifted + l); } - return row; + // Assign an attribute so spanning cells can still be identified + // as belonging to a column + cell.attr('data-dt-column', util.unique(cols).join(',')); } + loopCell = loopCell.nextSibling; } } - // If we get this far, then there was no match, create a new row - row = { - rowNum: rowNum - }; - row[align] = { - contents: [], - items: [] - }; - rows.push(row); - return row; + return layout; } /** - * Convert a `layout` object given by a user to the object structure needed - * for the renderer. This is done twice, once for above and once for below - * the table. Ordering must also be considered. + * Set the start position for draw * * @param settings DataTables settings object - * @param layout Layout object to convert - * @param side `top` or `bottom` - * @returns Converted array structure - one item for each row. */ -function convert(settings, layout, side) { - var rows = []; - // Split out into an array - util.object.each(layout, function (pos, items) { - var parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/); - if (items === null || !parts) { - return; - } - var rowNum = parts[2] ? parseInt(parts[2]) : 0; - var align = parts[3] ? parts[3].toLowerCase() : 'full'; - // Filter out the side we aren't interested in - if (parts[1] !== side) { - return; - } - // Only really a type check - if (align !== 'full' && align !== 'start' && align !== 'end') { - return; - } - // Get or create the row we should attach to - var row = getRow(rows, rowNum, align); - layoutItems(row, align, items); - }); - // Order by item identifier - rows.sort(function (a, b) { - var order1 = a.rowNum || 0; - var order2 = b.rowNum || 0; - // If both in the same row, then the row with `full` comes first - if (order1 === order2) { - var ret = a.full && !b.full ? -1 : 1; - return side === 'bottom' ? ret * -1 : ret; - } - return order2 - order1; - }); - // Invert for below the table - if (side === 'bottom') { - rows.reverse(); - } - for (var row = 0; row < rows.length; row++) { - delete rows[row].rowNum; - resolve(settings, rows[row]); +function setStartPosition(settings) { + var bServerSide = dataSource(settings) == 'ssp'; + var iInitDisplayStart = settings.displayStartInit; + // Check and see if we have an initial draw position from state saving + if (iInitDisplayStart !== undefined && iInitDisplayStart !== -1) { + settings.displayStart = bServerSide + ? iInitDisplayStart + : iInitDisplayStart >= recordsDisplay(settings) + ? 0 + : iInitDisplayStart; + settings.displayStartInit = -1; } - return rows; } /** - * Convert the contents of a row's layout object to nodes that can be inserted - * into the document by a renderer. Execute functions, look up plug-ins, etc. + * Get the number of records in the current record set, before filtering * - * @param settings DataTables settings object - * @param row Layout object for this row + * @param ctx DataTables settings object */ -function resolve(settings, row) { - var getFeature = function (feature, opts) { - if (!ext.features[feature]) { - log(settings, 0, 'Unknown feature: ' + feature); - } - return ext.features[feature].apply(this, [settings, opts]); - }; - // Resolve items in the `contents` array from being an identifier, such as - // the name of a feature, into the node to display. - var resolve = function (item) { - if (!row[item]) { - return; - } - row[item].contents = row[item].items - .filter(item => !!item) - .map(item => { - if (typeof item === 'string') { - return getFeature(item, null); - } - else if (util.is.plainObject(item)) { - // If it's an object, it just has feature and opts properties from - // the transform in _layoutArray - return getFeature(item.feature, item.opts); - } - else if (typeof item.node === 'function') { - return item.node(settings); - } - else if (typeof item === 'function') { - var inst = item(settings); - return typeof inst.node === 'function' ? inst.node() : inst; - } - else if (item.nodeName) { - // An HTML element - return item; - } - else if (item instanceof Dom) { - return item.get(0); - } - else if (item.length) { - // Possibly jQuery - return item[0]; - } - }); - }; - resolve('start'); - resolve('end'); - resolve('full'); +function recordsTotal(ctx) { + return dataSource(ctx) == 'ssp' + ? ctx.recordsTotal * 1 + : ctx.displayMaster.length; } /** - * Draw the table with the legacy DOM property + * Get the number of records in the current record set, after filtering * - * @param settings DT settings instance - * @param layout DOM string - * @param insert Insert point + * @param ctx DataTables settings object */ -function legacyDom(settings, layout, insert) { - let parts = layout.match(/(".*?")|('.*?')|./g); - let featureNode, option, newNode, next, attr; - if (!parts) { - return; +function recordsDisplay(ctx) { + return dataSource(ctx) == 'ssp' + ? ctx.recordsDisplay * 1 + : ctx.display.length; +} +/** + * Get the display end point - display index + * + * @param ctx DataTables settings object + */ +function displayEnd(ctx) { + var len = ctx.pageLength, start = ctx.displayStart, calc = start + len, records = ctx.display.length, features = ctx.features, paginate = features.paging; + if (features.serverSide) { + return paginate === false || len === -1 + ? start + records + : Math.min(start + len, ctx.recordsDisplay); } - for (let i = 0; i < parts.length; i++) { - featureNode = null; - option = parts[i]; - if (option == '<') { - // New container div - newNode = Dom.c('div'); - // Check to see if we should append an id and/or a class name to the container - next = parts[i + 1]; - if (next[0] == "'" || next[0] == '"') { - attr = next.replace(/['"]/g, ''); - let id = '', className; - /* The attribute can be in the format of "#id.class", "#id" or "class" This logic - * breaks the string into parts and applies them as needed - */ - if (attr.indexOf('.') != -1) { - let split = attr.split('.'); - id = split[0]; - className = split[1]; - } - else if (attr[0] == '#') { - id = attr; - } - else { - className = attr; - } - newNode.attr('id', id.substring(1)).classAdd(className); - i++; // Move along the position array - } - insert.append(newNode.get()); // TODO - insert = newNode; + else { + return !paginate || calc > records || len === -1 ? records : calc; + } +} + +/** + * Log an error message + * + * @param ctx DataTables settings object + * @param level log error messages, or display them to the user + * @param msg error message + * @param tn Technical note id to get more information about the error. + */ +function log(ctx, level, msg, tn) { + msg = + 'DataTables warning: ' + + (ctx ? 'table id=' + ctx.tableId + ' - ' : '') + + msg; + if (tn) { + msg += + '. For more information about this error, please see ' + + 'https://datatables.net/tn/' + + tn; + } + { + // Backwards compatibility pre 1.10 + var type = ext.sErrMode || ext.errMode; + if (ctx) { + callbackFire(ctx, null, 'dt-error', [ctx, tn, msg], true); } - else if (option == '>') { - // End container div - insert = insert.parent(); + if (type == 'alert') { + alert(msg); } - else if (option == 't') { - // Table - featureNode = featureTable(settings); + else if (type == 'throw') { + throw new Error(msg); } - else { - ext.feature.forEach(function (feature) { - if (option == feature.cFeature) { - featureNode = feature.fnInit(settings); - } - }); + else if (typeof type == 'function') { + type(ctx, tn, msg); } - // Add to the display - if (featureNode) { - // TODO when doing the full dom update, won't need this check - insert.append(featureNode instanceof Dom ? featureNode.get() : featureNode); + } +} +/** + * See if a property is defined on one object, if so assign it to the other + * object + * + * @param ret target object + * @param src source object + * @param name property + * @param mappedName name to map too - optional, name used if not given + */ +function map(ret, src, name, mappedName) { + if (Array.isArray(name)) { + for (let i = 0; i < name.length; i++) { + let val = name[i]; + if (Array.isArray(val)) { + map(ret, src, val[0], val[1]); + } + else { + map(ret, src, val); + } } + return; } -} - -function sortInit(settings) { - var target = settings.thead; - var headerRows = target.querySelectorAll('tr'); - var titleRow = settings.titleRow; - var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; - // Legacy support for `orderCellsTop` - if (titleRow === true) { - target = headerRows[0]; - } - else if (titleRow === false) { - target = headerRows[headerRows.length - 1]; - } - else if (titleRow !== null) { - target = headerRows[titleRow]; + if (mappedName === undefined) { + mappedName = name; } - // else - all rows - if (settings.orderHandler) { - sortAttachListener(settings, target, target === settings.thead - ? 'tr' + - notSelector + - ' th' + - notSelector + - ', tr' + - notSelector + - ' td' + - notSelector - : 'th' + notSelector + ', td' + notSelector); + if (src[name] !== undefined) { + ret[mappedName] = src[name]; } - // Need to resolve the user input array into our internal structure - var order = []; - sortResolve(settings, order, settings.order); - settings.order = order; } /** - * Attach event listeners to a node that will trigger ordering on a column + * Bind an event handler to allow a click or return key to activate the callback. + * This is good for accessibility since a return on the keyboard will have the + * same effect as a click, if the element has focus. * - * @param settings DataTables context - * @param node Node to attach to - * @param selector Delegate selector - * @param column Column index to target - * @param callback Callback for when done + * @param n Element to bind the action to + * @param selector Selector (for delegated events) + * @param fn Callback function for when the event is triggered */ -function sortAttachListener(settings, node, selector, column, callback) { - bindAction(node, selector, function (e) { - var run = false; - var columns = column === undefined - ? columnsFromHeader(e.target) - : typeof column === 'function' - ? column() - : Array.isArray(column) - ? column - : [column]; - if (columns.length) { - for (var i = 0, iLen = columns.length; i < iLen; i++) { - var ret = sortAdd(settings, columns[i], i, e.shiftKey); - if (ret !== false) { - run = true; - } - // If the first entry is no sort, then subsequent - // sort columns are ignored - if (settings.order.length === 1 && - settings.order[0][1] === '') { - break; - } - } - if (run) { - processingRun(settings, true, function () { - sort(settings); - sortDisplay(settings, settings.display); - reDraw(settings, false, false); - if (callback) { - callback(); - } - }); - } +function bindAction(n, selector, fn) { + Dom.s(n) + .on('click.DT', selector, function (e) { + fn(e); + }) + .on('keypress.DT', selector, function (e) { + if (e.which === 13) { + e.preventDefault(); + fn(e); } + }) + .on('selectstart.DT', selector, function () { + // Don't want a double click resulting in text selection + return false; }); } /** - * Sort the display array to match the master's order + * Register a callback function. Easily allows a callback function to be added + * to an array store of callback functions that can then all be called together. * - * @param settings DataTables context - * @param display The display array + * @param settings dataTables settings object + * @param store Name of the array storage for the callbacks in settings + * @param fn Function to be called back */ -function sortDisplay(settings, display) { - if (display.length < 2) { - return; - } - var master = settings.displayMaster; - var masterMap = {}; - var map = {}; - var i; - // Rather than needing an `indexOf` on master array, we can create a map - for (i = 0; i < master.length; i++) { - masterMap[master[i]] = i; - } - // And then cache what would be the indexOf from the display - for (i = 0; i < display.length; i++) { - map[display[i]] = masterMap[display[i]]; +function callbackReg(ctx, store, fn) { + if (fn) { + ctx.callbacks[store].push(fn); } - display.sort(function (a, b) { - // Short version of this function is simply `master.indexOf(a) - master.indexOf(b);` - return map[a] - map[b]; - }); } /** - * Convert the API variants that can be used for defining the order into our - * internal OrderColumn array. + * Fire callback functions and trigger events. Note that the loop over the + * callback array store is done backwards! Further note that you do not want to + * fire off triggers in time sensitive applications (for example cell creation) + * as its slow. * - * @param settings DataTable context object - * @param nestedSort Array to write the resolve values to - * @param sortItem Source object / array from user (It is really an `Order` - * but due to `aaSorting` being used for input and the internal structure - * it is currently any). - * @todo Split aaSorting into unresolved and resolved parameters (in state.ts as - * well) + * @param ctx DataTables settings object + * @param callbackArr Name of the array storage for the callbacks in the context + * @param eventName Name of the custom event to trigger. If null no trigger is + * fired + * @param args Array of arguments to pass to the callback function / trigger + * @param bubbles True if the event should bubble */ -function sortResolve(settings, nestedSort, sortItem // TODO typing -) { - var push = function (a) { - if (plainObject(a)) { - let orderIdx = a; - let orderName = a; - if (orderIdx.idx !== undefined) { - // Index based ordering - nestedSort.push([orderIdx.idx, orderIdx.dir]); - } - else if (orderName.name) { - // Name based ordering - var cols = pluck(settings.columns, 'name'); - var idx = cols.indexOf(orderName.name); - if (idx !== -1) { - nestedSort.push([idx, orderName.dir]); - } - } - } - else { - // Plain column index and direction pair - nestedSort.push(a); - } - }; - if (plainObject(sortItem)) { - // Object - push(sortItem); - } - else if (Array.isArray(sortItem) && typeof sortItem[0] === 'number') { - // 1D array - push(sortItem); +function callbackFire(ctx, callbackArr, eventName, args, bubbles = false) { + var ret = []; + if (callbackArr) { + ret = ctx.callbacks[callbackArr] + .slice() + .reverse() + .map(function (val) { + return val.apply(ctx.instance, args); + }); } - else if (Array.isArray(sortItem)) { - // 2D array - for (var z = 0; z < sortItem.length; z++) { - push(sortItem[z]); // Object or array + if (eventName !== null) { + let table = Dom.s(ctx.table); + let result = table.trigger(eventName + '.dt', bubbles, args, { + dt: ctx.api + }); + // If not yet attached to the document, trigger the event + // on the body directly to sort of simulate the bubble + if (bubbles && table.closest('body').count() === 0) { + Dom.s('body').trigger(eventName + '.dt', bubbles, args, { + dt: ctx.api + }); } + ret.push(result[0]); } + return ret; } -function sortFlatten(settings) { - var i, k, kLen, aSort = [], extSort = ext.type.order, aoColumns = settings.columns, dataSort, colIdx, type, srcCol, fixed = settings.orderFixed, fixedObj = plainObject(fixed), nestedSort = []; - if (!settings.features.ordering) { - return aSort; - } - // Build the sort array, with pre-fix and post-fix options if they have been - // specified - if (Array.isArray(fixed)) { - sortResolve(settings, nestedSort, fixed); - } - if (fixedObj && fixed.pre) { - sortResolve(settings, nestedSort, fixed.pre); +function lengthOverflow(ctx) { + var start = ctx.displayStart, end = displayEnd(ctx), len = ctx.pageLength; + // If we have space to show extra rows (backing up from the end point - then + // do so + if (start >= end) { + start = end - len; } - sortResolve(settings, nestedSort, settings.order); - if (fixedObj && fixed.post) { - sortResolve(settings, nestedSort, fixed.post); + // Keep the start record on the current page + start -= start % len; + if (len === -1 || start < 0) { + start = 0; } - for (i = 0; i < nestedSort.length; i++) { - srcCol = nestedSort[i][0]; - if (aoColumns[srcCol]) { - dataSort = aoColumns[srcCol].orderData; - for (k = 0, kLen = dataSort.length; k < kLen; k++) { - colIdx = dataSort[k]; - type = aoColumns[colIdx].type || 'string'; - if (nestedSort[i]._idx === undefined) { - nestedSort[i]._idx = aoColumns[colIdx].orderSequence.indexOf(nestedSort[i][1]); - } - if (nestedSort[i][1]) { - aSort.push({ - src: srcCol, - col: colIdx, - dir: nestedSort[i][1], - index: nestedSort[i]._idx, - type: type, - formatter: extSort[type + '-pre'], - sorter: extSort[type + '-' + nestedSort[i][1]] - }); - } - } - } + ctx.displayStart = start; +} +/** + * Detect the data source being used for the table. Used to simplify the code a + * little (ajax) and to make it compress a little smaller. + * + * @param ctx DataTables settings object + * @returns Data source + */ +function dataSource(ctx) { + if (ctx.features.serverSide) { + return 'ssp'; } - return aSort; + else if (ctx.ajax) { + return 'ajax'; + } + return 'dom'; } /** - * Change the order of the table + * Common replacement for language strings * * @param ctx DataTables settings object - * @param col Column to perform sort on - * @param dir Direction to sort on + * @param str String with values to replace + * @param entries Plural number for _ENTRIES_ - can be undefined + * @returns String */ -function sort(ctx, col, dir) { - var i, iLen, aiOrig = [], extSort = ext.type.order, data = ctx.data, sortCol, displayMaster = ctx.displayMaster, aSort; - // Make sure the columns all have types defined - columnTypes(ctx); - // Allow a specific column to be sorted, which will _not_ alter the display - // master - if (col !== undefined) { - var srcCol = ctx.columns[col]; - aSort = [ - { - src: col, - col: col, - dir: dir || '', - index: 0, - type: srcCol.type, - formatter: extSort[srcCol.type + '-pre'], - sorter: extSort[srcCol.type + '-' + dir] - } - ]; - displayMaster = displayMaster.slice(); +function macros(ctx, str, entries) { + // When infinite scrolling, we are always starting at 1. _iDisplayStart is + // used only internally + var formatter = ctx.formatNumber, start = ctx.displayStart + 1, len = ctx.pageLength, vis = recordsDisplay(ctx), max = recordsTotal(ctx), all = len === -1; + return str + .replace(/_START_/g, formatter(start, ctx)) + .replace(/_END_/g, formatter(displayEnd(ctx), ctx)) + .replace(/_MAX_/g, formatter(max, ctx)) + .replace(/_TOTAL_/g, formatter(vis, ctx)) + .replace(/_PAGE_/g, formatter(all ? 1 : Math.ceil(start / len), ctx)) + .replace(/_PAGES_/g, formatter(all ? 1 : Math.ceil(vis / len), ctx)) + .replace(/_ENTRIES_/g, ctx.api.i18n('entries', '', entries)) + .replace(/_ENTRIES-MAX_/g, ctx.api.i18n('entries', '', max)) + .replace(/_ENTRIES-TOTAL_/g, ctx.api.i18n('entries', '', vis)); +} +/** + * Add elements to an array as quickly as possible, but stack safe. + * + * @param arr Array to add the data to + * @param data Data array that is to be added + */ +function arrayApply(arr, data) { + if (!data) { + return; + } + // Chrome can throw a max stack error if apply is called with + // too large an array, but apply is faster. + if (data.length < 10000) { + arr.push.apply(arr, data); } else { - aSort = sortFlatten(ctx); + for (var i = 0; i < data.length; i++) { + arr.push(data[i]); + } } - for (i = 0, iLen = aSort.length; i < iLen; i++) { - sortCol = aSort[i]; - // Load the data needed for the sort, for each cell - sortData(ctx, sortCol.col); +} +/** + * Add one or more listeners to the table + * + * @param that JQ for the table + * @param name Event name + * @param src Listener(s) + */ +function listener(that, name, src) { + let srcArr = Array.isArray(src) ? src : [src]; + for (var i = 0; i < srcArr.length; i++) { + that.on(name + '.dt.DT', srcArr[i]); } - /* No sorting required if server-side or no sorting array */ - if (dataSource(ctx) != 'ssp' && aSort.length !== 0) { - // Reset the initial positions on each pass so we get a stable sort - for (i = 0, iLen = displayMaster.length; i < iLen; i++) { - aiOrig[i] = i; +} +/** + * Escape HTML entities in strings, in an object + */ +function escapeObject(obj) { + if (ext.escape.attributes) { + each(obj, function (key, val) { + obj[key] = escapeHtml(val); + }); + } + return obj; +} + +const store = { + className: {}, + detect: [], + render: {}, + search: {}, + order: {} +}; +// Common function to remove new lines, strip HTML and diacritic control +function _filterString(stripHtml, normalize) { + return function (str) { + if (util.is.empty(str) || typeof str !== 'string') { + return str; } - // If the first sort is desc, then reverse the array to preserve original - // order, just in reverse - if (aSort.length && aSort[0].dir === 'desc' && ctx.orderDescReverse) { - aiOrig.reverse(); + str = str.replace(util.regex.reNewLines, ' '); + if (stripHtml) { + str = util.stripHtml(str); } - /* Do the sort - here we want multi-column sorting based on a given data source (column) - * and sorting function (from oSort) in a certain direction. It's reasonably complex to - * follow on its own, but this is what we want (example two column sorting): - * fnLocalSorting = function(a,b){ - * var test; - * test = oSort['string-asc']('data11', 'data12'); - * if (test !== 0) - * return test; - * test = oSort['numeric-desc']('data21', 'data22'); - * if (test !== 0) - * return test; - * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); - * } - * Basically we have a test for each sorting column, if the data in that column is equal, - * test the next column. If all columns match, then we use a numeric sort on the row - * positions in the original data array to provide a stable sort. - */ - displayMaster.sort(function (a, b) { - var _a, _b; - var x, y, k, test, sortItem, len = aSort.length, dataA = (_a = data[a]) === null || _a === void 0 ? void 0 : _a.orderCache, dataB = (_b = data[b]) === null || _b === void 0 ? void 0 : _b.orderCache; - for (k = 0; k < len; k++) { - sortItem = aSort[k]; - // Data, which may have already been through a `-pre` function - x = dataA[sortItem.col]; - y = dataB[sortItem.col]; - if (sortItem.sorter) { - // If there is a custom sorter (`-asc` or `-desc`) for this - // data type, use it - test = sortItem.sorter(x, y); - if (test !== 0) { - return test; - } - } - else { - // Otherwise, use generic sorting - test = x < y ? -1 : x > y ? 1 : 0; - if (test !== 0) { - return sortItem.dir === 'asc' ? test : -test; - } - } - } - x = aiOrig[a]; - y = aiOrig[b]; - return x < y ? -1 : x > y ? 1 : 0; - }); + { + str = util.diacritics(str, false); + } + return str; + }; +} +function __numericReplace(d, decimalPlace, re1, re2) { + if (d !== 0 && (!d || d === '-')) { + return -Infinity; } - else if (aSort.length === 0) { - // Apply index order - displayMaster.sort(function (x, y) { - return x < y ? -1 : x > y ? 1 : 0; - }); + if (typeof d === 'number' || typeof d === 'bigint') { + return d; } - if (col === undefined) { - // Tell the draw function that we have sorted the data - ctx.wasOrdered = true; - ctx.sortDetails = aSort; - callbackFire(ctx, null, 'order', [ctx, aSort]); + // If a decimal place other than `.` is used, it needs to be given to the + // function so we can detect it and replace with a `.` which is the only + // decimal place JavaScript recognises - it is not locale aware. + if (decimalPlace) { + d = util.conv.numToDecimal(d, decimalPlace); } - return displayMaster; + if (typeof d === 'string') { + if (re1) { + d = d.replace(re1, ''); + } + if (re2) { + d = d.replace(re2, ''); + } + } + return d * 1; } -/** - * Function to run on user sort request - * - * @param settings dataTables settings object - * @param colIdx column sorting index - * @param addIndex Counter - * @param shift Shift click add - */ -function sortAdd(settings, colIdx, addIndex, shift) { - var col = settings.columns[colIdx]; - var sorting = settings.order; - var asSorting = col.orderSequence; - var nextSortIdx; - var next = function (a, overflow) { - var idx = a._idx; - if (idx === undefined) { - idx = asSorting.indexOf(a[1]); +function register$1(name, prop, val) { + if (!prop) { + return { + className: store.className[name], + detect: store.detect.find(function (fn) { + return fn._name === name; + }), + order: { + pre: store.order[name + '-pre'], + asc: store.order[name + '-asc'], + desc: store.order[name + '-desc'] + }, + render: store.render[name], + search: store.search[name] + }; + } + var setProp = function (prop2, propVal) { + store[prop2][name] = propVal; + }; + var setDetect = function (detect) { + // `detect` can be a function or an object - we set a name + // property for either - that is used for the detection + Object.defineProperty(detect, '_name', { value: name }); + var idx = store.detect.findIndex(function (item) { + return item._name === name; + }); + if (idx === -1) { + store.detect.unshift(detect); + } + else { + store.detect.splice(idx, 1, detect); } - return idx + 1 < asSorting.length ? idx + 1 : overflow ? null : 0; }; - if (!col.orderable) { - return false; + var setOrder = function (obj) { + store.order[name + '-pre'] = obj.pre; // can be undefined + store.order[name + '-asc'] = obj.asc; // can be undefined + store.order[name + '-desc'] = obj.desc; // can be undefined + }; + // prop is optional + if (val === undefined) { + val = prop; + prop = undefined; } - // Convert to 2D array if needed - if (typeof sorting[0] === 'number') { - sorting = settings.order = [sorting]; + if (prop === 'className') { + setProp('className', val); } - // If appending the sort then we are multi-column sorting - if ((shift || addIndex) && settings.features.orderMulti) { - // Are we already doing some kind of sort on this column? - var sortIdx = pluck(sorting, '0').indexOf(colIdx); - if (sortIdx !== -1) { - // Yes, modify the sort - nextSortIdx = next(sorting[sortIdx], true); - if (nextSortIdx === null && sorting.length === 1) { - nextSortIdx = 0; // can't remove sorting completely - } - if (nextSortIdx === null || asSorting[nextSortIdx] === '') { - sorting.splice(sortIdx, 1); - } - else { - sorting[sortIdx][1] = asSorting[nextSortIdx]; - sorting[sortIdx]._idx = nextSortIdx; - } + else if (prop === 'detect') { + setDetect(val); + } + else if (prop === 'order') { + setOrder(val); + } + else if (prop === 'render') { + setProp('render', val); + } + else if (prop === 'search') { + setProp('search', val); + } + else if (!prop) { + if (val.className) { + setProp('className', val.className); } - else if (shift) { - // No sort on this column yet, being added by shift click - // add it as itself - sorting.push([colIdx, asSorting[0], 0]); - sorting[sorting.length - 1]._idx = 0; + if (val.detect !== undefined) { + setDetect(val.detect); } - else { - // No sort on this column yet, being added from a colspan - // so add with same direction as first column - sorting.push([colIdx, sorting[0][1], 0]); - sorting[sorting.length - 1]._idx = 0; + if (val.order) { + setOrder(val.order); } - } - else if (sorting.length && sorting[0][0] == colIdx) { - // Single column - already sorting on this column, modify the sort - nextSortIdx = next(sorting[0]); - if (nextSortIdx) { - sorting.length = 1; - sorting[0][1] = asSorting[nextSortIdx]; - sorting[0]._idx = nextSortIdx; + if (val.render !== undefined) { + setProp('render', val.render); } - else { - sorting.length = 1; - sorting[0][1] = asSorting[0]; - sorting[0]._idx = 0; + if (val.search !== undefined) { + setProp('search', val.search); } } - else { - // Single column - sort only on this column - sorting.length = 0; - sorting.push([colIdx, asSorting[0]]); - sorting[0]._idx = 0; - } } -/** - * Set the sorting classes on table's body, Note: it is safe to call this function - * when bSort and bSortClasses are false - * - * @param settings DataTables settings object - */ -function sortingClasses(settings) { - var oldSort = settings.lastOrder; - var sortClass = settings.classes.order.position; - var sortFlat = sortFlatten(settings); - var features = settings.features; - var i, iLen, colIdx; - if (features.ordering && features.orderClasses) { - // Remove old sorting classes - for (i = 0, iLen = oldSort.length; i < iLen; i++) { - colIdx = oldSort[i].src; - // Remove column sorting - Dom.s(pluck(settings.data, 'cells', colIdx)).classRemove(sortClass + (i < 2 ? i + 1 : 3)); +// Get a list of types +function types() { + return store.detect.map(function (detect) { + return detect._name; + }); +} +var __diacriticSort = function (a, b) { + a = a !== null && a !== undefined ? a.toString().toLowerCase() : ''; + b = b !== null && b !== undefined ? b.toString().toLowerCase() : ''; + // Checked for `navigator.languages` support in `oneOf` so this code can't execute in old + // Safari and thus can disable this check + // eslint-disable-next-line compat/compat + return a.localeCompare(b, navigator.languages[0] || navigator.language, { + numeric: true, + ignorePunctuation: true + }); +}; +var __diacriticHtmlSort = function (a, b) { + a = util.stripHtml(a); + b = util.stripHtml(b); + return __diacriticSort(a, b); +}; +// +// Built in data types +// +register$1('string', { + detect: function () { + return 'string'; + }, + order: { + pre: function (a) { + // This is a little complex, but faster than always calling toString, + // http://jsperf.com/tostring-v-check + return util.is.empty(a) && typeof a !== 'boolean' + ? '' + : typeof a === 'string' + ? a.toLowerCase() + : !a.toString + ? '' + : a.toString(); } - // Add new column sorting - for (i = 0, iLen = sortFlat.length; i < iLen; i++) { - colIdx = sortFlat[i].src; - Dom.s(pluck(settings.data, 'cells', colIdx)).classAdd(sortClass + (i < 2 ? i + 1 : 3)); + }, + search: _filterString(false) +}); +register$1('string-utf8', { + detect: { + allOf: function () { + return true; + }, + oneOf: function (d) { + // At least one data point must contain a non-ASCII character + // This line will also check if navigator.languages is supported or not. If not (Safari 10.0-) + // this data type won't be supported. + // eslint-disable-next-line compat/compat + return (!util.is.empty(d) && + navigator.languages && + typeof d === 'string' && + !!d.match(/[^\x00-\x7F]/)); } - } - settings.lastOrder = sortFlat; -} -/** - * Get the data to sort a column, be it from cache, fresh (populating the - * cache), or from a sort formatter - * - * @param settings DataTables settings object - * @param colIdx Column index - */ -function sortData(settings, colIdx) { - // Custom sorting function - provided by the sort data type - var column = settings.columns[colIdx]; - var customSort = ext.order[column.orderDataType]; - var customData; - if (customSort) { - customData = customSort.call(settings.instance, settings, colIdx, columnIndexToVisible(settings, colIdx)); - } - // Use / populate cache - var row, cellData; - var formatter = ext.type.order[column.type + '-pre']; - var data = settings.data; - for (var rowIdx = 0; rowIdx < data.length; rowIdx++) { - // Sparse array - if (!data[rowIdx]) { - continue; + }, + order: { + asc: __diacriticSort, + desc: function (a, b) { + return __diacriticSort(a, b) * -1; } - row = data[rowIdx]; - if (row && !row.orderCache) { - row.orderCache = []; + }, + search: _filterString(false) +}); +register$1('html', { + detect: { + allOf: function (d) { + return (util.is.empty(d) || + (typeof d === 'string' && d.indexOf('<') !== -1)); + }, + oneOf: function (d) { + // At least one data point must contain a `<` + return (!util.is.empty(d) && + typeof d === 'string' && + d.indexOf('<') !== -1); + } + }, + order: { + pre: function (a) { + return util.is.empty(a) + ? '' + : a.replace + ? util.stripHtml(a).trim().toLowerCase() + : a + ''; + } + }, + search: _filterString(true) +}); +register$1('html-utf8', { + detect: { + allOf: function (d) { + return (util.is.empty(d) || + (typeof d === 'string' && d.indexOf('<') !== -1)); + }, + oneOf: function (d) { + // At least one data point must contain a `<` and a non-ASCII character + // eslint-disable-next-line compat/compat + return (navigator.languages && + !util.is.empty(d) && + typeof d === 'string' && + d.indexOf('<') !== -1 && + typeof d === 'string' && + !!d.match(/[^\x00-\x7F]/)); + } + }, + order: { + asc: __diacriticHtmlSort, + desc: function (a, b) { + return __diacriticHtmlSort(a, b) * -1; + } + }, + search: _filterString(true) +}); +register$1('date', { + className: 'dt-type-date', + detect: { + allOf: function (d) { + // V8 tries _very_ hard to make a string passed into `Date.parse()` + // valid, so we need to use a regex to restrict date formats. Use a + // plug-in for anything other than ISO8601 style strings + if (d && !(d instanceof Date) && !util.regex.reDate.test(d)) { + return null; + } + var parsed = Date.parse(d); + return (parsed !== null && !isNaN(parsed)) || util.is.empty(d); + }, + oneOf: function (d) { + // At least one entry must be a date or a string with a date + return (d instanceof Date || + (typeof d === 'string' && util.regex.reDate.test(d))); } - if (row && (!row.orderCache[colIdx] || customSort)) { - cellData = customSort - ? customData[rowIdx] // If there was a custom sort function, use data from there - : getCellData(settings, rowIdx, colIdx, 'sort'); - row.orderCache[colIdx] = formatter - ? formatter(cellData, settings) - : cellData; + }, + order: { + pre: function (d) { + var ts = Date.parse(d); + return isNaN(ts) ? -Infinity : ts; } } -} - -/** - * Alter the display settings to change the page - * - * @param settings DataTables settings object - * @param action Paging action to take: "first", "previous", "next" or "last" or - * page number to jump to (integer) - * @param redraw Automatically draw the update or not - * @returns true page has changed, false - no change - */ -function pageChange(settings, action, redraw) { - var start = settings.displayStart, len = settings.pageLength, records = recordsDisplay(settings); - if (records === 0 || len === -1) { - start = 0; - } - else if (typeof action === 'number') { - start = action * len; - if (start > records) { - start = 0; +}); +register$1('html-num-fmt', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, true, false); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, true, false); } - } - else if (action == 'first') { - start = 0; - } - else if (action == 'previous') { - start = len >= 0 ? start - len : 0; - if (start < 0) { - start = 0; + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reHtml, util.regex.reFormattedNumeric); } - } - else if (action == 'next') { - if (start + len < records) { - start += len; + }, + search: _filterString(true) +}); +register$1('html-num', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, false, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.htmlNum(d, decimal, false, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reHtml); + } + }, + search: _filterString(true) +}); +register$1('num-fmt', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.num(d, decimal, true, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.num(d, decimal, true, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp, util.regex.reFormattedNumeric); } } - else if (action == 'last') { - start = Math.floor((records - 1) / len) * len; - } - else if (action === 'ellipsis') { - return; - } - else { - log(settings, 0, 'Unknown paging action: ' + action, 5); - } - var changed = settings.displayStart !== start; - settings.displayStart = start; - callbackFire(settings, null, changed ? 'page' : 'page-nc', [settings]); - if (changed && redraw) { - draw(settings); +}); +register$1('num', { + className: 'dt-type-numeric', + detect: { + allOf: function (d, settings) { + var decimal = settings.language.decimal; + return util.is.num(d, decimal, false, true); + }, + oneOf: function (d, settings) { + // At least one data point must contain a numeric value + var decimal = settings.language.decimal; + return util.is.num(d, decimal, false, false); + } + }, + order: { + pre: function (d, s) { + var dp = s.language.decimal; + return __numericReplace(d, dp); + } } - return changed; -} +}); +/* + * Public helper functions. These aren't used internally by DataTables, or + * called by any of the options passed into DataTables, but they can be used + * externally by developers working with DataTables. They are helper functions + * to make working with DataTables a little bit easier. + */ /** - * State information for a table + * Common logic for moment, luxon or a date action. * - * @param settings DataTables settings object + * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again */ -function saveState(settings) { - if (settings.loadingState) { - return; +function __mld(dtLib, momentFn, luxonFn, dateFn, arg1) { + if (__moment) { + return dtLib[momentFn](arg1); } - // Sort state saving uses [[idx, order]] structure. - var sorting = []; - sortResolve(settings, sorting, settings.order); - /* Store the interesting variables */ - var columns = settings.columns; - var state = { - columns: settings.columns.map(function (col, i) { - return { - name: col.name, - visible: col.visible, - search: Object.assign({}, settings.searches[i]) - }; - }), - length: settings.pageLength, - order: sorting.map(function (sort) { - // If a column name is available, use it - return columns[sort[0]] && columns[sort[0]].name - ? [columns[sort[0]].name, sort[1]] - : sort.slice(); - }), - search: Object.assign({}, settings.searches['*']), - searchGroups: Object.keys(settings.searches) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => Object.assign({}, settings.searches[c])), - start: settings.displayStart, - time: +new Date() - }; - settings.stateSaved = state; - callbackFire(settings, 'stateSaveParams', 'stateSaveParams', [ - settings, - state - ]); - if (settings.features.stateSave && !settings.destroying) { - settings.stateSaveCallback.call(settings.instance, settings, state); + else if (__luxon) { + return dtLib[luxonFn](arg1); } + return dateFn ? dtLib[dateFn](arg1) : dtLib; } +var __mlWarning = false; +var __luxon; +var __moment; /** - * Attempt to load a saved table state * - * @param settings dataTables settings object - * @param callback Callback to execute when the state has been loaded */ -function loadState(settings, callback) { - if (!settings.features.stateSave) { - callback(); - return; - } - var loaded = function (state, ignoreTime = false) { - implementState(settings, state, ignoreTime, callback); - }; - var state = settings.stateLoadCallback.call(settings.instance, settings, loaded); - if (state !== undefined) { - implementState(settings, state, false, callback); - } - // otherwise, wait for the loaded callback to be executed - return true; +function resolveWindowLibs() { + __luxon = util.external('luxon'); + __moment = util.external('moment'); } -function implementState(settings, s, ignoreTime, callback) { - var i, iLen; - var columns = settings.columns; - var currentNames = pluck(settings.columns, 'name'); - settings.loadingState = true; - // When StateRestore was introduced the state could now be implemented at - // any time Not just initialisation. To do this an api instance is required - // in some places - var api = settings.initDone ? new Api(settings) : null; - if (!ignoreTime) { - if (!s || !s.time) { - settings.loadingState = false; - callback(); - return; +function __mldObj(d, format, locale) { + var dt; + resolveWindowLibs(); + if (__moment) { + dt = __moment(d, format, locale, true); + if (!dt.isValid()) { + return null; } - // Reject old data - var duration = settings.stateDuration; - if (duration > 0 && s.time < +new Date() - duration * 1000) { - settings.loadingState = false; - callback(); - return; + } + else if (__luxon) { + dt = + format && typeof d === 'string' + ? __luxon.DateTime.fromFormat(d, format) + : __luxon.DateTime.fromISO(d); + if (!dt.isValid) { + return null; } + dt = dt.setLocale(locale); + } + else if (!format) { + // No format given, must be ISO + dt = new Date(d); } - // Allow custom and plug-in manipulation functions to alter the saved data - // set and cancelling of loading by returning false - var abStateLoad = callbackFire(settings, 'stateLoadParams', 'stateLoadParams', [settings, s]); - if (abStateLoad.indexOf(false) !== -1) { - settings.loadingState = false; - callback(); - return; + else { + if (!__mlWarning) { + alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); + } + __mlWarning = true; } - // Store the saved state so it might be accessed at any time - settings.stateLoaded = assignDeep({}, s); - // This is needed for ColReorder, which has to happen first to allow all - // the stored indexes to be usable. It is not publicly documented. - callbackFire(settings, null, 'stateLoadInit', [settings, s], true); - // Page Length - if (s.length !== undefined) { - // If already initialised just set the value directly so that the select - // element is also updated - if (api) { - api.page.len(s.length); + return dt; +} +// Wrapper for date, datetime and time which all operate the same way with the +// exception of the output string for auto locale support +function __mlHelper(localeString) { + return function (from, to, locale, def) { + // Luxon and Moment support + // Argument shifting + if (arguments.length === 0) { + locale = 'en'; + to = null; // means toLocaleString + from = null; // means iso8601 } - else { - settings.pageLength = s.length; + else if (arguments.length === 1) { + locale = 'en'; + to = from; + from = null; } - } - // Restore key features - if (s.start !== undefined) { - if (api === null) { - settings.displayStart = s.start; - settings.displayStartInit = s.start; + else if (arguments.length === 2) { + locale = to; + to = from; + from = null; } - else { - pageChange(settings, s.start / settings.pageLength); + var typeName = 'datetime' + (to ? '-' + to : ''); + // Add type detection and sorting specific to this date format - we need + // to be able to identify date type columns as such, rather than as + // numbers in extensions. Hence the need for this. + if (!store.order[typeName + '-pre']) { + register$1(typeName, { + detect: function (d) { + // The renderer will give the value to type detect as the + // type! + return d === typeName ? typeName : false; + }, + order: { + pre: function (d) { + // The renderer gives us Moment, Luxon or Date objects + // for the sorting, all of which have a `valueOf` which + // gives milliseconds epoch + return d.valueOf(); + } + } + }); } - } - // Order - if (s.order !== undefined) { - settings.order = []; - for (let i = 0; i < s.order.length; i++) { - let col = s.order[i]; - let set = [col[0], col[1]]; - // A column name was stored and should be used for restore - if (typeof col[0] === 'string') { - // Find the name from the current list of column names - let idx = currentNames.indexOf(col[0]); - if (idx < 0) { - // If the column was not found ignore it and continue - continue; + if (!store.className[typeName]) { + store.className[typeName] = 'dt-right'; + } + return function (d, type) { + // Allow for a default value + if (d === null || d === undefined) { + if (def === '--now') { + // We treat everything as UTC further down, so no changes + // are made, as such need to get the local date / time as if + // it were UTC + var local = new Date(); + d = new Date(Date.UTC(local.getFullYear(), local.getMonth(), local.getDate(), local.getHours(), local.getMinutes(), local.getSeconds())); + } + else { + d = ''; } - set[0] = idx; } - else if (set[0] >= columns.length) { - // If the column index is out of bounds ignore it and continue - continue; + if (type === 'type') { + // Typing uses the type name for fast matching + return typeName; + } + if (d === '') { + return type !== 'sort' + ? '' + : __mldObj('0000-01-01 00:00:00', null, locale); + } + // Shortcut. If `from` and `to` are the same, we are using the + // renderer to format for ordering, not display - its already in the + // display format. + if (to !== null && + from === to && + type !== 'sort' && + type !== 'type' && + !(d instanceof Date)) { + return d; + } + // Determine if there is a timezone. If there is, we want to reuse + // it for the output, so the timezone doesn't change between the + // input and output. + let options = {}; + let tzMatch = typeof d === 'string' ? d.match(util.regex.isoTimezone) : null; + if (tzMatch) { + options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1]; + } + // Get a Date object (Luxon, moment or Date) + var dt = __mldObj(d, from, locale); + if (dt === null) { + return d; + } + if (type === 'sort') { + return dt; + } + var formatted = to === null + ? __mld(dt, 'toDate', 'toJSDate', '')[localeString](navigator.language, options) + : __mld(dt, 'format', 'toFormat', 'toISOString', to); + // XSS protection + return type === 'display' ? util.escapeHtml(formatted) : formatted; + }; + }; +} +// Based on locale, determine standard number formatting +// Fallback for legacy browsers is US English +var __thousands = ','; +var __decimal = '.'; +if (window.Intl !== undefined) { + try { + var num = new Intl.NumberFormat().formatToParts(100000.1); + for (var i = 0; i < num.length; i++) { + if (num[i].type === 'group') { + __thousands = num[i].value; + } + else if (num[i].type === 'decimal') { + __decimal = num[i].value; } - settings.order.push(set); } } - // Search - if (s.search !== undefined) { - Object.assign(settings.searches['*'], s.search); + catch (e) { + // noop } - if (s.searchGroups) { - s.searchGroups.forEach(group => { - if (group.columns) { - let index = group.columns.join(','); - settings.searches[index] = create$2(group); - } - }); +} +/** + * Register a date / time format for DataTables to use. + * + * @param format The date / time format to detect data in. Please refer to the + * Moment.js or Luxon document for the full list of tokens, depending on which + * of the two libraries you are using. + * @param locale The locale to pass to Moment.js / Luxon. + */ +function datetime(format, locale) { + var typeName = 'datetime-' + format; + if (!locale) { + locale = 'en'; } - // Columns - if (s.columns) { - var set = s.columns; - var incoming = pluck(s.columns, 'name'); - // Check if it is a 2.2 style state object with a `name` property for - // the columns, and if the name was defined. If so, then create a new - // array that will map the state object given, to the current columns - // (don't bother if they are already matching tho). - if (incoming.join('').length && - incoming.join('') !== currentNames.join('')) { - set = []; - // For each column, try to find the name in the incoming array - for (i = 0; i < currentNames.length; i++) { - if (currentNames[i] != '') { - var idx = incoming.indexOf(currentNames[i]); - if (idx >= 0) { - set.push(s.columns[idx]); - } - else { - // No matching column name in the state's columns, so - // this might be a new column and thus can't have a - // state already. - set.push({}); - } - } - else { - // If no name, but other columns did have a name, then there - // is no knowing where this one came from originally so it - // can't be restored. - set.push({}); - } - } - } - // If the number of columns to restore is different from current, then - // all bets are off. - if (set.length === columns.length) { - for (i = 0, iLen = set.length; i < iLen; i++) { - var col = set[i]; - // Visibility - if (col.visible !== undefined) { - // If the api is defined, the table has been initialised so - // we need to use it rather than internal settings - if (api) { - // Don't redraw the columns on every iteration of this - // loop, we will do this at the end instead - api.column(i).visible(col.visible, false); - } - else { - columns[i].visible = col.visible; - } - } - // Search - if (col.search !== undefined) { - Object.assign(settings.searches[i], col.search); - // If out of order due to a change in order from named - // columns we need to make sure the index is correct - settings.searches[i].columns = [i]; - } - } - // If the api is defined then we need to adjust the columns once the - // visibility has been changed - if (api) { - api.one('draw', function () { - api.columns.adjust(); - }); + if (!store.order[typeName]) { + register$1(typeName, { + detect: function (d) { + var dt = __mldObj(d, format, locale); + return d === '' || dt ? typeName : false; + }, + order: { + pre: function (d) { + return __mldObj(d, format, locale) || 0; + } } - } + }); + } + if (!store.className[typeName]) { + store.className[typeName] = 'dt-right'; } - settings.loadingState = false; - callbackFire(settings, 'stateLoaded', 'stateLoaded', [settings, s]); - callback(); } - /** - * Draw the table for the first time, adding all required features - * - * @param settings DataTables settings object + * Helpers for `columns.render`. */ -function initialise(settings) { - var i; - var init = settings.init; - var deferLoading = settings.deferLoading; - var dataSrc = dataSource(settings); - // Ensure that the table data is fully initialised - if (!settings.initialised) { - setTimeout(function () { - initialise(settings); - }, 200); - return; - } - // Build the header / footer for the table - buildHead(settings, 'header'); - buildHead(settings, 'footer'); - // Load the table's state (if needed) and then render around it and draw - loadState(settings, function () { - // Then draw the header / footer - drawHead(settings, settings.header); - drawHead(settings, settings.footer); - // Cache the paging start point, as the first redraw will reset it - var iAjaxStart = settings.displayStartInit; - // Local data load - // Check if there is data passing into the constructor - if (init && init.data) { - for (i = 0; i < init.data.length; i++) { - addData(settings, init.data[i]); - } +var helpers = { + date: __mlHelper('toLocaleDateString'), + datetime: __mlHelper('toLocaleString'), + time: __mlHelper('toLocaleTimeString'), + number: function (thousands, decimal, precision, prefix, postfix) { + // Auto locale detection + if (thousands === null || thousands === undefined) { + thousands = __thousands; } - else if (deferLoading || dataSrc == 'dom') { - // Grab the data from the page - addTr(settings, Dom.s(settings.tbody).children('tr')); + if (decimal === null || decimal === undefined) { + decimal = __decimal; } - // Filter not yet applied - copy the display master - settings.display = settings.displayMaster.slice(); - // Enable features - createLayout(settings); - sortInit(settings); - colGroup(settings); - /* Okay to show that something is going on now */ - processingDisplay(settings, true); - callbackFire(settings, null, 'preInit', [settings], true); - // If there is default sorting required - let's do it. The sort function - // will do the drawing for us. Otherwise we draw the table regardless of - // the Ajax source - this allows the table to look initialised for Ajax - // sourcing data (show 'loading' message possibly) - reDraw(settings); - // Server-side processing init complete is done by _fnAjaxUpdateDraw - if (dataSrc != 'ssp' || deferLoading) { - // if there is an ajax source load the data - if (dataSrc == 'ajax') { - buildAjax(settings, {}, function (json) { - var aData = ajaxDataSrc(settings, json, false); - // Got the data - add it to the table - for (i = 0; i < aData.length; i++) { - addData(settings, aData[i]); - } - // Reset the init display for cookie saving. We've already - // done a filter, and therefore cleared it before. So we - // need to make it appear 'fresh' - settings.displayStartInit = iAjaxStart; - reDraw(settings); - processingDisplay(settings, false); - initComplete(settings); - }); - } - else { - initComplete(settings); - processingDisplay(settings, false); + return { + display: function (d) { + if (typeof d !== 'number' && typeof d !== 'string') { + return d; + } + if (d === '' || d === null) { + return d; + } + var flo = typeof d === 'number' ? d : parseFloat(d); + var negative = flo < 0 ? '-' : ''; + var abs = Math.abs(flo); + // Scientific notation for large and small numbers + if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0)) { + var exp = flo.toExponential(precision).split(/e\+?/); + return exp[0] + ' x 10' + exp[1] + ''; + } + // If NaN then there isn't much formatting that we can do - just + // return immediately, escaping any HTML (this was supposed to + // be a number after all) + if (isNaN(flo)) { + return util.escapeHtml(d); + } + flo = flo.toFixed(precision); + var absPart = Math.abs(flo); + var intPart = Math.abs(parseInt(flo, 10)); + var floatPart = precision + ? decimal + + (absPart - intPart).toFixed(precision).substring(2) + : ''; + // If zero, then can't have a negative prefix + if (intPart === 0 && parseFloat(floatPart) === 0) { + negative = ''; + } + return (negative + + (prefix || '') + + intPart + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + + floatPart + + (postfix || '')); } - } - }); -} + }; + }, + text: function () { + return { + display: util.escapeHtml, + filter: util.escapeHtml + }; + } +}; + /** - * Draw the table for the first time, adding all required features + * Column options that can be given to DataTables at initialisation time. + */ +const defaults$2 = { + ariaTitle: '', + cellType: 'td', + className: '', + contentPadding: '', + createdCell: null, + data: null, + defaultContent: null, + footer: null, + name: '', + orderable: true, + orderData: null, + orderDataType: 'std', + orderSequence: ['asc', 'desc', ''], + render: null, + search: null, + searchable: true, + title: null, + type: null, + visible: true, + width: null +}; + +/** + * Internal settings object used for individual columns. Instances are held in + * the setting object's `columns` array and contains all the information that + * DataTables needs about each individual column. * - * @param settings DataTables settings object + * Note that this object is related to the column defaults but this one is the + * internal data store for DataTables's cache of columns. It should NOT be + * manipulated outside of DataTables. Any configuration should be done through + * the initialisation options. */ -function initComplete(settings) { - if (settings.initDone) { - return; - } - var args = [settings, settings.json]; - settings.initDone = true; - // If the footer element is empty after initialisation, then remove it - let tfoot = Dom.s(settings.tfoot); - if (tfoot.children().count() === 0) { - tfoot.remove(); +class Settings { + constructor() { + /** + * Flag to indicate if HTML5 data attributes should be used as the data + * source for filtering or sorting. True is either are. + */ + this.attrSrc = false; + this.ariaTitle = ''; + /** + * The class to apply to all cells in the table's `tbody`` for the column + */ + this.className = null; + /** + * When DataTables calculates the column widths to assign to each column, it + * finds the longest string in each column and then constructs a temporary + * table and reads the widths from that. The problem with this is that "mmm" + * is much wider then "iiii", but the latter is a longer string - thus the + * calculation can go wrong (doing it properly and putting it into an DOM + * object and measuring that is horribly(!) slow). Thus as a "work around" + * we provide this option. It will append its value to the text that is + * found to be the longest string for the column - i.e. padding. + */ + this.contentPadding = null; + /** + * Property to read the value for the cells in the column from the data + * source array / object. If null, then the default content is used, if a + * function is given then the return from the function is used. + */ + this.data = null; + /** + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because mData is + * set to null, or because the data source itself is null). + */ + this.defaultContent = null; + /** + * Name for the column, allowing reference to the column by name as well as + * by index (needs a lookup to work by name). + */ + this.name = null; + /** + * A list of the columns that sorting should occur on when this column is + * sorted. That this property is an array allows multi-column sorting to be + * defined for a column (for example first name / last name columns would + * benefit from this). The values are integers pointing to the columns to be + * sorted on (typically it will be a single integer pointing at itself, but + * that doesn't need to be the case). + */ + this.orderData = []; + /** + * Custom sorting data type - defines which of the available plug-ins in + * afnSortData the custom sorting will use - if any is defined. + */ + this.orderDataType = 'std'; + /** + * Class to be applied to the header element when sorting on this column + */ + this.orderingClass = null; + /** + * Define the sorting directions that are applied to the column, in sequence + * as the column is repeatedly sorted upon - i.e. the first value is used as + * the sorting direction when the column if first sorted (clicked on). Sort + * it again (click again) and it will move on to the next index. Repeat + * until loop. + */ + this.orderSequence = []; + /** + * Partner property to mData which is used (only when defined) to get the + * data - i.e. it is basically the same as mData, but without the 'set' + * option, and also the data fed to it is the result from mData. This is the + * rendering method to match the data method of mData. + */ + this.render = null; + /** + * Title of the column - what is seen in the TH element (nTh). + */ + this.title = null; + /** + * Store for manual type assignment using the `column.type` option. This + * is held in store so we can manipulate the column's `type` property. + */ + this.typeManual = null; + /** Cached longest strings from a column */ + this.wideStrings = null; + /** + * Width of the column + */ + this.width = null; + /** + * Width of the column when it was first "encountered" + */ + this.widthOrig = null; } - // Table is fully set up and we have data, so calculate the - // column widths - adjustColumnSizing(settings); - callbackFire(settings, null, 'plugin-init', args, true); - callbackFire(settings, 'init', 'init', args, true); } +const browser = { + barWidth: -1, + scrollbarLeft: false +}; +const hungarianToCamelRe = /^(a|aa|ai|ao|as|b|fn|i|m|o|s)([A-Z])([a-z].*$)/; /** - * Create an Ajax call based on the table's settings, taking into account that - * parameters can have multiple forms, and backwards compatibility. - * - * @param settings DataTables settings object - * @param data Data to send to the server, required by DataTables - may be - * augmented by developer callbacks - * @param fn Callback function to run when data is obtained + * Take an object which has hungarian notation parameters and convert them to + * camelCase style. This is to allow compatibility with DataTables 1.9 and + * earlier which only used hungarian notation, and also with DataTables 1.10 - 2 + * which allowed it to be used. */ -function buildAjax(settings, data, fn) { - var ajaxData; - var ajaxConfig = settings.ajax; - var instance = settings.instance; - var callback = function (json) { - var status = settings.jqXHR ? settings.jqXHR.status : null; - if (json === null || (typeof status === 'number' && status == 204)) { - json = {}; - ajaxDataSrc(settings, json, []); - } - var error = json.error || json.sError; - if (error) { - log(settings, 0, error); - } - // Microsoft often wrap JSON as a string in another JSON object Let's - // handle that automatically - if (json.d && typeof json.d === 'string') { - try { - json = JSON.parse(json.d); - } - catch (e) { - // noop - } - } - settings.json = json; - callbackFire(settings, null, 'xhr', [settings, json, settings.jqXHR], true); - fn(json); - }; - if (util.is.plainObject(ajaxConfig) && ajaxConfig.data) { - ajaxData = ajaxConfig.data; - var newData = typeof ajaxData === 'function' - ? ajaxData(data, settings) // fn can manipulate data or return - : ajaxData; // an object or array to merge - // If the function returned something, use that alone - data = - typeof ajaxData === 'function' && newData - ? newData - : util.object.assignDeep(data, newData); - // Remove the data property as we've resolved it already and don't want - // jQuery to do it again (it is restored at the end of the function) - delete ajaxConfig.data; +function hungarianToCamel(user) { + if (!user) { + return user; } - var baseAjax = { - url: typeof ajaxConfig === 'string' ? ajaxConfig : '', - data: data, - success: callback, - dataType: 'json', - cache: false, - type: settings.serverMethod, - error: function (xhr, error) { - var ret = callbackFire(settings, null, 'xhr', [settings, null, settings.jqXHR], true); - if (ret.indexOf(false) === -1) { - if (error == 'parsererror') { - log(settings, 0, 'Invalid JSON response', 1); - } - else if (xhr.readyState === 4) { - log(settings, 0, 'Ajax error', 7); - } - } - processingDisplay(settings, false); + let userKeys = Object.keys(user); + let userAny = user; + for (let i = 0; i < userKeys.length; i++) { + let userKey = userKeys[i]; + let match = userKey.match(hungarianToCamelRe); + // Is the key in hungarian notation? + if (match) { + // If so map it down + user[match[2].toLowerCase() + match[3]] = userAny[userKey]; + } + // Recurse down through the object + if (util.is.plainObject(userAny[userKey])) { + hungarianToCamel(userAny[userKey]); } - }; - // If `ajax` option is an object, extend and override our default base - if (util.is.plainObject(ajaxConfig)) { - util.object.assign(baseAjax, ajaxConfig); - } - // Store the data submitted for the API - settings.ajaxData = data; - // Allow plug-ins and external processes to modify the data - callbackFire(settings, null, 'preXhr', [settings, data, baseAjax], true); - if (typeof ajaxConfig === 'function') { - // Is a function - let the caller define what needs to be done - settings.jqXHR = ajaxConfig.call(instance, data, callback, settings); - } - else if (ajaxConfig && - typeof ajaxConfig !== 'string' && - ajaxConfig.url === '') { - // No url, so don't load any data. Just apply an empty data array - // to the object for the callback. - var empty = {}; - ajaxDataSrc(settings, empty, []); - callback(empty); - } - else { - // Object to extend the base settings - settings.jqXHR = util.ajax(baseAjax); - } - // Restore for next time around - if (ajaxData) { - ajaxConfig.data = ajaxData; } + return user; } /** - * Update the table using an Ajax call + * Map one parameter onto another * - * @param settings DataTables settings object - * @returns Block the table drawing or not + * @param o Object to map + * @param newKey The new parameter name + * @param oldKey The old parameter name */ -function ajaxUpdate(settings) { - settings.drawCount++; - processingDisplay(settings, true); - buildAjax(settings, ajaxParameters(settings), function (json) { - ajaxUpdateDraw(settings, json); - }); -} -function functionOrValue(val) { - return typeof val === 'function' ? 'function' : val.toString(); +function compatMap(o, newKey, oldKey) { + if (o[oldKey] !== undefined) { + o[newKey] = o[oldKey]; + } } /** - * Build up the parameters in an object needed for a server-side processing - * request. + * Provide backwards compatibility for the main DT options. Note that the new + * options are mapped onto the old parameters, so this is an external interface + * change only. * - * @param settings DataTables settings object - * @returns Block the table drawing or not + * @param init Object to map + * @param defaults Indicate if the object is the defaults object or not */ -function ajaxParameters(settings) { - var columns = settings.columns, features = settings.features, searches = settings.searches, searchesFixed = settings.searchesFixed, colData = function (idx, prop) { - return typeof columns[idx][prop] === 'function' - ? 'function' - : columns[idx][prop]; - }; - return { - draw: settings.drawCount, - columns: columns.map(function (column, i) { - return { - data: colData(i, 'data'), - name: column.name, - searchable: column.searchable, - orderable: column.orderable, - search: { - value: searches[i] - ? functionOrValue(searches[i].search) - : '', - regex: searches[i] ? searches[i].regex : false, - fixed: searchesFixed[i] - ? Object.keys(searchesFixed[i]).map(name => ({ - name: name, - term: functionOrValue(searchesFixed[i][name].search) - })) - : [] - } - }; - }), - order: sortFlatten(settings).map(function (val) { - return { - column: val.col, - dir: val.dir, - name: colData(val.col, 'name') - }; - }), - start: settings.displayStart, - length: features.paging ? settings.pageLength : -1, - search: { - value: functionOrValue(searches['*'].search), - regex: searches['*'].regex, - fixed: Object.keys(settings.searchesFixed['*']).map(name => ({ - name: name, - term: functionOrValue(settings.searchesFixed['*'][name].search) - })), - groups: Object.keys(settings.searches) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => ({ - columns: settings.searches[c].columns || [], - term: functionOrValue(settings.searches[c].search) - })), - groupsFixed: Object.keys(settings.searchesFixed) - .filter(c => c.includes(',')) // Limit to only multi-column subsets - .map(c => { - let searches = settings.searchesFixed[c]; - return Object.keys(searches).map(n => ({ - columns: searches[n].columns || [], - name: n, - term: functionOrValue(searches[n].search) - })); - }) - .flat() +function compatOpts(init, defaults = false) { + // Convert any old style parameters to camelCase + hungarianToCamel(init); + // Map old parameter names to new + compatMap(init, 'ordering', 'sort'); + compatMap(init, 'orderMulti', 'sortMulti'); + compatMap(init, 'orderClasses', 'sortClasses'); + compatMap(init, 'orderCellsTop', 'sortCellsTop'); + compatMap(init, 'order', 'sorting'); + compatMap(init, 'orderFixed', 'sortingFixed'); + compatMap(init, 'paging', 'paginate'); + compatMap(init, 'pagingType', 'paginationType'); + compatMap(init, 'pageLength', 'displayLength'); + compatMap(init, 'searching', 'filter'); + compatMap(init, 'stateDuration', 'cookieDuration'); + // Boolean initialisation of x-scrolling + if (typeof init.scrollX === 'boolean') { + init.scrollX = init.scrollX ? '100%' : ''; + } + // Objects for ordering + if (typeof init.ordering === 'object') { + init.orderIndicators = + init.ordering.indicators !== undefined + ? init.ordering.indicators + : true; + init.orderHandler = + init.ordering.handler !== undefined ? init.ordering.handler : true; + if (!defaults) { + init.ordering = true; } - }; + } + else if (init.ordering === false) { + init.orderIndicators = false; + init.orderHandler = false; + } + else if (init.ordering === true) { + init.orderIndicators = true; + init.orderHandler = true; + } + // Which cells are the title cells? + if (typeof init.orderCellsTop === 'boolean') { + init.titleRow = init.orderCellsTop; + } + // Column search objects are in an array, so it needs to be converted + // element by element + var searchCols = init.searchCols; + if (searchCols) { + for (var i = 0, iLen = searchCols.length; i < iLen; i++) { + if (searchCols[i]) { + hungarianToCamel(searchCols[i]); + } + } + } + // Enable search delay if server-side processing is enabled + if (init.serverSide && !init.searchDelay) { + init.searchDelay = 400; + } + // Language + if (init.language && init.language.url && !init.language.ajax) { + init.language.ajax = init.language.url; + } } /** - * Data the data from the server (nuking the old) and redraw the table + * Provide backwards compatibility for column options. Note that the new options + * are mapped onto the old parameters, so this is an external interface change + * only. * - * @param settings DataTables settings object - * @param json json data return from the server. + * @param init Object to map */ -function ajaxUpdateDraw(settings, json) { - var data = ajaxDataSrc(settings, json, false); - var drawUnique = ajaxDataSrcParam(settings, 'draw', json); - var recordsTotal = ajaxDataSrcParam(settings, 'recordsTotal', json); - var recordsFiltered = ajaxDataSrcParam(settings, 'recordsFiltered', json); - var existingTypes = settings.columns.map(c => c.type).join(','); - if (drawUnique !== undefined) { - // Protect against out of sequence returns - if (drawUnique * 1 < settings.drawCount) { - return; - } - settings.drawCount = drawUnique * 1; +function compatCols(init) { + // Convert any old style parameters to camelCase + hungarianToCamel(init); + // typeof columnDefaults + compatMap(init, 'orderable', 'sortable'); + compatMap(init, 'orderData', 'dataSort'); + compatMap(init, 'orderSequence', 'sorting'); + compatMap(init, 'orderDataType', 'sortDataType'); + compatMap(init, 'className', 'class'); + // orderData can be given as an integer + var dataSort = init.aDataSort; + var orderData = init.orderData; + if (typeof dataSort === 'number') { + init.orderData = [dataSort]; } - // No data in returned object, so rather than an array, we show an empty - // table - if (!data) { - data = []; + if (typeof orderData === 'number') { + init.orderData = [orderData]; } - clearTable(settings); - settings.recordsTotal = parseInt(recordsTotal, 10); - settings.recordsDisplay = parseInt(recordsFiltered, 10); - for (var i = 0, iLen = data.length; i < iLen; i++) { - addData(settings, data[i]); + // Backwards compatibility for mDataProp from 1.9- + if (init.dataProp !== undefined && !init.data) { + init.data = init.dataProp; } - settings.display = settings.displayMaster.slice(); - columnTypes(settings, existingTypes); - draw(settings, true); - initComplete(settings); - processingDisplay(settings, false); } /** - * Get the data from the JSON data source to use for drawing a table. + * Browser feature detection for capabilities, quirks * - * @param settings DataTables settings object - * @param json Data source object / array from the server - * @param write Array or object to write the data to - * @return Array of data to use + * @param ctx DataTables settings object */ -function ajaxDataSrc(settings, json, write) { - var dataProp = 'data'; - if (util.is.plainObject(settings.ajax) && - settings.ajax.dataSrc !== undefined) { - // Could in inside a `dataSrc` object, or not! - var dataSrc = settings.ajax.dataSrc; - // string, function and object are valid types - if (typeof dataSrc === 'string' || typeof dataSrc === 'function') { - dataProp = dataSrc; - } - else if (dataSrc.data !== undefined) { - dataProp = dataSrc.data; - } - } - if (!write) { - if (dataProp === 'data') { - // If the default, then we still want to support the old style, and - // safely ignore it if possible - return json.aaData || json[dataProp]; - } - return dataProp !== '' ? util.get(dataProp)(json) : json; +function browserDetect(ctx) { + // We don't need to do this every time DataTables is constructed, the values + // calculated are specific to the browser and OS configuration which we + // don't expect to change between initialisations + if (browser.barWidth === -1) { + // Scrolling feature / quirks detection + var n = Dom + .c('div') + .css({ + position: 'fixed', + top: '0', + left: -1 * window.pageXOffset + 'px', // allow for scrolling + height: '1px', + width: '1px', + overflow: 'hidden' + }) + .append(Dom + .c('div') + .css({ + position: 'absolute', + top: '1px', + left: '1px', + width: '100px', + overflow: 'scroll' + }) + .append(Dom.c('div').css({ + width: '100%', + height: '10px' + }))) + .appendTo('body'); + var outer = n.children(); + var inner = outer.children(); + browser.barWidth = outer.get(0).offsetWidth - outer.get(0).clientWidth; + browser.scrollbarLeft = Math.round(inner.offset().left) !== 1; + n.remove(); } - // set - util.set(dataProp)(json, write); + Object.assign(ctx.browser, browser); + ctx.scroll.barWidth = browser.barWidth; } + /** - * Very similar to ajaxDataSrc, but for the other SSP properties + * Add a column to the list used for the table with default values * * @param settings DataTables settings object - * @param param Target parameter - * @param json JSON data - * @returns Resolved value */ -function ajaxDataSrcParam(settings, param, json) { - var dataSrc = util.is.plainObject(settings.ajax) - ? settings.ajax.dataSrc // TODO - : null; - if (dataSrc && dataSrc[param]) { - // Get from custom location - return util.data.get(dataSrc[param])(json); +function addColumn(settings) { + // Add column to aoColumns array + let columnIdx = settings.columns.length; + let column = util.object.assign({}, new Settings(), defaults$2, { + orderData: defaults$2.orderData + ? defaults$2.orderData + : [columnIdx], + data: defaults$2.data ? defaults$2.data : columnIdx, + idx: columnIdx, + searchFixed: {}, + colEl: Dom + .c('col') + .attr('data-dt-column', columnIdx) + }); + settings.columns.push(column); + // Legacy support for `searchCols` property. If set, and there is a value + // for this column, then it should be applied to the search. The new, column + // specific `search` option is applied in `columnOptions`, but we always + // want the search object for the column to exist. + let searchCols = settings.searchCols; + settings.searches[columnIdx] = create$1(searchCols[columnIdx] + ? hungarianToCamel(searchCols[columnIdx]) + : {}); + settings.searches[columnIdx].columns = [columnIdx]; +} +/** + * Apply options for a column + * + * @param settings DataTables settings object + * @param colIdx column index to consider + * @param options Column configuration options + */ +function columnOptions(settings, colIdx, options) { + var column = settings.columns[colIdx]; + /* User specified column options */ + if (options !== undefined && options !== null) { + // Backwards compatibility + compatCols(options); + if (options.type) { + column.typeManual = options.type; + } + // `class` is a reserved word in JavaScript, so we need to provide + // the ability to use a valid name for the camel case input + if (options.className && !options.className) { + options.className = options.className; + } + var origClass = column.className; + util.object.assign(column, options); + map(column, options, 'width', 'widthOrig'); + // Merge class from previously defined classes with this one, rather + // than just overwriting it in the extend above + if (origClass !== column.className) { + column.className = origClass + ' ' + column.className; + } + map(column, options, 'orderData'); + // Search term specifically for this column + if (options.search) { + util.object.assign(settings.searches[colIdx], options.search); + } } - // else - Default behaviour - var old = ''; - // Legacy support - if (param === 'draw') { - old = 'sEcho'; + /* Cache the data get and set functions for speed */ + var dataSrc = column.data; + var dataFn = util.get(dataSrc); + // The `render` option can be given as an array to access the helper + // rendering methods. The first element is the rendering method to use, the + // rest are the parameters to pass + if (column.render && Array.isArray(column.render)) { + var copy = column.render.slice(); + var name = copy.shift(); + column.render = helpers[name].apply(window, copy); } - else if (param === 'recordsTotal') { - old = 'iTotalRecords'; + column.renderer = column.render ? util.get(column.render) : null; + var attrTest = function (src) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + column.attrSrc = + !!dataSrc && + util.is.plainObject(dataSrc) && + (attrTest(dataSrc.sort) || + attrTest(dataSrc.type) || + attrTest(dataSrc.filter)); + column.setter = null; + column.dataGet = function (rowData, type, meta) { + var innerData = dataFn(rowData, type, undefined, meta); + return column.renderer && type + ? column.renderer(innerData, type, rowData, meta) + : innerData; + }; + column.dataSet = function (rowData, val, meta) { + return util.set(dataSrc)(rowData, val, meta); + }; + // Indicate if DataTables should read DOM data as an object or array + // Used in _fnGetRowElements + if (typeof dataSrc !== 'number' && !column._isArrayHost) { + settings.rowReadObject = true; } - else if (param === 'recordsFiltered') { - old = 'iTotalDisplayRecords'; + // Feature sorting overrides column specific when off + if (!settings.features.ordering) { + column.orderable = false; } - return json[old] !== undefined ? json[old] : json[param]; } - -const __filter_div = Dom.c('div').get(0); -const __filter_div_textContent = __filter_div.textContent !== undefined; /** - * Filter the table using both the global filter and column based filtering + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! * * @param settings DataTables settings object */ -function filterComplete(settings) { - settings.columns; - // In server-side processing all filtering is done by the server, so no - // point hanging around here - if (dataSource(settings) != 'ssp') { - // Check if any of the rows were invalidated - filterData(settings); - // Start from the full data set - settings.display = settings.displayMaster.slice(); - // Column set filters first - util.object.each(settings.searches, (key, s) => { - filter(settings.display, settings, s.search, s); - }); - // Fixed (named) filters next - util.object.each(settings.searchesFixed, function (columns) { - util.object.each(settings.searchesFixed[columns], function (name, s) { - filter(settings.display, settings, s.search, s); - }); - }); - // And finally legacy global filtering - filterCustom(settings); +function adjustColumnSizing(settings) { + calculateColumnWidths(settings); + columnSizes(settings); + let scroll = settings.scroll; + if (scroll.y !== '' || scroll.x !== '') { + scrollDraw(settings); } - // Tell the draw function we have been filtering - settings.wasFiltered = true; - callbackFire(settings, null, 'search', [settings]); + callbackFire(settings, null, 'column-sizing', [settings]); } /** - * Apply custom filtering functions - * - * This is legacy now that we have named functions, but it is widely used - * from 1.x, so it is not yet deprecated. + * Apply column sizes * * @param settings DataTables settings object */ -function filterCustom(settings) { - let filters = ext.search; - let displayRows = settings.display; - let row, rowIdx; - for (let i = 0, iLen = filters.length; i < iLen; i++) { - let rows = []; - // Loop over each row and see if it should be included - for (let j = 0, jen = displayRows.length; j < jen; j++) { - rowIdx = displayRows[j]; - row = settings.data[rowIdx]; - if (row && - filters[i](settings, row.searchCellCache, rowIdx, row.data, j)) { - rows.push(rowIdx); +function columnSizes(settings) { + let cols = settings.columns; + for (let i = 0; i < cols.length; i++) { + let width = columnsSumWidth(settings, [i], false); + if (width) { + cols[i].colEl.css('width', width); + if (settings.scroll.x) { + cols[i].colEl.css('min-width', width); } } - // So the array reference doesn't break set the results into the - // existing array - displayRows.length = 0; - arrayApply(displayRows, rows); } } /** - * Filter the data table based on user input and draw the table + * Convert the index of a visible column to the index in the data array (take + * account of hidden columns) * - * @param searchRows - * @param settings - * @param input - * @param options - * @returns - */ -function filter(searchRows, settings, input, options) { - if (input === '') { - return; - } - let i = 0; - let matched = []; - // Search term can be a function, regex or string - if a string we apply our - // smart filtering regex (assuming the options require that) - let searchFunc = typeof input === 'function' ? input : null; - let rpSearch = input instanceof RegExp - ? input - : searchFunc - ? null - : filterCreateSearch(input, options); - let columns = options.columns - ? options.columns - : util.array.range(settings.columns.length); - // Then for each row, does the test pass. If not, lop the row from the array - for (i = 0; i < searchRows.length; i++) { - let row = settings.data[searchRows[i]]; - if (row) { - // Get the data array based on the columns to include in the search - let data = util.array.selectiveJoin(row.searchCellCache, columns); - // Run the search action - if ((searchFunc && - searchFunc(data, row.data, searchRows[i], columns.length === 1 ? columns[0] : columns // compat - )) || - (rpSearch && typeof data === 'string' && rpSearch.test(data))) { - matched.push(searchRows[i]); - } - } - } - // Mutate the searchRows array - searchRows.length = matched.length; - for (i = 0; i < matched.length; i++) { - searchRows[i] = matched[i]; - } -} -/** - * Build a regular expression object suitable for searching a table - */ -function filterCreateSearch(searchIn, inOpts) { - let not = []; - let options = Object.assign({}, { - boundary: false, - caseInsensitive: true, - exact: false, - regex: false, - smart: true - }, inOpts); - let search = typeof searchIn !== 'string' ? searchIn.toString() : searchIn; - // Remove diacritics if normalize is set up to do so - search = util.diacritics(search); - if (options.exact) { - return new RegExp('^' + util.escapeRegex(search) + '$', options.caseInsensitive ? 'i' : ''); - } - search = options.regex ? search : util.escapeRegex(search); - if (options.smart) { - /* For smart filtering we want to allow the search to work regardless of - * word order. We also want double quoted text to be preserved, so word - * order is important - a la google. And a negative look around for - * finding rows which don't contain a given string. - * - * So this is the sort of thing we want to generate: - * - * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ - */ - let parts = search.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g) || [ - '' - ]; - let a = parts.map(function (word) { - let negative = false; - let m; - // Determine if it is a "does not include" - if (word.charAt(0) === '!') { - negative = true; - word = word.substring(1); - } - // Strip the quotes from around matched phrases - if (word.charAt(0) === '"') { - m = word.match(/^"(.*)"$/); - word = m ? m[1] : word; - } - else if (word.charAt(0) === '\u201C') { - // Smart quote match (iPhone users) - m = word.match(/^\u201C(.*)\u201D$/); - word = m ? m[1] : word; - } - // For our "not" case, we need to modify the string that is - // allowed to match at the end of the expression. - if (negative) { - if (word.length > 1) { - not.push('(?!' + word + ')'); - } - word = ''; - } - return word.replace(/"/g, ''); - }); - let match = not.length ? not.join('') : ''; - let boundary = options.boundary ? '\\b' : ''; - search = - '^(?=.*?' + - boundary + - a.join(')(?=.*?' + boundary) + - ')(' + - match + - '.)*$'; - } - return new RegExp(search, options.caseInsensitive ? 'i' : ''); -} -// Update the filtering data for each row if needed (by invalidation or first -// run) -function filterData(settings) { - let columns = settings.columns; - let data = settings.data; - let column; - let j, jen, cellData, row; - let wasInvalidated = false; - for (let rowIdx = 0; rowIdx < data.length; rowIdx++) { - if (!data[rowIdx]) { - continue; - } - row = data[rowIdx]; - if (row && !row.searchCellCache) { - const rowFilterData = []; - for (j = 0, jen = columns.length; j < jen; j++) { - column = columns[j]; - if (column.searchable) { - cellData = getCellData(settings, rowIdx, j, 'filter'); - // Search in DataTables is string based - if (cellData === null) { - cellData = ''; - } - if (typeof cellData !== 'string' && cellData.toString) { - cellData = cellData.toString(); - } - } - else { - cellData = ''; - } - // If it looks like there is an HTML entity in the string, - // attempt to decode it so sorting works as expected. Note that - // we could use a single line of jQuery to do this, but the DOM - // method used here is much faster - // https://jsperf.com/html-decode - if (cellData.indexOf && cellData.indexOf('&') !== -1) { - __filter_div.innerHTML = cellData; - cellData = __filter_div_textContent - ? __filter_div.textContent - : __filter_div.innerText; - } - if (cellData.replace) { - cellData = cellData.replace(/[\r\n\u2028]/g, ''); - } - rowFilterData.push(cellData); + * @param settings DataTables settings object + * @param visIdx Visible column index to lookup + * @returns i the data index + */ +function visibleToColumnIndex(settings, visIdx) { + let aiVis = getColumns(settings, 'visible'); + return typeof aiVis[visIdx] === 'number' ? aiVis[visIdx] : null; +} +/** + * Convert the index of an index in the data array and convert it to the visible + * column index (take account of hidden columns) + * + * @param settings DataTables settings object + * @param match Column index to lookup + * @returns The data index + */ +function columnIndexToVisible(settings, match) { + let aiVis = getColumns(settings, 'visible'); + let iPos = aiVis.indexOf(match); + return iPos !== -1 ? iPos : null; +} +/** + * Get the number of visible columns + * + * @param settings DataTables settings object + * @returns i the number of visible columns + */ +function visibleColumns(settings) { + let layout = settings.header; + let columns = settings.columns; + let vis = 0; + if (layout.length) { + for (let i = 0, iLen = layout[0].length; i < iLen; i++) { + if (columns[i].visible && + Dom.s(layout[0][i].cell).css('display') !== 'none') { + vis++; } - row.searchCellCache = rowFilterData; - row.searchRowCache = rowFilterData.join(' '); - wasInvalidated = true; } } - return wasInvalidated; + return vis; } - /** - * Render and cache a row's display data for the columns, if required + * Get an array of column indexes that match a given property * * @param settings DataTables settings object - * @param rowIdx Row index - * @returns Array with display information + * @param param Parameter in the columns array to look for + * @returns Array of indexes with matched properties */ -function getRowDisplay(settings, rowIdx) { - var rowModal = settings.data[rowIdx]; - var columns = settings.columns; - if (!rowModal) { - return []; - } - if (!rowModal.displayData) { - // Need to render and cache - rowModal.displayData = []; - for (var colIdx = 0, len = columns.length; colIdx < len; colIdx++) { - rowModal.displayData.push(getCellData(settings, rowIdx, colIdx, 'display')); +function getColumns(settings, param) { + let a = []; + settings.columns.map(function (val, i) { + if (val[param]) { + a.push(i); } - } - return rowModal.displayData; + }); + return a; } /** - * Create a new TR element (and it's TD children) for a row + * Allow the result from a type detection function to be `true` while + * translating that into a string. Old type detection functions will return the + * type name if it passes. An object store would be better, but not backwards + * compatible. * + * @param typeDetect Object or function for type detection + * @param res Result from the type detection function + * @returns Type name or false + */ +function _typeResult(typeDetect, res) { + return res === true ? typeDetect._name : res; +} +/** + * Calculate the 'type' of a column * @param settings DataTables settings object - * @param rowIdx Row to consider - * @param trIn TR element to add to the table - optional. If not given, - * DataTables will create a row automatically - * @param tds Array of TD|TH elements for the row - must be given if trIn is. */ -function createTr(settings, rowIdx, trIn, tds) { - var row = settings.data[rowIdx], cells = [], tr, td, column, i, iLen, create, trClass = settings.classes.tbody.row; - if (row && row.tr === null) { - let rowData = row.data; - tr = trIn || document.createElement('tr'); - row.tr = tr; - row.cells = cells; - Dom.s(tr).classAdd(trClass); - // Use a private property on the node to allow reserve mapping from the node - // to the aoData array for fast look up - tr._DT_RowIndex = rowIdx; - // Special parameters can be given by the data source to be used on the - // row - rowAttributes(settings, row); - /* Process each column */ - for (i = 0, iLen = settings.columns.length; i < iLen; i++) { - column = settings.columns[i]; - create = trIn && tds && tds[i] ? false : true; - td = create - ? document.createElement(column.cellType) - : tds[i]; - if (!td) { - log(settings, 0, 'Incorrect column count', 18); - } - td._DT_CellIndex = { - row: rowIdx, - column: i - }; - cells.push(td); - var display = getRowDisplay(settings, rowIdx); - // Need to create the HTML if new, or if a rendering function is - // defined - if (create || - ((column.render || column.data !== i) && - (!util.is.plainObject(column.data) || - (column.data && - column.data._ !== i + '.display')))) { - writeCell(td, display[i]); - } - // column class - Dom.s(td).classAdd(column.className); - // Visibility - add or remove as required - if (column.visible && create) { - tr.appendChild(td); +function columnTypes(settings, originalTypes = '') { + var columns = settings.columns; + var data = settings.data; + var types = ext.type.detect; + var i, iLen, j, jen, k, ken; + var col, detectedType, cache; + if (!originalTypes) { + originalTypes = columns.map(c => c.type).join(','); + } + // For each column, spin over the data type detection functions, seeing if + // one matches + for (i = 0, iLen = columns.length; i < iLen; i++) { + col = columns[i]; + cache = []; + if (!col.type && col.typeManual) { + col.type = col.typeManual; + } + else if (!col.type) { + // With SSP type detection can be unreliable and error prone, so we + // provide a way to turn it off. + if (!settings.typeDetect) { + return; } - else if (!column.visible && !create) { - td.parentNode.removeChild(td); + for (j = 0, jen = types.length; j < jen; j++) { + let typeDetect = types[j]; + let oneOf; + let allOf; + let init; + let one = false; + // There can be either one, or three type detection functions + if (typeof typeDetect === 'function') { + allOf = typeDetect; + } + else { + oneOf = typeDetect.oneOf; + allOf = typeDetect.allOf; + init = typeDetect.init; + } + detectedType = null; + // Fast detect based on column assignment + if (init) { + detectedType = _typeResult(typeDetect, init(settings, col, i)); + if (detectedType) { + col.type = detectedType; + break; + } + } + for (k = 0, ken = data.length; k < ken; k++) { + if (!data[k]) { + continue; + } + // Use a cache array so we only need to get the type data + // from the formatter once (when using multiple detectors) + if (cache[k] === undefined) { + cache[k] = getCellData(settings, k, i, 'type'); + } + // Only one data point in the column needs to match this + // function + if (oneOf && !one) { + one = _typeResult(typeDetect, oneOf(cache[k], settings)); + } + // All data points need to match this function + detectedType = _typeResult(typeDetect, allOf(cache[k], settings)); + // If null, then this type can't apply to this column, so + // rather than testing all cells, break out. There is an + // exception for the last type which is `html`. We need to + // scan all rows since it is possible to mix string and HTML + // types + if (!detectedType && j !== types.length - 3) { + break; + } + // Only a single match is needed for html type since it is + // bottom of the pile and very similar to string - but it + // must not be empty + if (detectedType === 'html' && !util.is.empty(cache[k])) { + break; + } + } + // Type is valid for all data points in the column - use this + // type + if ((oneOf && one && detectedType) || + (!oneOf && detectedType)) { + col.type = detectedType; + break; + } } - if (column.createdCell) { - column.createdCell.call(settings.instance, td, getCellData(settings, rowIdx, i), rowData, rowIdx, i); + // Fall back - if no type was detected, always use string + if (!col.type) { + col.type = 'string'; } } - callbackFire(settings, 'rowCreated', 'row-created', [ - tr, - rowData, - rowIdx, - cells - ]); - } - else if (row) { - Dom.s(row.tr).classAdd(trClass); + // Set class names for header / footer for auto type classes + var autoClass = ext.type.className[col.type]; + if (autoClass) { + _columnAutoClass(settings.header, i, autoClass); + _columnAutoClass(settings.footer, i, autoClass); + } + var renderer = ext.type.render[col.type]; + // This can only happen once! There is no way to remove + // a renderer. After the first time the renderer has + // already been set so createTr will run the renderer itself. + if (renderer && !col.renderer) { + col.renderer = util.get(renderer); + _columnAutoRender(settings, i); + } + } + var newTypes = columns.map(c => c.type).join(','); + if (newTypes !== originalTypes) { + callbackFire(settings, null, 'columnTypes', [settings], false); + } +} +/** + * Apply an auto detected renderer to data which doesn't yet have a renderer + */ +function _columnAutoRender(settings, colIdx) { + let data = settings.data; + for (let i = 0; i < data.length; i++) { + let d = data[i]; + if (d && d.tr) { + // We have to update the display here since there is no invalidation + // check for the data + let display = getCellData(settings, i, colIdx, 'display'); + d.displayData[colIdx] = display; + writeCell(d.cells[colIdx], display); + // No need to update sort / filter data since it has been + // invalidated and will be re-read with the renderer now applied + } } } /** - * Add attributes to a row based on the special `DT_*` parameters in a data - * source object. + * Apply a class name to a column's header cells * - * @param settings DataTables settings object - * @param row Row object for the row to be modified + * @param container The header / footer structure array + * @param colIdx Column index + * @param className Class name to apply */ -function rowAttributes(settings, row) { - var tr = row.tr; - var data = row.data; - if (tr) { - var id = settings.rowIdFn(data); - if (id) { - tr.id = id; - } - if (data.DT_RowClass) { - // Remove any classes added by DT_RowClass before - var a = data.DT_RowClass.split(' '); - row.addedClasses = row.addedClasses - ? util.unique(row.addedClasses.concat(a)) - : a; - Dom.s(tr) - .classRemove(row.addedClasses.join(' ')) - .classAdd(data.DT_RowClass); - } - if (data.DT_RowAttr) { - Dom.s(tr).attr(data.DT_RowAttr); - } - if (data.DT_RowData) { - Dom.s(tr).data(data.DT_RowData); +function _columnAutoClass(container, colIdx, className) { + container.forEach(function (row) { + if (row[colIdx] && row[colIdx].unique) { + Dom.s(row[colIdx].cell).classAdd(className); } - } + }); } /** - * Create the HTML header for the table + * Take the column definitions and static columns arrays and calculate how they + * relate to column indexes. The callback function will then apply the + * definition found for a column to a suitable configuration object. * - * @param settings DataTable instance - * @param side If the header or footer should be used - * @returns + * @param settings DataTables settings object + * @param aoColDefs The aoColumnDefs array that is to be applied + * @param aoCols The aoColumns array that defines columns individually + * @param headerLayout Layout for header as it was loaded + * @param fn Callback function - takes two parameters, the calculated column + * index and the definition for that column. */ -function buildHead(settings, side) { - let classes = settings.classes; - let columns = settings.columns; - let i, iLen, row; - let target = Dom.s(side === 'header' ? settings.thead : settings.tfoot); - let titleProp = side === 'header' ? 'title' : side; - // Footer might be defined - if (!target) { - return; - } - // If no cells yet and we have content for them, then create - if (side === 'header' || - util.array.pluck(settings.columns, titleProp).join('')) { - row = target.find('tr'); - // Add a row if needed - if (!row.count()) { - row = Dom.c('tr').appendTo(target); - } - // Add the number of cells needed to make up to the number of columns - if (row.count() === 1) { - let cellCount = 0; - row.find('td, th').each(el => { - cellCount += el.colSpan; - }); - for (i = cellCount, iLen = columns.length; i < iLen; i++) { - Dom.c('th') - .html(columns[i][titleProp] || '') - .appendTo(row); +function applyColumnDefs(settings, aoColDefs, aoCols, headerLayout, fn) { + var i, iLen, j, jLen, k, kLen; + var columns = settings.columns; + if (aoCols) { + for (i = 0, iLen = aoCols.length; i < iLen; i++) { + // Compat + if (aoCols[i] && aoCols[i].name) { + columns[i].name = aoCols[i].name; } } } - let detected = detectHeader(settings, target.get(0), true); - if (side === 'header') { - settings.header = detected; - target.find('tr').classAdd(classes.thead.row); + // Column definitions with aTargets + if (aoColDefs) { + // Loop over the definitions array - loop in reverse so first instance + // has priority + for (i = aoColDefs.length - 1; i >= 0; i--) { + let def = aoColDefs[i]; + /* Each definition can target multiple columns, as it is an array */ + let aTargets = def.target !== undefined + ? def.target + : def.targets !== undefined + ? def.targets + : def.aTargets; // legacy + if (!Array.isArray(aTargets)) { + aTargets = [aTargets]; + } + for (j = 0, jLen = aTargets.length; j < jLen; j++) { + var target = aTargets[j]; + if (typeof target === 'number' && target >= 0) { + /* Add columns that we don't yet know about */ + while (columns.length <= target) { + addColumn(settings); + } + /* Integer, basic index */ + fn(target, def); + } + else if (typeof target === 'number' && target < 0) { + /* Negative integer, right to left column counting */ + fn(columns.length + target, def); + } + else if (typeof target === 'string') { + for (k = 0, kLen = columns.length; k < kLen; k++) { + if (target === '_all') { + // Apply to all columns + fn(k, def); + } + else if (target.indexOf(':name') !== -1) { + // Column selector + if (columns[k].name === target.replace(':name', '')) { + fn(k, def); + } + } + else { + // Cell selector + headerLayout.forEach(function (row) { + if (row[k]) { + var cell = row[k].cell; + // Legacy support. Note that it means that + // we don't support an element name selector + // only, since they are treated as class + // names for 1.x compat. + if (target.match(/^[a-z][\w-]*$/i)) { + target = '.' + target; + } + if (cell.matches(target)) { + fn(k, def); + } + } + }); + } + } + } + } + } } - else { - settings.footer = detected; - target.find('tr').classAdd(classes.tfoot.row); + // Statically defined columns array + if (aoCols) { + for (i = 0, iLen = aoCols.length; i < iLen; i++) { + fn(i, aoCols[i]); + } } - // Every cell needs to be passed through the renderer - target - .children('tr') - .children('th, td') - .each(el => { - // Should just be able to do `renderer(settings, side)` here but - // Typescript doesn't like it, despite it already being constrained! - let runner = side === 'header' - ? renderer(settings, 'header') - : renderer(settings, 'footer'); - runner(settings, Dom.s(el), classes); - }); } /** - * Build a layout structure for a header or footer + * Get the width for a given set of columns * - * @param settings DataTables settings - * @param source Source layout array - * @param incColumns What columns should be included - * @returns Layout array in column index order + * @param settings DataTables settings object + * @param targets Columns - comma separated string or array of numbers + * @param original Use the original width (true) or calculated (false) + * @param incVisible Include visible columns (true) or not (false) + * @returns Combined CSS value */ -function headerLayout(settings, source, incColumns) { - var row, column, cell; - var local = []; - var structure = []; - var columns = settings.columns; - var columnCount = columns.length; - var rowspan, colspan; - if (!source) { - return; - } - // Default is to work on only visible columns - if (!incColumns) { - incColumns = util.array.range(columnCount).filter(function (idx) { - return columns[idx].visible; - }); - } - // Make a copy of the master layout array, but with only the columns we want - for (row = 0; row < source.length; row++) { - // Remove any columns we haven't selected - local[row] = source[row].slice().filter(function (c, i) { - return incColumns.includes(i); - }); - // Prep the structure array - it needs an element for each row - structure.push([]); +function columnsSumWidth(settings, targets, original, incVisible) { + if (!Array.isArray(targets)) { + targets = columnsFromHeader(targets); } - for (row = 0; row < local.length; row++) { - for (column = 0; column < local[row].length; column++) { - rowspan = 1; - colspan = 1; - // Check to see if there is already a cell (row/colspan) covering - // our target insert point. If there is, then there is nothing to - // do. - if (structure[row][column] === undefined) { - cell = local[row][column].cell; - // Expand for rowspan - while (local[row + rowspan] !== undefined && - local[row][column].cell == local[row + rowspan][column].cell) { - structure[row + rowspan][column] = null; - rowspan++; - } - // And for colspan - while (local[row][column + colspan] !== undefined && - local[row][column].cell == local[row][column + colspan].cell) { - // Which also needs to go over rows - for (var k = 0; k < rowspan; k++) { - structure[row + k][column + colspan] = null; - } - colspan++; - } - var titleSpan = Dom.s(cell).find('.dt-column-title'); - structure[row][column] = { - cell: cell, - colspan: colspan, - rowspan: rowspan, - title: titleSpan.count() - ? titleSpan.html() - : Dom.s(cell).html() - }; + let sum = 0; + let unit = 'px'; + let columns = settings.columns; + for (let i = 0, iLen = targets.length; i < iLen; i++) { + let column = columns[targets[i]]; + let definedWidth = original ? column.widthOrig : column.width; + if (column.visible === false) { + continue; + } + if (definedWidth === null || definedWidth === undefined) { + return null; // can't determine a defined width - browser defined + } + else if (typeof definedWidth === 'number') { + sum += definedWidth; + } + else { + let matched = definedWidth.match(/([\d\.]+)([^\d]*)/); + if (matched) { + sum += parseFloat(matched[1]); + unit = matched.length === 3 ? matched[2] : 'px'; } } } - return structure; + return sum + unit; } /** - * Draw the header (or footer) element based on the column visibility states. + * Determine what columns a header cell covers (can be multiple for colspan + * cases). * - * @param settings DataTables settings object - * @param source Layout array from detectHeader + * @param cell The header cell in question + * @returns An array of column indexes */ -function drawHead(settings, source) { - let layout = headerLayout(settings, source); - let tr; - if (!layout) { - return; +function columnsFromHeader(cell) { + let attr = Dom.s(cell).closest('[data-dt-column]').attr('data-dt-column'); + if (!attr) { + return []; } - for (let row = 0; row < source.length; row++) { - tr = source[row].row; - // All cells are going to be replaced, so empty out the row - if (tr) { - Dom.s(tr).detachChildren(); - } - for (let column = 0; column < layout[row].length; column++) { - let point = layout[row][column]; - if (point) { - Dom.s(point.cell) - .appendTo(tr) - .attr('rowspan', point.rowspan) - .attr('colspan', point.colspan); + return attr.split(',').map(function (val) { + return parseInt(val); + }); +} +/** + * Get cells from the header or footer, including a specific row and / or cell + * + * @param header The header or footer strcture + * @param row A specific row, or null + * @param column A specific column, or null + * @returns An array of all matching cells + */ +function columnCells(header, row = null, column = null) { + var out = []; + for (var i = 0; i < header.length; i++) { + if (row === null || row === i) { + for (var j = 0; j < header[i].length; j++) { + var cell = header[i][j].cell; + if ((column === null || column === j) && !out.includes(cell)) { + out.push(cell); + } } } } + return out; } /** - * Insert the required TR nodes into the table for display + * Get cells that apply for ordering handler or icons * - * @param settings DataTables settings object - * @param ajaxComplete true after ajax call to complete rendering + * @param settings Context + * @param notSelector DOM selector to exclude elements + * @returns Array of selected elements */ -function draw(settings, ajaxComplete) { - // Allow for state saving and a custom start position - setStartPosition(settings); - // Provide a pre-callback function which can be used to cancel the draw is - // false is returned - var aPreDraw = callbackFire(settings, 'preDraw', 'preDraw', [settings]); - if (aPreDraw.indexOf(false) !== -1) { - processingDisplay(settings, false); - return; +function columnOrderingCells(settings, notSelector) { + var cells = []; + var titleRow = settings.titleRow; + if (titleRow === true) { + // Top row (legacy `orderCellsTop`) + cells = columnCells(settings.header, 0); } - var rowEls = []; - var rowCount = 0; - var isServerSide = dataSource(settings) == 'ssp'; - var display = settings.display; - var start = settings.displayStart; - var end = displayEnd(settings); - var columns = settings.columns; - var body = Dom.s(settings.tbody); - settings.doingDraw = true; - /* Server-side processing draw intercept */ - if (settings.deferLoading) { - settings.deferLoading = false; - settings.drawCount++; - processingDisplay(settings, false); + else if (titleRow === false) { + // Bottom row (legacy `orderCellsTop`) + cells = columnCells(settings.header, settings.header.length - 1); } - else if (!isServerSide) { - settings.drawCount++; + else if (titleRow !== null) { + // Specific row + cells = columnCells(settings.header, titleRow); } - else if (!settings.destroying && !ajaxComplete) { - // Show loading message for server-side processing - if (settings.drawCount === 0) { - body.empty().append(_emptyRow(settings)); - } - ajaxUpdate(settings); + else { + // All + cells = columnCells(settings.header); + } + return Dom.s(cells) + .filter('th' + notSelector + ', td' + notSelector) + .filter(el => { + return (Dom.s(el) + .parent() + .filter(notSelector) + .length !== 0); + }); +} + +const footer = (settings, cell, classes) => { + cell.classAdd(classes.tfoot.cell); +}; +const header = (settings, cell, classes) => { + cell.classAdd(classes.thead.cell); + if (!settings.features.ordering) { + cell.classAdd(classes.order.none); + } + // Conditions to not apply the ordering icons + if (!columnOrderingCells(settings, ':not([data-dt-order="disable"])') + .get() + .includes(cell[0])) { return; } - if (display.length !== 0) { - var iStart = isServerSide ? 0 : start; - var iEnd = isServerSide ? settings.data.length : end; - for (var j = iStart; j < iEnd; j++) { - var dataIdx = display[j]; - var data = settings.data[dataIdx]; - // Row has been deleted - can't be displayed - if (data === null) { - continue; - } - // Row node hasn't been created yet - if (data.tr === null) { - createTr(settings, dataIdx); + // No additional mark-up required. Attach a sort listener to update on sort + // - note that using the `DT` namespace will allow the event to be removed + // automatically on destroy, while the `dt` namespaced event is the one we + // are listening for + Dom.s(settings.table).on('order.dt.DT column-visibility.dt.DT', function (e, ctx, column) { + if (settings !== ctx) { + // need to check if this is the host + return; // table, not a nested one + } + var sorting = ctx.sortDetails; + if (!sorting) { + return; + } + var orderedColumns = pluck(sorting, 'col'); + // This handler is only needed on column visibility if the column is + // part of the ordering. If it isn't, then we can bail out to save + // performance. It could be a separate event handler, but this is a + // balance between code reuse / size and performance console.log(e, + // e.name, column, orderedColumns, orderedColumns.includes(column)) + if (e.type === 'column-visibility' && + !orderedColumns.includes(column)) { + return; + } + var i; + var orderClasses = classes.order; + var columns = ctx.api.columns(cell); + var col = settings.columns[columns.flatten()[0]]; + var orderable = columns.orderable().includes(true); + var ariaType = ''; + var indexes = columns.indexes(); + var sortDirs = columns.orderable(true).flatten(); + var tabIndex = settings.tabIndex; + var canOrder = ctx.orderHandler && orderable; + cell.classRemove(orderClasses.isAsc + ' ' + orderClasses.isDesc) + .classToggle(orderClasses.none, !orderable) + .classToggle(orderClasses.canAsc, canOrder && sortDirs.includes('asc')) + .classToggle(orderClasses.canDesc, canOrder && sortDirs.includes('desc')); + // Determine if all of the columns that this cell covers are + // included in the current ordering + var isOrdering = true; + for (i = 0; i < indexes.length; i++) { + if (!orderedColumns.includes(indexes[i])) { + isOrdering = false; } - var nRow = data.tr; - // Add various classes as needed - for (var i = 0; i < columns.length; i++) { - var col = columns[i]; - var td = data.cells[i]; - Dom.s(td) - .classAdd(col.type ? ext.type.className[col.type] : null) // auto class - .classAdd(settings.classes.tbody.cell); // all cells + } + if (isOrdering) { + // Get the ordering direction for the columns under this cell + // Note that it is possible for a cell to be asc and desc + // sorting (column spanning cells) + var orderDirs = columns.order(); + cell.classAdd((orderDirs.includes('asc') ? orderClasses.isAsc : '') + + (orderDirs.includes('desc') ? orderClasses.isDesc : '')); + } + // Find the first visible column that has ordering applied to it - + // it get's the aria information, as the ARIA spec says that only + // one column should be marked with aria-sort + var firstVis = -1; // column index + for (i = 0; i < orderedColumns.length; i++) { + if (settings.columns[orderedColumns[i]].visible) { + firstVis = orderedColumns[i]; + break; } - // Row callback functions - might want to manipulate the row - // rowCount and j are not currently documented. Are they at all - // useful? - callbackFire(settings, 'row', null, [ - nRow, - data.data, - rowCount, - j, - dataIdx - ]); - rowEls.push(nRow); - rowCount++; } - } - else { - rowEls[0] = _emptyRow(settings); - } - /* Header and footer callbacks */ - callbackFire(settings, 'header', 'header', [ - Dom.s(settings.thead).children('tr').get(0), - getDataMaster(settings), - start, - end, - display - ]); - callbackFire(settings, 'footer', 'footer', [ - Dom.s(settings.tfoot).children('tr').get(0), - getDataMaster(settings), - start, - end, - display - ]); - body.detachChildren().append(rowEls); - // Empty table needs a specific class - Dom.s(settings.tableWrapper).classToggle('dt-empty-footer', Dom.s(settings.tfoot).find('tr').count() === 0); - // Call all required callback functions for the end of a draw - callbackFire(settings, 'draw', 'draw', [settings], true); - // Draw is complete, sorting and filtering must be as well - settings.wasOrdered = false; - settings.wasFiltered = false; - settings.doingDraw = false; -} -/** - * Redraw the table - taking account of the various features which are enabled - * - * @param settings DataTables settings object - * @param holdPosition Keep the current paging position. By default the paging - * is reset to the first page - * @param recompute Indicate if a rebuild of sort and filter should happen - */ -function reDraw(settings, holdPosition, recompute) { - let features = settings.features, doSort = features.ordering, doFilter = features.searching; - if (recompute === undefined || recompute === true) { - // Resolve any column types that are unknown due to addition or - // invalidation - columnTypes(settings); - columnWidths(settings); - if (doSort) { - sort(settings); + if (indexes[0] == firstVis) { + var firstSort = sorting[0]; + var sortOrder = col.orderSequence; + cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending'); + // Determine if the next click will remove sorting or change the + // sort + ariaType = + sortOrder && !sortOrder[firstSort.index + 1] + ? 'Remove' + : 'Reverse'; } - if (doFilter) { - filterComplete(settings); + else { + cell.attrRemove('aria-sort'); + } + // Make the headers tab-able for keyboard navigation + if (orderable) { + var orderSpan = cell.find('.dt-column-order'); + orderSpan + .attr('role', 'button') + .attr('aria-label', orderable + ? col.ariaTitle + + ctx.api.i18n('aria.orderable' + ariaType) + : col.ariaTitle); + if (tabIndex !== -1) { + orderSpan.attr('tabindex', tabIndex); + } + } + }); +}; +const layout = (settings, container, items) => { + let classes = settings.classes.layout; + let row = Dom.c('div') + .attr('id', items.id || null) + .classAdd(items.className || classes.row) + .appendTo(container); + displayRowCells(items, function (key, val) { + var klass = ''; + if (val.table) { + row.classAdd(classes.tableRow); + klass += classes.tableCell + ' '; + } + if (key === 'start') { + klass += classes.start; + } + else if (key === 'end') { + klass += classes.end; } else { - // No filtering, so we want to just use the display master - settings.display = settings.displayMaster.slice(); + klass += classes.full; } - } - if (holdPosition !== true) { - settings.displayStart = 0; - } - else { - // Keep position, but make sure that there is actually data to display, - // otherwise we need to rewind a bit (e.g. if rows were deleted) - lengthOverflow(settings); - } - // Let any modules know about the draw hold position state (used by - // scrolling internally) - settings.drawHold = holdPosition; - draw(settings); - settings.api.one('draw', function () { - settings.drawHold = false; + Dom.c('div') + .attr({ + id: val.id || null, + class: val.className + ? val.className + : classes.cell + ' ' + klass + }) + .append(val.contents) + .appendTo(row); }); -} -/** - * Table is empty - create a row with an empty message in it - * - * @param settings DataTables context - */ -function _emptyRow(settings) { - let lang = settings.language; - let zero = lang.zeroRecords; - let dataSrc = dataSource(settings); - // Make use of the fact that settings.json is only set once the initial data - // has been loaded. Show loading when that isn't the case - if ((dataSrc === 'ssp' || dataSrc === 'ajax') && !settings.json) { - zero = lang.loadingRecords; - } - else if (lang.emptyTable && recordsTotal(settings) === 0) { - zero = lang.emptyTable; +}; +const pagingButton = (settings, buttonType, content, active, disabled) => { + var classes = settings.classes.paging; + var btnClasses = [classes.button]; + var btn; + if (active) { + btnClasses.push(classes.active); } - return Dom - .c('tr') - .append(Dom - .c('td') - .attr('colSpan', visibleColumns(settings)) - .classAdd(settings.classes.empty.row) - .html(zero)) - .get(0); -} -/** - * Use the DOM source to create up an array of header cells. The idea here is to - * create a layout grid (array) of rows x columns, which contains a reference to - * the cell at that point in the grid (regardless of col/rowspan), such that any - * column / row could be removed and the new grid constructed. - * - * @param settings DataTables context - * @param thead thead / tbody element - * @param write If cells should be written (if required) - * @returns Calculated layout array - */ -function detectHeader(settings, thead, write) { - let columns = settings.columns; - let rows = Dom.s(thead).children('tr'); - let row, loopCell; - let i, k, l, len, shifted, column, colspan, rowspan; - let titleRow = settings.titleRow; - let isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; - let layout = []; - let isUnique; - let shift = function (a, b, j) { - let d = a[b]; - while (d[j]) { - j++; - } - return j; - }; - // We know how many rows there are in the layout - so prep it - for (i = 0, len = rows.count(); i < len; i++) { - layout.push([]); + if (disabled) { + btnClasses.push(classes.disabled); } - for (i = 0, len = rows.count(); i < len; i++) { - row = rows.get(i); - column = 0; - // For every cell in the row.. - loopCell = row.firstChild; - while (loopCell) { - if (loopCell.nodeName.toUpperCase() == 'TD' || - loopCell.nodeName.toUpperCase() == 'TH') { - let cell = Dom.s(loopCell); - let cols = []; - // Get the col and rowspan attributes from the DOM and sanitise - // them - colspan = parseInt(cell.attr('colspan') || '1') || 1; - rowspan = parseInt(cell.attr('rowspan') || '1') || 1; - colspan = - !colspan || colspan === 0 || colspan === 1 ? 1 : colspan; - rowspan = - !rowspan || rowspan === 0 || rowspan === 1 ? 1 : rowspan; - // There might be colspan cells already in this row, so shift - // our target accordingly - shifted = shift(layout, i, column); - // Cache calculation for unique columns - isUnique = colspan === 1 ? true : false; - // Perform header setup - if (write) { - if (isUnique) { - // Allow column options to be set from HTML attributes - columnOptions(settings, shifted, escapeObject(cell.data())); - // Get the width for the column. This can be defined - // from the width attribute, style attribute or - // `columns.width` option - let columnDef = columns[shifted]; - let width = cell.attr('width') || null; - let t = cell - .get(0) - .style.width.match(/width:\s*(\d+[pxem%]+)/); - if (t) { - width = t[1]; - } - columnDef.widthOrig = columnDef.width || width; - if (isHeader) { - // Column title handling - can be user set, or read - // from the DOM This happens before the render, so - // the original is still in place - if (columnDef.title !== null && - !columnDef.autoTitle) { - if ((titleRow === true && i === 0) || // top row - (titleRow === false && - i === rows.count() - 1) || // bottom row - titleRow === i || // specific row - titleRow === null) { - cell.html(columnDef.title); - } - } - if (!columnDef.title && isUnique) { - columnDef.title = util.string.stripHtml(cell.html()); - columnDef.autoTitle = true; - } - } - else { - // Footer specific operations - if (columnDef.footer) { - cell.html(columnDef.footer); - } - } - // Fall back to the aria-label attribute on the table - // header if no ariaTitle is provided. - if (!columnDef.ariaTitle) { - columnDef.ariaTitle = - cell.attr('aria-label') || columnDef.title; - } - // Column specific class names - if (columnDef.className) { - cell.classAdd(columnDef.className); - } - } - // Wrap the column title so we can write to it in future - if (cell.find('div.dt-column-title').count() === 0) { - Dom.c('div') - .classAdd('dt-column-title') - .append(Array.from(cell.get(0).childNodes)) - .appendTo(cell); - } - if (settings.orderIndicators && - isHeader && - cell.filter(':not([data-dt-order=disable])').count() !== - 0 && - cell.parent(':not([data-dt-order=disable])').count() !== - 0 && - cell.find('div.dt-column-order').count() === 0) { - Dom.c('div') - .classAdd('dt-column-order') - .appendTo(cell); - } - // We need to wrap the elements in the header in another - // element to use flexbox layout for those elements - var headerFooter = isHeader ? 'header' : 'footer'; - if (cell.find('div.dt-column-' + headerFooter).count() === - 0) { - Dom.c('div') - .classAdd('dt-column-' + headerFooter) - .append(Array.from(cell.get(0).childNodes)) - .appendTo(cell); - } - } - // If there is col / rowspan, copy the information into the - // layout grid - for (l = 0; l < colspan; l++) { - for (k = 0; k < rowspan; k++) { - layout[i + k][shifted + l] = { - cell: cell.get(0), - unique: isUnique - }; - layout[i + k].row = row; - } - cols.push(shifted + l); - } - // Assign an attribute so spanning cells can still be identified - // as belonging to a column - cell.attr('data-dt-column', util.unique(cols).join(',')); - } - loopCell = loopCell.nextSibling; - } + if (buttonType === 'ellipsis') { + btn = Dom.c('span').classAdd('ellipsis').html(content).get(0); } - return layout; -} -/** - * Set the start position for draw - * - * @param settings DataTables settings object - */ -function setStartPosition(settings) { - var bServerSide = dataSource(settings) == 'ssp'; - var iInitDisplayStart = settings.displayStartInit; - // Check and see if we have an initial draw position from state saving - if (iInitDisplayStart !== undefined && iInitDisplayStart !== -1) { - settings.displayStart = bServerSide - ? iInitDisplayStart - : iInitDisplayStart >= recordsDisplay(settings) - ? 0 - : iInitDisplayStart; - settings.displayStartInit = -1; + else { + btn = Dom.c('button') + .classAdd(btnClasses.join(' ')) + .attr('role', 'link') + .attr('type', 'button') + .html(content) + .get(0); + } + return { + display: btn, + clicker: btn + }; +}; +const pagingContainer = (settings, buttons) => { + // No wrapping element - just append directly to the host + return buttons; +}; +function displayRowCells(items, fn) { + if (items.start) { + fn('start', items.start); + } + if (items.end) { + fn('end', items.end); + } + if (items.full) { + fn('full', items.full); } } + /** - * Get the number of records in the current record set, before filtering - * - * @param ctx DataTables settings object - */ -function recordsTotal(ctx) { - return dataSource(ctx) == 'ssp' - ? ctx.recordsTotal * 1 - : ctx.displayMaster.length; -} -/** - * Get the number of records in the current record set, after filtering + * DataTables extensions * - * @param ctx DataTables settings object - */ -function recordsDisplay(ctx) { - return dataSource(ctx) == 'ssp' - ? ctx.recordsDisplay * 1 - : ctx.display.length; -} -/** - * Get the display end point - display index + * This namespace acts as a collection area for plug-ins that can be used to + * extend DataTables capabilities. Indeed many of the build in methods + * use this method to provide their own capabilities (sorting methods for + * example). * - * @param ctx DataTables settings object + * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy + * reasons */ -function displayEnd(ctx) { - var len = ctx.pageLength, start = ctx.displayStart, calc = start + len, records = ctx.display.length, features = ctx.features, paginate = features.paging; - if (features.serverSide) { - return paginate === false || len === -1 - ? start + records - : Math.min(start + len, ctx.recordsDisplay); - } - else { - return !paginate || calc > records || len === -1 ? records : calc; - } -} +const ext = { + /** + * DataTables build type (expanded by the download builder) + */ + builder: 'bs5/dt-3.0.4', + /** + * Buttons. For use with the Buttons extension for DataTables. This is + * defined here so other extensions can define buttons regardless of load + * order. It is _not_ used by DataTables core. + */ + buttons: {}, + /** + * ColumnControl buttons and content + */ + ccContent: {}, + /** + * Element class names + */ + classes: classes$1, + /** + * Error reporting. + * + * How should DataTables report an error. Can take the value 'alert', + * 'throw', 'none' or a function. + */ + errMode: 'alert', + /** HTML entity escaping */ + escape: { + /** When reading data-* attributes for initialisation options */ + attributes: false + }, + /** + * Legacy so v1 plug-ins don't throw js errors on load + */ + feature: legacy, + /** + * Feature plug-ins. + * + * This is an object of callbacks which provide the features for DataTables + * to be initialised via the `layout` option. + */ + features: features, + /** + * Row searching. + * + * This method of searching is complimentary to the default type based + * searching, and a lot more comprehensive as it allows you complete control + * over the searching logic. Each element in this array is a function + * (parameters described below) that is called for every row in the table, + * and your logic decides if it should be included in the searching data set + * or not. + */ + search: [], + /** + * Selector extensions + * + * The `selector` option can be used to extend the options available for the + * selector modifier options (`selector-modifier` object data type) that + * each of the three built in selector types offer (row, column and cell + + * their plural counterparts). For example the Select extension uses this + * mechanism to provide an option to select only rows, columns and cells + * that have been marked as selected by the end user (`{selected: true}`), + * which can be used in conjunction with the existing built in selector + * options. + */ + selector: { + cell: [], + column: [], + row: [] + }, + settings: [], + /** + * Legacy configuration options. Enable and disable legacy options that + * are available in DataTables. + * + * @type object + */ + legacy: { + /** + * Enable / disable DataTables 1.9 compatible server-side processing + * requests + */ + ajax: null + }, + /** + * Pagination plug-in methods. + * + * Each entry in this object is a function and defines which buttons should + * be shown by the pagination rendering method that is used for the table. + * The renderer addresses how the buttons are displayed in the document, + * while the functions here tell it what buttons to display. This is done by + * returning an array of button descriptions (what each button will do). + */ + pager: pager, + renderer: { + footer: { + _: footer + }, + header: { + _: header + }, + layout: { + _: layout + }, + pagingButton: { + _: pagingButton + }, + pagingContainer: { + _: pagingContainer + } + }, + /** + * Rendering helper function exposed for use by the styling integrations. + */ + rendererDisplayRowCells: displayRowCells, + /** + * Ordering plug-ins - custom data source + * + * The extension options for ordering of data available here is + * complimentary to the default type based ordering that DataTables + * typically uses. It allows much greater control over the data that is + * being used to order a column, but is necessarily therefore more complex. + */ + order: {}, + /** + * Type based plug-ins. + * + * Each column in DataTables has a type assigned to it, either by automatic + * detection or by direct assignment using the `type` option for the column. + * The type of a column will effect how it is ordering and search (plug-ins + * can also make use of the column type if required). + */ + type: store, + /** + * Unique DataTables instance counter + * + * @type int + * @private + */ + _unique: 0, + // + // Depreciated + // The following properties are retained for backwards compatibility only. + // The should not be used in new projects and will be removed in a future + // version + // + /** + * Software version + * @type string + */ + version: '3.0.4' +}; +// +// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts +// +Object.assign(ext, { + afnFiltering: ext.search, + aTypes: ext.type.detect, + ofnSearch: ext.type.search, + oSort: ext.type.order, + afnSortData: ext.order, + aoFeatures: ext.feature, + oStdClasses: ext.classes, + oPagination: ext.pager, + sVersion: ext.version, + fnVersionCheck: check$1 +}); /** * Common run function for selector types @@ -9863,10 +9893,7 @@ function columnHeader(settings, column, row) { // backwards compatibility) for (var i = 0; i < header.length; i++) { if (header[i][column].unique && - Dom - .s(header[i][column].cell) - .find('.dt-column-title') - .text()) { + Dom.s(header[i][column].cell).find('.dt-column-title').text()) { target = i; } } @@ -9876,20 +9903,8 @@ function columnHeader(settings, column, row) { } return header[target][column].cell; } -function columnHeaderCells(header) { - var out = []; - for (var i = 0; i < header.length; i++) { - for (var j = 0; j < header[i].length; j++) { - var cell = header[i][j].cell; - if (!out.includes(cell)) { - out.push(cell); - } - } - } - return out; -} function selectColumns(settings, selector, opts) { - var columns = settings.columns, names, titles, nodes = columnHeaderCells(settings.header); + var columns = settings.columns, names, titles; var run = function (s) { var selInt = intVal(s); // Selector - all @@ -9943,8 +9958,8 @@ function selectColumns(settings, selector, opts) { } // Selector if (match && match[1]) { - return Dom - .s(nodes[mapIdx]) + let columnElements = columnCells(settings.header, null, col.idx); + return Dom.s(columnElements) .filter(match[1]) .count() > 0 ? mapIdx @@ -9980,11 +9995,10 @@ function selectColumns(settings, selector, opts) { return [s._DT_CellIndex.column]; } // Selector on the TH elements for the columns - var result = Dom - .s(nodes) + var result = Dom.s(columnCells(settings.header)) .filter(s) .mapTo(el => { - return columnsFromHeader(el); // `nodes` is column index complete and in order + return columnsFromHeader(el); }) .flat() .sort(function (a, b) { @@ -10121,9 +10135,7 @@ registerPlural('columns().titles()', 'column().title()', function (title, row) { row = title; title = undefined; } - var span = Dom - .s(this.column(column).header(row)) - .find('.dt-column-title'); + var span = Dom.s(this.column(column).header(row)).find('.dt-column-title'); if (title !== undefined) { span.html(title); return this; @@ -10194,9 +10206,7 @@ registerPlural('columns().widths()', 'column().width()', function () { // be read, regardless of colspan in the header and rows being present // in the body var columns = this.columns(':visible'); - var row = Dom - .c('tr') - .html('' + Array(columns.count()).join('') + ''); + var row = Dom.c('tr').html('' + Array(columns.count()).join('') + ''); Dom.s(this.table().body()).append(row); var widths = []; var indexes = columns.indexes(); @@ -10907,7 +10917,7 @@ register('search()', function (input, regex, smart, caseInsen) { } let target = ctx.searches['*']; if (!target) { - target = create$2(); + target = create$1(); } if (typeof regex === 'object') { // New style object of options @@ -10942,7 +10952,7 @@ register('search.fixed()', function (name, search, options) { else { let target = fixed[name]; if (!target || !util.is.plainObject(target)) { - target = create$2(); + target = create$1(); } if (options) { assign(target, options); @@ -10966,7 +10976,7 @@ register(['columns().search()', 'column().search()'], function (input, regex, sm let colIdxs = columns.join(','); let target = ctx.searches[colIdxs]; if (!target) { - target = create$2(); + target = create$1(); } // Delete the search for custom grouping types if removing if ((input === '' || input === null) && columns.length > 1) { @@ -11026,7 +11036,7 @@ register(['columns().search.fixed()', 'column().search.fixed()'], function (name else { let target = fixed[name]; if (!target || !util.is.plainObject(target)) { - target = create$2(); + target = create$1(); } if (options) { assign(target, options); @@ -11958,7 +11968,7 @@ register$2('search', function (settings, optsIn) { let searchName = opts.columns === '*' ? '*' : indexes.join(','); let appliedSearch = settings.searches[searchName]; if (!appliedSearch) { - appliedSearch = create$2(); + appliedSearch = create$1(); settings.searches[searchName] = appliedSearch; } appliedSearch.columns = indexes; @@ -12206,8 +12216,8 @@ function create(parts = {}) { var models = { Column: Settings, - Row: create$1, - Search: create$2, + Row: create$2, + Search: create$1, Settings: create }; @@ -12220,7 +12230,7 @@ const defaults = { autoWidth: true, caption: '', classes: {}, - column: defaults$4, + column: defaults$2, columnDefs: null, columns: null, createdRow: null, @@ -12392,8 +12402,8 @@ const DataTable = function (selector, options) { } table.trigger('options.dt', true, [init]); // Backwards compatibility parameter mapping - compatOpts(defaults); - compatCols(defaults$4); + compatOpts(defaults, true); + compatCols(defaults$2); // Allow data properties on the table element to be used as // initialisation options util.object.assign(init, escapeObject(table.data())); @@ -12522,7 +12532,7 @@ const DataTable = function (selector, options) { ]); map(settings.language, config, 'infoCallback'); // Setup global search - settings.searches['*'] = create$2(config.search); + settings.searches['*'] = create$1(config.search); /* Callback functions which are array driven */ callbackReg(settings, 'draw', config.drawCallback); callbackReg(settings, 'stateSaveParams', config.stateSaveParams); From 3347698712d3e99e652ad4394ef2fc0ce2800fdd Mon Sep 17 00:00:00 2001 From: "Berk D. Demir" <11135+bdd@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:34:41 -0700 Subject: [PATCH 33/34] Add `pm-34171-card-scanner` feature flag (#7477) Introduced in 2026.4.1 Mobile releases (Android and iOS), enables use of device camera to autofill a card type item. N.B. This feature relies on Google ML Kit. F-Droid policy disallows Google ML Kit. Because of this, F-Droid builds do not ship with card scanner feature. (See https://github.com/bitwarden/android/pull/6890) --- .env.template | 1 + src/config.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index 8ed6c5c2..62231776 100644 --- a/.env.template +++ b/.env.template @@ -403,6 +403,7 @@ ## - "cxp-export-mobile": Enable the export via CXP on iOS (Clients >= 2025.9.2) ## - "pm-30529-webauthn-related-origins": ## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0) +## - "pm-34171-card-scanner": Enable the new card scanner feature on mobile (Android >= 2026.4.1, iOS >= 2026.4.1) ## - "desktop-ui-migration-milestone-1": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-2": Special feature flag for desktop UI (Desktop >= 2026.2.0) ## - "desktop-ui-migration-milestone-3": Special feature flag for desktop UI (Desktop >= 2026.2.0) diff --git a/src/config.rs b/src/config.rs index 0bb22f7f..9f0ae2e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1440,6 +1440,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "mutual-tls", "cxp-import-mobile", "cxp-export-mobile", + "pm-34171-card-scanner", // Platform Team "pm-30529-webauthn-related-origins", // Vault Team From cc67d644f62605cb46f4d16c4a2eed1a861cc8bb Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:35:11 +0200 Subject: [PATCH 34/34] set user_created bool for each separate invitation (#7753) --- src/api/core/organizations.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 4f490854..36297d30 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1075,7 +1075,7 @@ async fn send_invite( && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) && data.permissions.get("createNewCollections") == Some(&json!(true))); - let mut user_created: bool = false; + let mut user_created: bool; for email in &data.emails { let mut member_status = MembershipStatus::Invited as i32; let user = match User::find_by_mail(email, &conn).await { @@ -1110,6 +1110,7 @@ async fn send_invite( member_status = MembershipStatus::Accepted as i32; } } + user_created = false; user } };