[
  {
    "path": ".dockerignore",
    "content": "// Ignore everything\n*\n\n// Allow what is needed\n!.git\n!docker/healthcheck.sh\n!docker/start.sh\n!macros\n!migrations\n!src\n\n!build.rs\n!Cargo.lock\n!Cargo.toml\n!rustfmt.toml\n!rust-toolchain.toml\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\n\n[*.{rs,py}]\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{yml,yaml}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Ignore vendored scripts in GitHub stats\nsrc/static/scripts/* linguist-vendored\n\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "/.github @dani-garcia @BlackDex\n/.github/** @dani-garcia @BlackDex\n/.github/CODEOWNERS @dani-garcia @BlackDex\n/.github/ISSUE_TEMPLATE/** @dani-garcia @BlackDex\n/.github/workflows/** @dani-garcia @BlackDex\n/SECURITY.md @dani-garcia @BlackDex\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: dani-garcia\nliberapay: dani-garcia\ncustom: [\"https://paypal.me/DaniGG\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\nlabels: [\"bug\"]\nbody:\n  #\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n        Please **do not** submit feature requests or ask for help on how to configure Vaultwarden here!\n\n        The [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions/) has sections for Questions and Ideas.\n\n        Our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki/) has topics on how to configure Vaultwarden.\n\n        Also, make sure you are running [![GitHub Release](https://img.shields.io/github/release/dani-garcia/vaultwarden.svg)](https://github.com/dani-garcia/vaultwarden/releases/latest) of Vaultwarden!\n\n        Be sure to check and validate the Vaultwarden Admin Diagnostics (`/admin/diagnostics`) page for any errors!\n        See here [how to enable the admin page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).\n\n        > [!IMPORTANT]\n        > ## :bangbang: Search for existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=) regarding your topic before posting! :bangbang:\n  #\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Prerequisites\n      description: Please confirm you have completed the following before submitting an issue!\n      options:\n        - label: I have searched the existing **Closed _AND_ Open** [Issues](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue%20) **_AND_** [Discussions](https://github.com/dani-garcia/vaultwarden/discussions?discussions_q=)\n          required: true\n        - label: I have searched and read the [documentation](https://github.com/dani-garcia/vaultwarden/wiki/)\n          required: true\n  #\n  - id: support-string\n    type: textarea\n    attributes:\n      label: Vaultwarden Support String\n      description: Output of the **Generate Support String** from the `/admin/diagnostics` page.\n      placeholder: |\n        1. Go to the Vaultwarden Admin of your instance https://example.domain.tld/admin/diagnostics\n        2. Click on `Generate Support String`\n        3. Click on `Copy To Clipboard`\n        4. Replace this text by pasting it into this textarea without any modifications\n    validations:\n      required: true\n  #\n  - id: version\n    type: input\n    attributes:\n      label: Vaultwarden Build Version\n      description: What version of Vaultwarden are you running?\n      placeholder: ex. v1.34.0 or v1.34.1-53f58b14\n    validations:\n      required: true\n  #\n  - id: deployment\n    type: dropdown\n    attributes:\n      label: Deployment method\n      description: How did you deploy Vaultwarden?\n      multiple: false\n      options:\n        - Official Container Image\n        - Build from source\n        - OS Package (apt, yum/dnf, pacman, apk, nix, ...)\n        - Manually Extracted from Container Image\n        - Downloaded from GitHub Actions Release Workflow\n        - Other method\n    validations:\n      required: true\n  #\n  - id: deployment-other\n    type: textarea\n    attributes:\n      label: Custom deployment method\n      description: If you deployed Vaultwarden via any other method, please describe how.\n  #\n  - id: reverse-proxy\n    type: input\n    attributes:\n      label: Reverse Proxy\n      description: Are you using a reverse proxy, if so which and what version?\n      placeholder: ex. nginx 1.29.0, caddy 2.10.0, traefik 3.4.4, haproxy 3.2\n    validations:\n      required: true\n  #\n  - id: os\n    type: dropdown\n    attributes:\n      label: Host/Server Operating System\n      description: On what operating system are you running the Vaultwarden server?\n      multiple: false\n      options:\n        - Linux\n        - NAS/SAN\n        - Cloud\n        - Windows\n        - macOS\n        - Other\n    validations:\n      required: true\n  #\n  - id: os-version\n    type: input\n    attributes:\n      label: Operating System Version\n      description: What version of the operating system(s) are you seeing the problem on?\n      placeholder: ex. Arch Linux, Ubuntu 24.04, Kubernetes, Synology DSM 7.x, Windows 11\n  #\n  - id: clients\n    type: dropdown\n    attributes:\n      label: Clients\n      description: What client(s) are you seeing the problem on?\n      multiple: true\n      options:\n        - Web Vault\n        - Browser Extension\n        - CLI\n        - Desktop\n        - Android\n        - iOS\n    validations:\n      required: true\n  #\n  - id: client-version\n    type: input\n    attributes:\n      label: Client Version\n      description: What version(s) of the client(s) are you seeing the problem on?\n      placeholder: ex. CLI v2025.7.0, Firefox 140 - v2025.6.1\n  #\n  - id: reproduce\n    type: textarea\n    attributes:\n      label: Steps To Reproduce\n      description: How can we reproduce the behavior.\n      value: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. Click on '...'\n        5. Etc '...'\n    validations:\n      required: true\n  #\n  - id: expected\n    type: textarea\n    attributes:\n      label: Expected Result\n      description: A clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  #\n  - id: actual\n    type: textarea\n    attributes:\n      label: Actual Result\n      description: A clear and concise description of what is happening.\n    validations:\n      required: true\n  #\n  - id: logs\n    type: textarea\n    attributes:\n      label: Logs\n      description: Provide the logs generated by Vaultwarden during the time this issue occurs.\n      render: text\n  #\n  - id: screenshots\n    type: textarea\n    attributes:\n      label: Screenshots or Videos\n      description: If applicable, add screenshots and/or a short video to help explain your problem.\n  #\n  - id: additional-context\n    type: textarea\n    attributes:\n      label: Additional Context\n      description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: GitHub Discussions for Vaultwarden\n    url: https://github.com/dani-garcia/vaultwarden/discussions\n    about: Use the discussions to request features or get help with usage/configuration.\n  - name: Discourse forum for Vaultwarden\n    url: https://vaultwarden.discourse.group/\n    about: An alternative to the GitHub Discussions, if this is easier for you.\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    paths:\n      - \".github/workflows/build.yml\"\n      - \"src/**\"\n      - \"migrations/**\"\n      - \"Cargo.*\"\n      - \"build.rs\"\n      - \"rust-toolchain.toml\"\n      - \"rustfmt.toml\"\n      - \"diesel.toml\"\n      - \"docker/Dockerfile.j2\"\n      - \"docker/DockerSettings.yaml\"\n      - \"macros/**\"\n\n  pull_request:\n    paths:\n      - \".github/workflows/build.yml\"\n      - \"src/**\"\n      - \"migrations/**\"\n      - \"Cargo.*\"\n      - \"build.rs\"\n      - \"rust-toolchain.toml\"\n      - \"rustfmt.toml\"\n      - \"diesel.toml\"\n      - \"docker/Dockerfile.j2\"\n      - \"docker/DockerSettings.yaml\"\n      - \"macros/**\"\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  build:\n    name: Build and Test ${{ matrix.channel }}\n    runs-on: ubuntu-24.04\n    timeout-minutes: 120\n    # Make warnings errors, this is to prevent warnings slipping through.\n    # This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.\n    env:\n      RUSTFLAGS: \"-Dwarnings\"\n    strategy:\n      fail-fast: false\n      matrix:\n        channel:\n          - \"rust-toolchain\" # The version defined in rust-toolchain\n          - \"msrv\" # The supported MSRV\n\n    steps:\n      # Install dependencies\n      - name: \"Install dependencies Ubuntu\"\n        run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config\n      # End Install dependencies\n\n      # Checkout the repo\n      - name: \"Checkout\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n          fetch-depth: 0\n      # End Checkout the repo\n\n      # Determine rust-toolchain version\n      - name: Init Variables\n        id: toolchain\n        env:\n          CHANNEL: ${{ matrix.channel }}\n        run: |\n          if [[ \"${CHANNEL}\" == 'rust-toolchain' ]]; then\n            RUST_TOOLCHAIN=\"$(grep -m1 -oP 'channel.*\"(\\K.*?)(?=\")' rust-toolchain.toml)\"\n          elif [[ \"${CHANNEL}\" == 'msrv' ]]; then\n            RUST_TOOLCHAIN=\"$(grep -m1 -oP 'rust-version\\s.*\"(\\K.*?)(?=\")' Cargo.toml)\"\n          else\n            RUST_TOOLCHAIN=\"${CHANNEL}\"\n          fi\n          echo \"RUST_TOOLCHAIN=${RUST_TOOLCHAIN}\" | tee -a \"${GITHUB_OUTPUT}\"\n      # End Determine rust-toolchain version\n\n\n      # Only install the clippy and rustfmt components on the default rust-toolchain\n      - name: \"Install rust-toolchain version\"\n        uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1\n        if: ${{ matrix.channel == 'rust-toolchain' }}\n        with:\n          toolchain: \"${{steps.toolchain.outputs.RUST_TOOLCHAIN}}\"\n          components: clippy, rustfmt\n      # End Uses the rust-toolchain file to determine version\n\n\n      # Install the any other channel to be used for which we do not execute clippy and rustfmt\n      - name: \"Install MSRV version\"\n        uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master @ Feb 13, 2026, 3:46 AM GMT+1\n        if: ${{ matrix.channel != 'rust-toolchain' }}\n        with:\n          toolchain: \"${{steps.toolchain.outputs.RUST_TOOLCHAIN}}\"\n      # End Install the MSRV channel to be used\n\n      # Set the current matrix toolchain version as default\n      - name: \"Set toolchain ${{steps.toolchain.outputs.RUST_TOOLCHAIN}} as default\"\n        env:\n          RUST_TOOLCHAIN: ${{steps.toolchain.outputs.RUST_TOOLCHAIN}}\n        run: |\n          # Remove the rust-toolchain.toml\n          rm rust-toolchain.toml\n          # Set the default\n          rustup default \"${RUST_TOOLCHAIN}\"\n\n      # Show environment\n      - name: \"Show environment\"\n        run: |\n          rustc -vV\n          cargo -vV\n      # End Show environment\n\n      # Enable Rust Caching\n      - name: Rust Caching\n        uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2\n        with:\n          # Use a custom prefix-key to force a fresh start. This is sometimes needed with bigger changes.\n          # Like changing the build host from Ubuntu 20.04 to 22.04 for example.\n          # Only update when really needed! Use a <year>.<month>[.<inc>] format.\n          prefix-key: \"v2025.09-rust\"\n      # End Enable Rust Caching\n\n      # Run cargo tests\n      # First test all features together, afterwards test them separately.\n      - name: \"test features: sqlite,mysql,postgresql,enable_mimalloc,s3\"\n        id: test_sqlite_mysql_postgresql_mimalloc_s3\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3\n\n      - name: \"test features: sqlite,mysql,postgresql,enable_mimalloc\"\n        id: test_sqlite_mysql_postgresql_mimalloc\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc\n\n      - name: \"test features: sqlite,mysql,postgresql\"\n        id: test_sqlite_mysql_postgresql\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features sqlite,mysql,postgresql\n\n      - name: \"test features: sqlite\"\n        id: test_sqlite\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features sqlite\n\n      - name: \"test features: mysql\"\n        id: test_mysql\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features mysql\n\n      - name: \"test features: postgresql\"\n        id: test_postgresql\n        if: ${{ !cancelled() }}\n        run: |\n          cargo test --profile ci --features postgresql\n      # End Run cargo tests\n\n\n      # Run cargo clippy, and fail on warnings\n      - name: \"clippy features: sqlite,mysql,postgresql,enable_mimalloc,s3\"\n        id: clippy\n        if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}\n        run: |\n          cargo clippy --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3\n      # End Run cargo clippy\n\n\n      # Run cargo fmt (Only run on rust-toolchain defined version)\n      - name: \"check formatting\"\n        id: formatting\n        if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}\n        run: |\n          cargo fmt --all -- --check\n      # End Run cargo fmt\n\n\n      # Check for any previous failures, if there are stop, else continue.\n      # This is useful so all test/clippy/fmt actions are done, and they can all be addressed\n      - name: \"Some checks failed\"\n        if: ${{ failure() }}\n        env:\n          TEST_DB_M_S3: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_s3.outcome }}\n          TEST_DB_M: ${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }}\n          TEST_DB: ${{ steps.test_sqlite_mysql_postgresql.outcome }}\n          TEST_SQLITE: ${{ steps.test_sqlite.outcome }}\n          TEST_MYSQL: ${{ steps.test_mysql.outcome }}\n          TEST_POSTGRESQL: ${{ steps.test_postgresql.outcome }}\n          CLIPPY: ${{ steps.clippy.outcome }}\n          FMT: ${{ steps.formatting.outcome }}\n        run: |\n          echo \"### :x: Checks Failed!\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|Job|Status|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|---|------|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (sqlite,mysql,postgresql,enable_mimalloc,s3)|${TEST_DB_M_S3}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (sqlite,mysql,postgresql,enable_mimalloc)|${TEST_DB_M}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (sqlite,mysql,postgresql)|${TEST_DB}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (sqlite)|${TEST_SQLITE}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (mysql)|${TEST_MYSQL}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|test (postgresql)|${TEST_POSTGRESQL}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|clippy (sqlite,mysql,postgresql,enable_mimalloc,s3)|${CLIPPY}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"|fmt|${FMT}|\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"Please check the failed jobs and fix where needed.\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"\" >> \"${GITHUB_STEP_SUMMARY}\"\n          exit 1\n\n\n      # Check for any previous failures, if there are stop, else continue.\n      # This is useful so all test/clippy/fmt actions are done, and they can all be addressed\n      - name: \"All checks passed\"\n        if: ${{ success() }}\n        run: |\n          echo \"### :tada: Checks Passed!\" >> \"${GITHUB_STEP_SUMMARY}\"\n          echo \"\" >> \"${GITHUB_STEP_SUMMARY}\"\n"
  },
  {
    "path": ".github/workflows/check-templates.yml",
    "content": "name: Check templates\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non: [ push, pull_request ]\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  docker-templates:\n    name: Validate docker templates\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n\n    steps:\n      # Checkout the repo\n      - name: \"Checkout\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      # End Checkout the repo\n\n      - name: Run make to rebuild templates\n        working-directory: docker\n        run: make\n\n      - name: Check for unstaged changes\n        working-directory: docker\n        run: git diff --exit-code\n        continue-on-error: false\n"
  },
  {
    "path": ".github/workflows/hadolint.yml",
    "content": "name: Hadolint\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non: [ push, pull_request ]\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  hadolint:\n    name: Validate Dockerfile syntax\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n\n    steps:\n      # Start Docker Buildx\n      - name: Setup Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n        # https://github.com/moby/buildkit/issues/3969\n        # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills\n        with:\n          buildkitd-config-inline: |\n            [worker.oci]\n              max-parallelism = 2\n          driver-opts: |\n            network=host\n\n      # Download hadolint - https://github.com/hadolint/hadolint/releases\n      - name: Download hadolint\n        run: |\n          sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \\\n          sudo chmod +x /usr/local/bin/hadolint\n        env:\n          HADOLINT_VERSION: 2.14.0\n      # End Download hadolint\n      # Checkout the repo\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      # End Checkout the repo\n\n      # Test Dockerfiles with hadolint\n      - name: Run hadolint\n        run: hadolint docker/Dockerfile.{debian,alpine}\n      # End Test Dockerfiles with hadolint\n\n      # Test Dockerfiles with docker build checks\n      - name: Run docker build check\n        run: |\n          echo \"Checking docker/Dockerfile.debian\"\n          docker build --check . -f docker/Dockerfile.debian\n          echo \"Checking docker/Dockerfile.alpine\"\n          docker build --check . -f docker/Dockerfile.alpine\n      # End Test Dockerfiles with docker build checks\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\npermissions: {}\n\nconcurrency:\n  # Apply concurrency control only on the upstream repo\n  group: ${{ github.repository == 'dani-garcia/vaultwarden' && format('{0}-{1}', github.workflow, github.ref) || github.run_id }}\n  # Don't cancel other runs when creating a tag\n  cancel-in-progress: ${{ github.ref_type == 'branch' }}\n\non:\n  push:\n    branches:\n      - main\n\n    tags:\n      # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet\n      - '[1-2].[0-9]+.[0-9]+'\n\ndefaults:\n  run:\n    shell: bash\n\nenv:\n  # The *_REPO variables need to be configured as repository variables\n  # Append `/settings/variables/actions` to your repo url\n  # DOCKERHUB_REPO needs to be 'index.docker.io/<user>/<repo>'\n  # Check for Docker hub credentials in secrets\n  HAVE_DOCKERHUB_LOGIN: ${{ vars.DOCKERHUB_REPO != '' && secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}\n  # GHCR_REPO needs to be 'ghcr.io/<user>/<repo>'\n  # Check for Github credentials in secrets\n  HAVE_GHCR_LOGIN: ${{ vars.GHCR_REPO != '' && github.repository_owner != '' && secrets.GITHUB_TOKEN != '' }}\n  # QUAY_REPO needs to be 'quay.io/<user>/<repo>'\n  # Check for Quay.io credentials in secrets\n  HAVE_QUAY_LOGIN: ${{ vars.QUAY_REPO != '' && secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }}\n\njobs:\n  docker-build:\n    name: Build Vaultwarden containers\n    if: ${{ github.repository == 'dani-garcia/vaultwarden' }}\n    permissions:\n      packages: write # Needed to upload packages and artifacts\n      contents: read\n      attestations: write # Needed to generate an artifact attestation for a build\n      id-token: write # Needed to mint the OIDC token necessary to request a Sigstore signing certificate\n    runs-on: ${{ contains(matrix.arch, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}\n    timeout-minutes: 120\n    env:\n      SOURCE_COMMIT: ${{ github.sha }}\n      SOURCE_REPOSITORY_URL: \"https://github.com/${{ github.repository }}\"\n    strategy:\n      matrix:\n        arch: [\"amd64\", \"arm64\", \"arm/v7\", \"arm/v6\"]\n        base_image: [\"debian\",\"alpine\"]\n\n    steps:\n      - name: Initialize QEMU binfmt support\n        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0\n        with:\n          platforms: \"arm64,arm\"\n\n      # Start Docker Buildx\n      - name: Setup Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n        # https://github.com/moby/buildkit/issues/3969\n        # Also set max parallelism to 2, the default of 4 breaks GitHub Actions and causes OOMKills\n        with:\n          cache-binary: false\n          buildkitd-config-inline: |\n            [worker.oci]\n              max-parallelism = 2\n          driver-opts: |\n            network=host\n\n      # Checkout the repo\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        # We need fetch-depth of 0 so we also get all the tag metadata\n        with:\n          persist-credentials: false\n          fetch-depth: 0\n\n      # Normalize the architecture string for use in paths and cache keys\n      - name: Normalize architecture string\n        env:\n          MATRIX_ARCH: ${{ matrix.arch }}\n        run: |\n          # Replace slashes with nothing to create a safe string for paths/cache keys\n          NORMALIZED_ARCH=\"${MATRIX_ARCH//\\/}\"\n          echo \"NORMALIZED_ARCH=${NORMALIZED_ARCH}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Determine Source Version\n      - name: Determine Source Version\n        run: |\n          # Get the Source Version for this release\n          GIT_EXACT_TAG=\"$(git describe --tags --abbrev=0 --exact-match 2>/dev/null || true)\"\n          if [[ -n \"${GIT_EXACT_TAG}\" ]]; then\n              echo \"SOURCE_VERSION=${GIT_EXACT_TAG}\" | tee -a \"${GITHUB_ENV}\"\n          else\n              GIT_LAST_TAG=\"$(git describe --tags --abbrev=0)\"\n              echo \"SOURCE_VERSION=${GIT_LAST_TAG}-${SOURCE_COMMIT:0:8}\" | tee -a \"${GITHUB_ENV}\"\n          fi\n\n      # Login to Docker Hub\n      - name: Login to Docker Hub\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n        if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}\n\n      - name: Add registry for DockerHub\n        if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}\n        env:\n          DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${DOCKERHUB_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Login to GitHub Container Registry\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}\n\n      - name: Add registry for ghcr.io\n        if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}\n        env:\n          GHCR_REPO: ${{ vars.GHCR_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${GHCR_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Login to Quay.io\n      - name: Login to Quay.io\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          registry: quay.io\n          username: ${{ secrets.QUAY_USERNAME }}\n          password: ${{ secrets.QUAY_TOKEN }}\n        if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}\n\n      - name: Add registry for Quay.io\n        if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}\n        env:\n          QUAY_REPO: ${{ vars.QUAY_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${QUAY_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      - name: Configure build cache from/to\n        env:\n          GHCR_REPO: ${{ vars.GHCR_REPO }}\n          BASE_IMAGE: ${{ matrix.base_image }}\n          NORMALIZED_ARCH: ${{ env.NORMALIZED_ARCH }}\n        run: |\n          #\n          # Check if there is a GitHub Container Registry Login and use it for caching\n          if [[ -n \"${HAVE_GHCR_LOGIN}\" ]]; then\n            echo \"BAKE_CACHE_FROM=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH}\" | tee -a \"${GITHUB_ENV}\"\n            echo \"BAKE_CACHE_TO=type=registry,ref=${GHCR_REPO}-buildcache:${BASE_IMAGE}-${NORMALIZED_ARCH},compression=zstd,mode=max\" | tee -a \"${GITHUB_ENV}\"\n          else\n            echo \"BAKE_CACHE_FROM=\"\n            echo \"BAKE_CACHE_TO=\"\n          fi\n          #\n\n      - name: Generate tags\n        id: tags\n        env:\n          CONTAINER_REGISTRIES: \"${{ env.CONTAINER_REGISTRIES }}\"\n        run: |\n          # Convert comma-separated list to newline-separated set commands\n          TAGS=$(echo \"${CONTAINER_REGISTRIES}\" | tr ',' '\\n' | sed \"s|.*|*.tags=&|\")\n\n          # Output for use in next step\n          {\n            echo \"TAGS<<EOF\"\n            echo \"$TAGS\"\n            echo \"EOF\"\n          } >> \"$GITHUB_ENV\"\n\n      - name: Bake ${{ matrix.base_image }} containers\n        id: bake_vw\n        uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7.0.0\n        env:\n          BASE_TAGS: \"${{ steps.determine-version.outputs.BASE_TAGS }}\"\n          SOURCE_COMMIT: \"${{ env.SOURCE_COMMIT }}\"\n          SOURCE_VERSION: \"${{ env.SOURCE_VERSION }}\"\n          SOURCE_REPOSITORY_URL: \"${{ env.SOURCE_REPOSITORY_URL }}\"\n        with:\n          pull: true\n          source: .\n          files: docker/docker-bake.hcl\n          targets: \"${{ matrix.base_image }}-multi\"\n          set: |\n            *.cache-from=${{ env.BAKE_CACHE_FROM }}\n            *.cache-to=${{ env.BAKE_CACHE_TO }}\n            *.platform=linux/${{ matrix.arch }}\n            ${{ env.TAGS }}\n            *.output=type=local,dest=./output\n            *.output=type=image,push-by-digest=true,name-canonical=true,push=true\n\n      - name: Extract digest SHA\n        env:\n          BAKE_METADATA: ${{ steps.bake_vw.outputs.metadata }}\n          BASE_IMAGE: ${{ matrix.base_image }}\n        run: |\n          GET_DIGEST_SHA=\"$(jq -r --arg base \"$BASE_IMAGE\" '.[$base + \"-multi\"].\"containerimage.digest\"' <<< \"${BAKE_METADATA}\")\"\n          echo \"DIGEST_SHA=${GET_DIGEST_SHA}\" | tee -a \"${GITHUB_ENV}\"\n\n      - name: Export digest\n        env:\n          DIGEST_SHA: ${{ env.DIGEST_SHA }}\n          RUNNER_TEMP: ${{ runner.temp }}\n        run: |\n          mkdir -p \"${RUNNER_TEMP}\"/digests\n          digest=\"${DIGEST_SHA}\"\n          touch \"${RUNNER_TEMP}/digests/${digest#sha256:}\"\n\n      - name: Upload digest\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: digests-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}\n          path: ${{ runner.temp }}/digests/*\n          if-no-files-found: error\n          retention-days: 1\n\n      - name: Rename binaries to match target platform\n        env:\n          NORMALIZED_ARCH: ${{ env.NORMALIZED_ARCH }}\n        run: |\n          mv ./output/vaultwarden vaultwarden-\"${NORMALIZED_ARCH}\"\n\n      # Upload artifacts to Github Actions and Attest the binaries\n      - name: Attest binaries\n        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0\n        with:\n          subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }}\n\n      - name: Upload binaries as artifacts\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-${{ env.NORMALIZED_ARCH }}-${{ matrix.base_image }}\n          path: vaultwarden-${{ env.NORMALIZED_ARCH }}\n\n  merge-manifests:\n    name: Merge manifests\n    runs-on: ubuntu-latest\n    needs: docker-build\n    permissions:\n      packages: write # Needed to upload packages and artifacts\n      attestations: write # Needed to generate an artifact attestation for a build\n      id-token: write # Needed to mint the OIDC token necessary to request a Sigstore signing certificate\n    strategy:\n      matrix:\n        base_image: [\"debian\",\"alpine\"]\n\n    steps:\n      - name: Download digests\n        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0\n        with:\n          path: ${{ runner.temp }}/digests\n          pattern: digests-*-${{ matrix.base_image }}\n          merge-multiple: true\n\n      # Login to Docker Hub\n      - name: Login to Docker Hub\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n        if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}\n\n      - name: Add registry for DockerHub\n        if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' }}\n        env:\n          DOCKERHUB_REPO: ${{ vars.DOCKERHUB_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${DOCKERHUB_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Login to GitHub Container Registry\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}\n\n      - name: Add registry for ghcr.io\n        if: ${{ env.HAVE_GHCR_LOGIN == 'true' }}\n        env:\n          GHCR_REPO: ${{ vars.GHCR_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${GHCR_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Login to Quay.io\n      - name: Login to Quay.io\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          registry: quay.io\n          username: ${{ secrets.QUAY_USERNAME }}\n          password: ${{ secrets.QUAY_TOKEN }}\n        if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}\n\n      - name: Add registry for Quay.io\n        if: ${{ env.HAVE_QUAY_LOGIN == 'true' }}\n        env:\n          QUAY_REPO: ${{ vars.QUAY_REPO }}\n        run: |\n          echo \"CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}${QUAY_REPO}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Determine Base Tags\n      - name: Determine Base Tags\n        env:\n          BASE_IMAGE_TAG: \"${{ matrix.base_image != 'debian' && format('-{0}', matrix.base_image) || '' }}\"\n          REF_TYPE: ${{ github.ref_type }}\n        run: |\n          # Check which main tag we are going to build determined by ref_type\n          if [[ \"${REF_TYPE}\" == \"tag\" ]]; then\n            echo \"BASE_TAGS=latest${BASE_IMAGE_TAG},${GITHUB_REF#refs/*/}${BASE_IMAGE_TAG}${BASE_IMAGE_TAG//-/,}\" | tee -a \"${GITHUB_ENV}\"\n          elif [[ \"${REF_TYPE}\" == \"branch\" ]]; then\n            echo \"BASE_TAGS=testing${BASE_IMAGE_TAG}\" | tee -a \"${GITHUB_ENV}\"\n          fi\n\n      - name: Create manifest list, push it and extract digest SHA\n        working-directory: ${{ runner.temp }}/digests\n        env:\n          BASE_TAGS: \"${{ env.BASE_TAGS }}\"\n          CONTAINER_REGISTRIES: \"${{ env.CONTAINER_REGISTRIES }}\"\n        run: |\n          IFS=',' read -ra IMAGES <<< \"${CONTAINER_REGISTRIES}\"\n          IFS=',' read -ra TAGS <<< \"${BASE_TAGS}\"\n\n          TAG_ARGS=()\n          for img in \"${IMAGES[@]}\"; do\n            for tag in \"${TAGS[@]}\"; do\n              TAG_ARGS+=(\"-t\" \"${img}:${tag}\")\n            done\n          done\n\n          echo \"Creating manifest\"\n          if ! OUTPUT=$(docker buildx imagetools create \\\n                          \"${TAG_ARGS[@]}\" \\\n                          $(printf \"${IMAGES[0]}@sha256:%s \" *) 2>&1); then\n            echo \"Manifest creation failed\"\n            echo \"${OUTPUT}\"\n            exit 1\n          fi\n\n          echo \"Manifest created successfully\"\n          echo \"${OUTPUT}\"\n\n          # Extract digest SHA for subsequent steps\n          GET_DIGEST_SHA=\"$(echo \"${OUTPUT}\" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)\"\n          echo \"DIGEST_SHA=${GET_DIGEST_SHA}\" | tee -a \"${GITHUB_ENV}\"\n\n      # Attest container images\n      - name: Attest - docker.io - ${{ matrix.base_image }}\n        if: ${{ env.HAVE_DOCKERHUB_LOGIN == 'true' && env.DIGEST_SHA != ''}}\n        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0\n        with:\n          subject-name: ${{ vars.DOCKERHUB_REPO }}\n          subject-digest: ${{ env.DIGEST_SHA }}\n          push-to-registry: true\n\n      - name: Attest - ghcr.io - ${{ matrix.base_image }}\n        if: ${{ env.HAVE_GHCR_LOGIN == 'true' && env.DIGEST_SHA != ''}}\n        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0\n        with:\n          subject-name: ${{ vars.GHCR_REPO }}\n          subject-digest: ${{ env.DIGEST_SHA }}\n          push-to-registry: true\n\n      - name: Attest - quay.io - ${{ matrix.base_image }}\n        if: ${{ env.HAVE_QUAY_LOGIN == 'true' && env.DIGEST_SHA != ''}}\n        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0\n        with:\n          subject-name: ${{ vars.QUAY_REPO }}\n          subject-digest: ${{ env.DIGEST_SHA }}\n          push-to-registry: true\n"
  },
  {
    "path": ".github/workflows/releasecache-cleanup.yml",
    "content": "name: Cleanup\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}\n  cancel-in-progress: false\n\non:\n  workflow_dispatch:\n    inputs:\n      manual_trigger:\n        description: \"Manual trigger buildcache cleanup\"\n        required: false\n        default: \"\"\n\n  schedule:\n    - cron: '0 1 * * FRI'\n\njobs:\n  releasecache-cleanup:\n    name: Releasecache Cleanup\n    permissions:\n      packages: write # To be able to cleanup old caches\n    runs-on: ubuntu-24.04\n    continue-on-error: true\n    timeout-minutes: 30\n    steps:\n      - name: Delete vaultwarden-buildcache containers\n        uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0\n        with:\n          package-name: 'vaultwarden-buildcache'\n          package-type: 'container'\n          min-versions-to-keep: 0\n          delete-only-untagged-versions: 'false'\n"
  },
  {
    "path": ".github/workflows/trivy.yml",
    "content": "name: Trivy\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches:\n      - main\n\n    tags:\n      - '*'\n\n  pull_request:\n    branches:\n      - main\n\n  schedule:\n    - cron: '08 11 * * *'\n\njobs:\n  trivy-scan:\n    # Only run this in the upstream repo and not on forks\n    # When all forks run this at the same time, it is causing `Too Many Requests` issues\n    if: ${{ github.repository == 'dani-garcia/vaultwarden' }}\n    name: Trivy Scan\n    permissions:\n      security-events: write # To write the security report\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Run Trivy vulnerability scanner\n        uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2\n        env:\n          TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2\n          TRIVY_JAVA_DB_REPOSITORY: docker.io/aquasec/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1\n        with:\n          scan-type: repo\n          ignore-unfixed: true\n          format: sarif\n          output: trivy-results.sarif\n          severity: CRITICAL,HIGH\n\n      - name: Upload Trivy scan results to GitHub Security tab\n        uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n        with:\n          sarif_file: 'trivy-results.sarif'\n"
  },
  {
    "path": ".github/workflows/typos.yml",
    "content": "name: Code Spell Checking\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non: [ push, pull_request ]\n\njobs:\n  typos:\n    name: Run typos spell checking\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n\n    steps:\n      # Checkout the repo\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      # End Checkout the repo\n\n      # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too\n      - name: Spell Check Repo\n        uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0\n"
  },
  {
    "path": ".github/workflows/zizmor.yml",
    "content": "name: Security Analysis with zizmor\npermissions: {}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"**\"]\n\njobs:\n  zizmor:\n    name: Run zizmor\n    runs-on: ubuntu-latest\n    permissions:\n      security-events: write # To write the security report\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Run zizmor\n        uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0\n        with:\n          # intentionally not scanning the entire repository,\n          # since it contains integration tests.\n          inputs: ./.github/\n"
  },
  {
    "path": ".gitignore",
    "content": "# Local build artifacts\ntarget\n\n# Data folder\ndata\n\n# IDE files\n.vscode\n.idea\n*.iml\n\n# Environment file\n.env\n\n# Web vault\nweb-vault\n"
  },
  {
    "path": ".hadolint.yaml",
    "content": "ignored:\n  # To prevent issues and make clear some images only work on linux/amd64, we ignore this\n  - DL3029\n  # disable explicit version for apt install\n  - DL3008\n  # disable explicit version for apk install\n  - DL3018\n  # Ignore shellcheck info message\n  - SC1091\ntrustedRegistries:\n  - docker.io\n  - ghcr.io\n  - quay.io\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "---\nrepos:\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0\n    hooks:\n    - id: check-yaml\n    - id: check-json\n    - id: check-toml\n    - id: mixed-line-ending\n      args: [\"--fix=no\"]\n    - id: end-of-file-fixer\n      exclude: \"(.*js$|.*css$)\"\n    - id: check-case-conflict\n    - id: check-merge-conflict\n    - id: detect-private-key\n    - id: check-symlinks\n    - id: forbid-submodules\n-   repo: local\n    hooks:\n    - id: fmt\n      name: fmt\n      description: Format files with cargo fmt.\n      entry: cargo fmt\n      language: system\n      always_run: true\n      pass_filenames: false\n      args: [\"--\", \"--check\"]\n    - id: cargo-test\n      name: cargo test\n      description: Test the package for errors.\n      entry: cargo test\n      language: system\n      args: [\"--features\", \"sqlite,mysql,postgresql\", \"--\"]\n      types_or: [rust, file]\n      files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\\.rs$)\n      pass_filenames: false\n    - id: cargo-clippy\n      name: cargo clippy\n      description: Lint Rust sources\n      entry: cargo clippy\n      language: system\n      args: [\"--features\", \"sqlite,mysql,postgresql\", \"--\", \"-D\", \"warnings\"]\n      types_or: [rust, file]\n      files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\\.rs$)\n      pass_filenames: false\n    - id: check-docker-templates\n      name: check docker templates\n      description: Check if the Docker templates are updated\n      language: system\n      entry: sh\n      args:\n        - \"-c\"\n        - \"cd docker && make\"\n# When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too\n- repo: https://github.com/crate-ci/typos\n  rev: 631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0\n  hooks:\n    - id: typos\n"
  },
  {
    "path": ".typos.toml",
    "content": "[files]\nextend-exclude = [\n    \".git/\",\n    \"playwright/\",\n    \"*.js\", # Ignore all JavaScript files\n    \"!admin*.js\", # Except our own JavaScript files\n]\nignore-hidden = false\n\n[default]\nextend-ignore-re = [\n    # We use this in place of the reserved type identifier at some places\n    \"typ\",\n    # In SMTP it's called HELO, so ignore it\n    \"(?i)helo_name\",\n    \"Server name sent during.+HELO\",\n    # COSE Is short for CBOR Object Signing and Encryption, ignore these specific items\n    \"COSEKey\",\n    \"COSEAlgorithm\",\n    # Ignore this specific string as it's valid\n    \"Ensure they are valid OTPs\",\n    # This word is misspelled upstream\n    # https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86\n    # https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45\n    \"AuthRequestResponseRecieved\",\n]\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace.package]\nedition = \"2021\"\nrust-version = \"1.92.0\"\nlicense = \"AGPL-3.0-only\"\nrepository = \"https://github.com/dani-garcia/vaultwarden\"\npublish = false\n\n[workspace]\nmembers = [\"macros\"]\n\n[package]\nname = \"vaultwarden\"\nversion = \"1.0.0\"\nauthors = [\"Daniel García <dani-garcia@users.noreply.github.com>\"]\nreadme = \"README.md\"\nbuild = \"build.rs\"\nresolver = \"2\"\nrepository.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nlicense.workspace = true\npublish.workspace = true\n\n[features]\ndefault = [\n    # \"sqlite\",\n    # \"mysql\",\n    # \"postgresql\",\n]\n# Empty to keep compatibility, prefer to set USE_SYSLOG=true\nenable_syslog = []\nmysql = [\"diesel/mysql\", \"diesel_migrations/mysql\"]\npostgresql = [\"diesel/postgres\", \"diesel_migrations/postgres\"]\nsqlite = [\"diesel/sqlite\", \"diesel_migrations/sqlite\", \"dep:libsqlite3-sys\"]\n# Enable to use a vendored and statically linked openssl\nvendored_openssl = [\"openssl/vendored\"]\n# Enable MiMalloc memory allocator to replace the default malloc\n# This can improve performance for Alpine builds\nenable_mimalloc = [\"dep:mimalloc\"]\ns3 = [\"opendal/services-s3\", \"dep:aws-config\", \"dep:aws-credential-types\", \"dep:aws-smithy-runtime-api\", \"dep:anyhow\", \"dep:http\", \"dep:reqsign\"]\n\n# OIDC specific features\noidc-accept-rfc3339-timestamps = [\"openidconnect/accept-rfc3339-timestamps\"]\noidc-accept-string-booleans = [\"openidconnect/accept-string-booleans\"]\n\n# Enable unstable features, requires nightly\n# Currently only used to enable rusts official ip support\nunstable = []\n\n[target.\"cfg(unix)\".dependencies]\n# Logging\nsyslog = \"7.0.0\"\n\n[dependencies]\nmacros = { path = \"./macros\" }\n\n# Logging\nlog = \"0.4.29\"\nfern = { version = \"0.7.1\", features = [\"syslog-7\", \"reopen-1\"] }\ntracing = { version = \"0.1.44\", features = [\"log\"] } # Needed to have lettre and webauthn-rs trace logging to work\n\n# A `dotenv` implementation for Rust\ndotenvy = { version = \"0.15.7\", default-features = false }\n\n# Numerical libraries\nnum-traits = \"0.2.19\"\nnum-derive = \"0.4.2\"\nbigdecimal = \"0.4.10\"\n\n# Web framework\nrocket = { version = \"0.5.1\", features = [\"tls\", \"json\"], default-features = false }\nrocket_ws = { version =\"0.1.1\" }\n\n# WebSockets libraries\nrmpv = \"1.3.1\" # MessagePack library\n\n# Concurrent HashMap used for WebSocket messaging and favicons\ndashmap = \"6.1.0\"\n\n# Async futures\nfutures = \"0.3.32\"\ntokio = { version = \"1.50.0\", features = [\"rt-multi-thread\", \"fs\", \"io-util\", \"parking_lot\", \"time\", \"signal\", \"net\"] }\ntokio-util = { version = \"0.7.18\", features = [\"compat\"]}\n\n# A generic serialization/deserialization framework\nserde = { version = \"1.0.228\", features = [\"derive\"] }\nserde_json = \"1.0.149\"\n\n# A safe, extensible ORM and Query builder\n# Currently pinned diesel to v2.3.3 as newer version break MySQL/MariaDB compatibility\ndiesel = { version = \"2.3.6\", features = [\"chrono\", \"r2d2\", \"numeric\"] }\ndiesel_migrations = \"2.3.1\"\n\nderive_more = { version = \"2.1.1\", features = [\"from\", \"into\", \"as_ref\", \"deref\", \"display\"] }\ndiesel-derive-newtype = \"2.1.2\"\n\n# Bundled/Static SQLite\nlibsqlite3-sys = { version = \"0.35.0\", features = [\"bundled\"], optional = true }\n\n# Crypto-related libraries\nrand = \"0.10.0\"\nring = \"0.17.14\"\nsubtle = \"2.6.1\"\n\n# UUID generation\nuuid = { version = \"1.22.0\", features = [\"v4\"] }\n\n# Date and time libraries\nchrono = { version = \"0.4.44\", features = [\"clock\", \"serde\"], default-features = false }\nchrono-tz = \"0.10.4\"\ntime = \"0.3.47\"\n\n# Job scheduler\njob_scheduler_ng = \"2.4.0\"\n\n# Data encoding library Hex/Base32/Base64\ndata-encoding = \"2.10.0\"\n\n# JWT library\njsonwebtoken = { version = \"10.3.0\", features = [\"use_pem\", \"rust_crypto\"], default-features = false }\n\n# TOTP library\ntotp-lite = \"2.0.1\"\n\n# Yubico Library\nyubico = { package = \"yubico_ng\", version = \"0.14.1\", features = [\"online-tokio\"], default-features = false }\n\n# WebAuthn libraries\n# danger-allow-state-serialisation is needed to save the state in the db\n# danger-credential-internals is needed to support U2F to Webauthn migration\nwebauthn-rs = { version = \"0.5.4\", features = [\"danger-allow-state-serialisation\", \"danger-credential-internals\"] }\nwebauthn-rs-proto = \"0.5.4\"\nwebauthn-rs-core = \"0.5.4\"\n\n# Handling of URL's for WebAuthn and favicons\nurl = \"2.5.8\"\n\n# Email libraries\nlettre = { version = \"0.11.19\", features = [\"smtp-transport\", \"sendmail-transport\", \"builder\", \"serde\", \"hostname\", \"tracing\", \"tokio1-rustls\", \"ring\", \"rustls-native-certs\"], default-features = false }\npercent-encoding = \"2.3.2\" # URL encoding library used for URL's in the emails\nemail_address = \"0.2.9\"\n\n# HTML Template library\nhandlebars = { version = \"6.4.0\", features = [\"dir_source\"] }\n\n# HTTP client (Used for favicons, version check, DUO and HIBP API)\nreqwest = { version = \"0.12.28\", features = [\"rustls-tls\", \"rustls-tls-native-roots\", \"stream\", \"json\", \"deflate\", \"gzip\", \"brotli\", \"zstd\", \"socks\", \"cookies\", \"charset\", \"http2\", \"system-proxy\"], default-features = false}\nhickory-resolver = \"0.25.2\"\n\n# Favicon extraction libraries\nhtml5gum = \"0.8.3\"\nregex = { version = \"1.12.3\", features = [\"std\", \"perf\", \"unicode-perl\"], default-features = false }\ndata-url = \"0.3.2\"\nbytes = \"1.11.1\"\nsvg-hush = \"0.9.6\"\n\n# Cache function results (Used for version check and favicon fetching)\ncached = { version = \"0.56.0\", features = [\"async\"] }\n\n# Used for custom short lived cookie jar during favicon extraction\ncookie = \"0.18.1\"\ncookie_store = \"0.22.1\"\n\n# Used by U2F, JWT and PostgreSQL\nopenssl = \"0.10.75\"\n\n# CLI argument parsing\npico-args = \"0.5.0\"\n\n# Macro ident concatenation\npastey = \"0.2.1\"\ngovernor = \"0.10.4\"\n\n# OIDC for SSO\nopenidconnect = { version = \"4.0.1\", features = [\"reqwest\", \"rustls-tls\"] }\nmoka = { version = \"0.12.13\", features = [\"future\"] }\n\n# Check client versions for specific features.\nsemver = \"1.0.27\"\n\n# Allow overriding the default memory allocator\n# Mainly used for the musl builds, since the default musl malloc is very slow\nmimalloc = { version = \"0.1.48\", features = [\"secure\"], default-features = false, optional = true }\n\nwhich = \"8.0.1\"\n\n# Argon2 library with support for the PHC format\nargon2 = \"0.5.3\"\n\n# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN\nrpassword = \"7.4.0\"\n\n# Loading a dynamic CSS Stylesheet\ngrass_compiler = { version = \"0.13.4\", default-features = false }\n\n# File are accessed through Apache OpenDAL\nopendal = { version = \"0.55.0\", features = [\"services-fs\"], default-features = false }\n\n# For retrieving AWS credentials, including temporary SSO credentials\nanyhow = { version = \"1.0.102\", optional = true }\naws-config = { version = \"1.8.15\", features = [\"behavior-version-latest\", \"rt-tokio\", \"credentials-process\", \"sso\"], default-features = false, optional = true }\naws-credential-types = { version = \"1.2.14\", optional = true }\naws-smithy-runtime-api = { version = \"1.11.6\", optional = true }\nhttp = { version = \"1.4.0\", optional = true }\nreqsign = { version = \"0.16.5\", optional = true }\n\n# Strip debuginfo from the release builds\n# The debug symbols are to provide better panic traces\n# Also enable fat LTO and use 1 codegen unit for optimizations\n[profile.release]\nstrip = \"debuginfo\"\nlto = \"fat\"\ncodegen-units = 1\ndebug = false\n\n# Optimize for size\n[profile.release-micro]\ninherits = \"release\"\nstrip = \"symbols\"\nopt-level = \"z\"\npanic = \"abort\"\n\n# Profile for systems with low resources\n# It will use less resources during build\n[profile.release-low]\ninherits = \"release\"\nstrip = \"symbols\"\nlto = \"thin\"\ncodegen-units = 16\n\n# Used for profiling and debugging like valgrind or heaptrack\n# Inherits release to be sure all optimizations have been done\n[profile.dbg]\ninherits = \"release\"\nstrip = \"none\"\nsplit-debuginfo = \"off\"\ndebug = \"full\"\n\n# A little bit of a speedup for generic building\n[profile.dev]\nsplit-debuginfo = \"unpacked\"\ndebug = \"line-tables-only\"\n\n# Used for CI builds to improve compile time\n[profile.ci]\ninherits = \"dev\"\ndebug = false\ndebug-assertions = false\nstrip = \"symbols\"\npanic = \"abort\"\n\n# Always build argon2 using opt-level 3\n# This is a huge speed improvement during testing\n[profile.dev.package.argon2]\nopt-level = 3\n\n# Linting config\n# https://doc.rust-lang.org/rustc/lints/groups.html\n[workspace.lints.rust]\n# Forbid\nunsafe_code = \"forbid\"\nnon_ascii_idents = \"forbid\"\n\n# Deny\ndeprecated_in_future = \"deny\"\ndeprecated_safe = { level = \"deny\", priority = -1 }\nfuture_incompatible = { level = \"deny\", priority = -1 }\nkeyword_idents = { level = \"deny\", priority = -1 }\nlet_underscore = { level = \"deny\", priority = -1 }\nnonstandard_style = { level = \"deny\", priority = -1 }\nnoop_method_call = \"deny\"\nrefining_impl_trait = { level = \"deny\", priority = -1 }\nrust_2018_idioms = { level = \"deny\", priority = -1 }\nrust_2021_compatibility = { level = \"deny\", priority = -1 }\nrust_2024_compatibility = { level = \"deny\", priority = -1 }\nsingle_use_lifetimes = \"deny\"\ntrivial_casts = \"deny\"\ntrivial_numeric_casts = \"deny\"\nunused = { level = \"deny\", priority = -1 }\nunused_import_braces = \"deny\"\nunused_lifetimes = \"deny\"\nunused_qualifications = \"deny\"\nvariant_size_differences = \"deny\"\n# Allow the following lints since these cause issues with Rust v1.84.0 or newer\n# Building Vaultwarden with Rust v1.85.0 with edition 2024 also works without issues\nedition_2024_expr_fragment_specifier = \"allow\" # Once changed to Rust 2024 this should be removed and macro's should be validated again\nif_let_rescope = \"allow\"\ntail_expr_drop_order = \"allow\"\n\n# https://rust-lang.github.io/rust-clippy/stable/index.html\n[workspace.lints.clippy]\n# Warn\ndbg_macro = \"warn\"\ntodo = \"warn\"\n\n# Ignore/Allow\nresult_large_err = \"allow\"\n\n# Deny\nbranches_sharing_code = \"deny\"\ncase_sensitive_file_extension_comparisons = \"deny\"\ncast_lossless = \"deny\"\nclone_on_ref_ptr = \"deny\"\nequatable_if_let = \"deny\"\nexcessive_precision = \"deny\"\nfilter_map_next = \"deny\"\nfloat_cmp_const = \"deny\"\nimplicit_clone = \"deny\"\ninefficient_to_string = \"deny\"\niter_on_empty_collections = \"deny\"\niter_on_single_items = \"deny\"\nlinkedlist = \"deny\"\nmacro_use_imports = \"deny\"\nmanual_assert = \"deny\"\nmanual_instant_elapsed = \"deny\"\nmanual_string_new = \"deny\"\nmatch_wildcard_for_single_variants = \"deny\"\nmem_forget = \"deny\"\nneedless_borrow = \"deny\"\nneedless_collect = \"deny\"\nneedless_continue = \"deny\"\nneedless_lifetimes = \"deny\"\noption_option = \"deny\"\nredundant_clone = \"deny\"\nstring_add_assign = \"deny\"\nunnecessary_join = \"deny\"\nunnecessary_self_imports = \"deny\"\nunnested_or_patterns = \"deny\"\nunused_async = \"deny\"\nunused_self = \"deny\"\nuseless_let_if_seq = \"deny\"\nverbose_file_reads = \"deny\"\nzero_sized_map_values = \"deny\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "![Vaultwarden Logo](./resources/vaultwarden-logo-auto.svg)\n\nAn alternative server implementation of the Bitwarden Client API, written in Rust and compatible with [official Bitwarden clients](https://bitwarden.com/download/) [[disclaimer](#disclaimer)], perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.\n\n---\n\n[![GitHub Release](https://img.shields.io/github/release/dani-garcia/vaultwarden.svg?style=for-the-badge&logo=vaultwarden&color=005AA4)](https://github.com/dani-garcia/vaultwarden/releases/latest)\n[![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?style=for-the-badge&logo=github&logoColor=fff&color=005AA4&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2Fdani-garcia%2Fvaultwarden%2Fvaultwarden.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden)\n[![Docker Pulls](https://img.shields.io/docker/pulls/vaultwarden/server.svg?style=for-the-badge&logo=docker&logoColor=fff&color=005AA4&label=docker.io%20pulls)](https://hub.docker.com/r/vaultwarden/server)\n[![Quay.io](https://img.shields.io/badge/quay.io-download-005AA4?style=for-the-badge&logo=redhat&cacheSeconds=14400)](https://quay.io/repository/vaultwarden/server) <br>\n[![Contributors](https://img.shields.io/github/contributors-anon/dani-garcia/vaultwarden.svg?style=flat-square&logo=vaultwarden&color=005AA4)](https://github.com/dani-garcia/vaultwarden/graphs/contributors)\n[![Forks](https://img.shields.io/github/forks/dani-garcia/vaultwarden.svg?style=flat-square&logo=github&logoColor=fff&color=005AA4)](https://github.com/dani-garcia/vaultwarden/network/members)\n[![Stars](https://img.shields.io/github/stars/dani-garcia/vaultwarden.svg?style=flat-square&logo=github&logoColor=fff&color=005AA4)](https://github.com/dani-garcia/vaultwarden/stargazers)\n[![Issues Open](https://img.shields.io/github/issues/dani-garcia/vaultwarden.svg?style=flat-square&logo=github&logoColor=fff&color=005AA4&cacheSeconds=300)](https://github.com/dani-garcia/vaultwarden/issues)\n[![Issues Closed](https://img.shields.io/github/issues-closed/dani-garcia/vaultwarden.svg?style=flat-square&logo=github&logoColor=fff&color=005AA4&cacheSeconds=300)](https://github.com/dani-garcia/vaultwarden/issues?q=is%3Aissue+is%3Aclosed)\n[![AGPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/vaultwarden.svg?style=flat-square&logo=vaultwarden&color=944000&cacheSeconds=14400)](https://github.com/dani-garcia/vaultwarden/blob/main/LICENSE.txt) <br>\n[![Dependency Status](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fdeps.rs%2Frepo%2Fgithub%2Fdani-garcia%2Fvaultwarden%2Fstatus.svg&query=%2F*%5Blocal-name()%3D'svg'%5D%2F*%5Blocal-name()%3D'g'%5D%5B2%5D%2F*%5Blocal-name()%3D'text'%5D%5B4%5D&style=flat-square&logo=rust&label=dependencies&color=005AA4)](https://deps.rs/repo/github/dani-garcia/vaultwarden)\n[![GHA Release](https://img.shields.io/github/actions/workflow/status/dani-garcia/vaultwarden/release.yml?style=flat-square&logo=github&logoColor=fff&label=Release%20Workflow)](https://github.com/dani-garcia/vaultwarden/actions/workflows/release.yml)\n[![GHA Build](https://img.shields.io/github/actions/workflow/status/dani-garcia/vaultwarden/build.yml?style=flat-square&logo=github&logoColor=fff&label=Build%20Workflow)](https://github.com/dani-garcia/vaultwarden/actions/workflows/build.yml) <br>\n[![Matrix Chat](https://img.shields.io/matrix/vaultwarden:matrix.org.svg?style=flat-square&logo=matrix&logoColor=fff&color=953B00&cacheSeconds=14400)](https://matrix.to/#/#vaultwarden:matrix.org)\n[![GitHub Discussions](https://img.shields.io/github/discussions/dani-garcia/vaultwarden?style=flat-square&logo=github&logoColor=fff&color=953B00&cacheSeconds=300)](https://github.com/dani-garcia/vaultwarden/discussions)\n[![Discourse Discussions](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fvaultwarden.discourse.group%2F&style=flat-square&logo=discourse&color=953B00)](https://vaultwarden.discourse.group/)\n\n> [!IMPORTANT]\n> **When using this server, please report any bugs or suggestions directly to us (see [Get in touch](#get-in-touch)), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official Bitwarden support channels.**\n\n<br>\n\n## Features\n\nA nearly complete implementation of the Bitwarden Client API is provided, including:\n\n * [Personal Vault](https://bitwarden.com/help/managing-items/)\n * [Send](https://bitwarden.com/help/about-send/)\n * [Attachments](https://bitwarden.com/help/attachments/)\n * [Website icons](https://bitwarden.com/help/website-icons/)\n * [Personal API Key](https://bitwarden.com/help/personal-api-key/)\n * [Organizations](https://bitwarden.com/help/getting-started-organizations/)\n   - [Collections](https://bitwarden.com/help/about-collections/),\n     [Password Sharing](https://bitwarden.com/help/sharing/),\n     [Member Roles](https://bitwarden.com/help/user-types-access-control/),\n     [Groups](https://bitwarden.com/help/about-groups/),\n     [Event Logs](https://bitwarden.com/help/event-logs/),\n     [Admin Password Reset](https://bitwarden.com/help/admin-reset/),\n     [Directory Connector](https://bitwarden.com/help/directory-sync/),\n     [Policies](https://bitwarden.com/help/policies/)\n * [Multi/Two Factor Authentication](https://bitwarden.com/help/bitwarden-field-guide-two-step-login/)\n   - [Authenticator](https://bitwarden.com/help/setup-two-step-login-authenticator/),\n     [Email](https://bitwarden.com/help/setup-two-step-login-email/),\n     [FIDO2 WebAuthn](https://bitwarden.com/help/setup-two-step-login-fido/),\n     [YubiKey](https://bitwarden.com/help/setup-two-step-login-yubikey/),\n     [Duo](https://bitwarden.com/help/setup-two-step-login-duo/)\n * [Emergency Access](https://bitwarden.com/help/emergency-access/)\n * [Vaultwarden Admin Backend](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page)\n * [Modified Web Vault client](https://github.com/dani-garcia/bw_web_builds) (Bundled within our containers)\n\n<br>\n\n## Usage\n\n> [!IMPORTANT]\n> The web-vault requires the use a secure context for the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).\n> That means it will only work via `http://localhost:8000` (using the port from the example below) or if you [enable HTTPS](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS).\n\nThe recommended way to install and use Vaultwarden is via our container images which are published to [ghcr.io](https://github.com/dani-garcia/vaultwarden/pkgs/container/vaultwarden), [docker.io](https://hub.docker.com/r/vaultwarden/server) and [quay.io](https://quay.io/repository/vaultwarden/server).\nSee [which container image to use](https://github.com/dani-garcia/vaultwarden/wiki/Which-container-image-to-use) for an explanation of the provided tags.\n\nThere are also [community driven packages](https://github.com/dani-garcia/vaultwarden/wiki/Third-party-packages) which can be used, but those might be lagging behind the latest version or might deviate in the way Vaultwarden is configured, as described in our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).\n\nAlternatively, you can also [build Vaultwarden](https://github.com/dani-garcia/vaultwarden/wiki/Building-binary) yourself.\n\nWhile Vaultwarden is based upon the [Rocket web framework](https://rocket.rs) which has built-in support for TLS our recommendation would be that you setup a reverse proxy (see [proxy examples](https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples)).\n\n> [!TIP]\n>**For more detailed examples on how to install, use and configure Vaultwarden you can check our [Wiki](https://github.com/dani-garcia/vaultwarden/wiki).**\n\n### Docker/Podman CLI\n\nPull the container image and mount a volume from the host for persistent storage.<br>\nYou can replace `docker` with `podman` if you prefer to use podman.\n\n```shell\ndocker pull vaultwarden/server:latest\ndocker run --detach --name vaultwarden \\\n  --env DOMAIN=\"https://vw.domain.tld\" \\\n  --volume /vw-data/:/data/ \\\n  --restart unless-stopped \\\n  --publish 127.0.0.1:8000:80 \\\n  vaultwarden/server:latest\n```\n\nThis will preserve any persistent data under `/vw-data/`, you can adapt the path to whatever suits you.\n\n### Docker Compose\n\nTo use Docker compose you need to create a `compose.yaml` which will hold the configuration to run the Vaultwarden container.\n\n```yaml\nservices:\n  vaultwarden:\n    image: vaultwarden/server:latest\n    container_name: vaultwarden\n    restart: unless-stopped\n    environment:\n      DOMAIN: \"https://vw.domain.tld\"\n    volumes:\n      - ./vw-data/:/data/\n    ports:\n      - 127.0.0.1:8000:80\n```\n\n<br>\n\n## Get in touch\n\nHave a question, suggestion or need help? Join our community on [Matrix](https://matrix.to/#/#vaultwarden:matrix.org), [GitHub Discussions](https://github.com/dani-garcia/vaultwarden/discussions) or [Discourse Forums](https://vaultwarden.discourse.group/).\n\nEncountered a bug or crash? Please search our issue tracker and discussions to see if it's already been reported. If not, please [start a new discussion](https://github.com/dani-garcia/vaultwarden/discussions) or [create a new issue](https://github.com/dani-garcia/vaultwarden/issues/). Ensure you're using the latest version of Vaultwarden and there aren't any similar issues open or closed!\n\n<br>\n\n## Contributors\n\nThanks for your contribution to the project!\n\n[![Contributors Count](https://img.shields.io/github/contributors-anon/dani-garcia/vaultwarden?style=for-the-badge&logo=vaultwarden&color=005AA4)](https://github.com/dani-garcia/vaultwarden/graphs/contributors)<br>\n[![Contributors Avatars](https://contributors-img.web.app/image?repo=dani-garcia/vaultwarden)](https://github.com/dani-garcia/vaultwarden/graphs/contributors)\n\n<br>\n\n## Disclaimer\n\n**This project is not associated with [Bitwarden](https://bitwarden.com/) or Bitwarden, Inc.**\n\nHowever, one of the active maintainers for Vaultwarden is employed by Bitwarden and is allowed to contribute to the project on their own time. These contributions are independent of Bitwarden and are reviewed by other maintainers.\n\nThe maintainers work together to set the direction for the project, focusing on serving the self-hosting community, including individuals, families, and small organizations, while ensuring the project's sustainability.\n\n**Please note:** We cannot be held liable for any data loss that may occur while using Vaultwarden. This includes passwords, attachments, and other information handled by the application. We highly recommend performing regular backups of your files and database. However, should you experience data loss, we encourage you to contact us immediately.\n\n<br>\n\n## Bitwarden_RS\n\nThis project was known as Bitwarden_RS and has been renamed to separate itself from the official Bitwarden server in the hopes of avoiding confusion and trademark/branding issues.<br>\nPlease see [#1642 - v1.21.0 release and project rename to Vaultwarden](https://github.com/dani-garcia/vaultwarden/discussions/1642) for more explanation.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "Vaultwarden tries to prevent security issues but there could always slip something through.\nIf you believe you've found a security issue in our application, we encourage you to\nnotify us. We welcome working with you to resolve the issue promptly. Thanks in advance!\n\n# Disclosure Policy\n\n- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every\n  effort to quickly resolve the issue.\n- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a\n  third-party. We may publicly disclose the issue before resolving it, if appropriate.\n- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or\n  degradation of our service. Only interact with accounts you own or with explicit permission of the\n  account holder.\n\n# In-scope\n\n- Security issues in any current release of Vaultwarden. Source code is available at https://github.com/dani-garcia/vaultwarden. This includes the current `latest` release and `main / testing` release.\n\n# Exclusions\n\nThe following bug classes are out-of scope:\n\n- Bugs that are already reported on Vaultwarden's issue tracker (https://github.com/dani-garcia/vaultwarden/issues)\n- Bugs that are not part of Vaultwarden, like on the web-vault or mobile and desktop clients. These issues need to be reported in the respective project issue tracker at https://github.com/bitwarden to which we are not associated\n- Issues in an upstream software dependency (ex: Rust, or External Libraries) which are already reported to the upstream maintainer\n- Attacks requiring physical access to a user's device\n- Issues related to software or protocols not under Vaultwarden's control\n- Vulnerabilities in outdated versions of Vaultwarden\n- Missing security best practices that do not directly lead to a vulnerability (You may still report them as a normal issue)\n- Issues that do not have any impact on the general public\n\nWhile researching, we'd like to ask you to refrain from:\n\n- Denial of service\n- Spamming\n- Social engineering (including phishing) of Vaultwarden developers, contributors or users\n\nThank you for helping keep Vaultwarden and our users safe!\n\n# How to contact us\n\n- You can contact us on Matrix https://matrix.to/#/#vaultwarden:matrix.org (users: `@danig:matrix.org` and/or `@blackdex:matrix.org`)\n- You can send an ![security-contact](/.github/security-contact.gif) to report a security issue.<br>\n  If you want to send an encrypted email you can use the following GPG key: 13BB3A34C9E380258CE43D595CB150B31F6426BC<br>\n  It can be found on several public GPG key servers.<br>\n    * https://keys.openpgp.org/search?q=security%40vaultwarden.org\n    * https://keys.mailvelope.com/pks/lookup?op=get&search=security%40vaultwarden.org\n    * https://pgpkeys.eu/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index\n    * https://keyserver.ubuntu.com/pks/lookup?search=security%40vaultwarden.org&fingerprint=on&op=index\n"
  },
  {
    "path": "build.rs",
    "content": "use std::env;\nuse std::process::Command;\n\nfn main() {\n    // This allow using #[cfg(sqlite)] instead of #[cfg(feature = \"sqlite\")], which helps when trying to add them through macros\n    #[cfg(feature = \"sqlite\")]\n    println!(\"cargo:rustc-cfg=sqlite\");\n    #[cfg(feature = \"mysql\")]\n    println!(\"cargo:rustc-cfg=mysql\");\n    #[cfg(feature = \"postgresql\")]\n    println!(\"cargo:rustc-cfg=postgresql\");\n    #[cfg(feature = \"s3\")]\n    println!(\"cargo:rustc-cfg=s3\");\n\n    #[cfg(not(any(feature = \"sqlite\", feature = \"mysql\", feature = \"postgresql\")))]\n    compile_error!(\n        \"You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite\"\n    );\n\n    // Use check-cfg to let cargo know which cfg's we define,\n    // and avoid warnings when they are used in the code.\n    println!(\"cargo::rustc-check-cfg=cfg(sqlite)\");\n    println!(\"cargo::rustc-check-cfg=cfg(mysql)\");\n    println!(\"cargo::rustc-check-cfg=cfg(postgresql)\");\n    println!(\"cargo::rustc-check-cfg=cfg(s3)\");\n\n    // Rerun when these paths are changed.\n    // Someone could have checked-out a tag or specific commit, but no other files changed.\n    println!(\"cargo:rerun-if-changed=.git\");\n    println!(\"cargo:rerun-if-changed=.git/HEAD\");\n    println!(\"cargo:rerun-if-changed=.git/index\");\n    println!(\"cargo:rerun-if-changed=.git/refs/tags\");\n\n    // Support $BWRS_VERSION for legacy compatibility, but default to $VW_VERSION.\n    // If neither exist, read from git.\n    let maybe_vaultwarden_version =\n        env::var(\"VW_VERSION\").or_else(|_| env::var(\"BWRS_VERSION\")).or_else(|_| version_from_git_info());\n\n    if let Ok(version) = maybe_vaultwarden_version {\n        println!(\"cargo:rustc-env=VW_VERSION={version}\");\n        println!(\"cargo:rustc-env=CARGO_PKG_VERSION={version}\");\n    }\n}\n\nfn run(args: &[&str]) -> Result<String, std::io::Error> {\n    let out = Command::new(args[0]).args(&args[1..]).output()?;\n    if !out.status.success() {\n        use std::io::Error;\n        return Err(Error::other(\"Command not successful\"));\n    }\n    Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())\n}\n\n/// This method reads info from Git, namely tags, branch, and revision\n/// To access these values, use:\n///    - `env!(\"GIT_EXACT_TAG\")`\n///    - `env!(\"GIT_LAST_TAG\")`\n///    - `env!(\"GIT_BRANCH\")`\n///    - `env!(\"GIT_REV\")`\n///    - `env!(\"VW_VERSION\")`\nfn version_from_git_info() -> Result<String, std::io::Error> {\n    // The exact tag for the current commit, can be empty when\n    // the current commit doesn't have an associated tag\n    let exact_tag = run(&[\"git\", \"describe\", \"--abbrev=0\", \"--tags\", \"--exact-match\"]).ok();\n    if let Some(ref exact) = exact_tag {\n        println!(\"cargo:rustc-env=GIT_EXACT_TAG={exact}\");\n    }\n\n    // The last available tag, equal to exact_tag when\n    // the current commit is tagged\n    let last_tag = run(&[\"git\", \"describe\", \"--abbrev=0\", \"--tags\"])?;\n    println!(\"cargo:rustc-env=GIT_LAST_TAG={last_tag}\");\n\n    // The current branch name\n    let branch = run(&[\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"])?;\n    println!(\"cargo:rustc-env=GIT_BRANCH={branch}\");\n\n    // The current git commit hash\n    let rev = run(&[\"git\", \"rev-parse\", \"HEAD\"])?;\n    let rev_short = rev.get(..8).unwrap_or_default();\n    println!(\"cargo:rustc-env=GIT_REV={rev_short}\");\n\n    // Combined version\n    if let Some(exact) = exact_tag {\n        Ok(exact)\n    } else if &branch != \"main\" && &branch != \"master\" && &branch != \"HEAD\" {\n        Ok(format!(\"{last_tag}-{rev_short} ({branch})\"))\n    } else {\n        Ok(format!(\"{last_tag}-{rev_short}\"))\n    }\n}\n"
  },
  {
    "path": "diesel.toml",
    "content": "# For documentation on how to configure this file,\n# see diesel.rs/guides/configuring-diesel-cli\n\n[print_schema]\nfile = \"src/db/schema.rs\""
  },
  {
    "path": "docker/DockerSettings.yaml",
    "content": "---\nvault_version: \"v2026.2.0\"\nvault_image_digest: \"sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447\"\n# Cross Compile Docker Helper Scripts v1.9.0\n# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts\n# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags\nxx_image_digest: \"sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707\"\nrust_version: 1.94.0 # Rust version to be used\ndebian_version: trixie # Debian release name to be used\nalpine_version: \"3.23\" # Alpine version to be used\n# For which platforms/architectures will we try to build images\nplatforms: [\"linux/amd64\", \"linux/arm64\", \"linux/arm/v7\", \"linux/arm/v6\"]\n# Determine the build images per OS/Arch\nbuild_stage_image:\n  debian:\n    image: \"docker.io/library/rust:{{rust_version}}-slim-{{debian_version}}\"\n    platform: \"$BUILDPLATFORM\"\n  alpine:\n    image: \"build_${TARGETARCH}${TARGETVARIANT}\"\n    arch_image:\n      amd64: \"ghcr.io/blackdex/rust-musl:x86_64-musl-stable-{{rust_version}}\"\n      arm64: \"ghcr.io/blackdex/rust-musl:aarch64-musl-stable-{{rust_version}}\"\n      armv7: \"ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-{{rust_version}}\"\n      armv6: \"ghcr.io/blackdex/rust-musl:arm-musleabi-stable-{{rust_version}}\"\n# The final image which will be used to distribute the container images\nruntime_stage_image:\n  debian: \"docker.io/library/debian:{{debian_version}}-slim\"\n  alpine: \"docker.io/library/alpine:{{alpine_version}}\"\n"
  },
  {
    "path": "docker/Dockerfile.alpine",
    "content": "# syntax=docker/dockerfile:1\n# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform\n\n# This file was generated using a Jinja2 template.\n# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`\n# This will generate two Dockerfile's `Dockerfile.debian` and `Dockerfile.alpine`\n\n# Using multistage build:\n# \thttps://docs.docker.com/develop/develop-images/multistage-build/\n# \thttps://whitfin.io/speeding-up-rust-docker-builds/\n\n####################### VAULT BUILD IMAGE #######################\n# The web-vault digest specifies a particular web-vault build on Docker Hub.\n# Using the digest instead of the tag name provides better security,\n# as the digest of an image is immutable, whereas a tag name can later\n# be changed to point to a malicious image.\n#\n# To verify the current digest for a given tag name:\n# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,\n#   click the tag name to view the digest of the image it currently points to.\n# - From the command line:\n#     $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0\n#     $ docker image inspect --format \"{{.RepoDigests}}\" docker.io/vaultwarden/web-vault:v2026.2.0\n#     [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447]\n#\n# - Conversely, to get the tag name from the digest:\n#     $ docker image inspect --format \"{{.RepoTags}}\" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447\n#     [docker.io/vaultwarden/web-vault:v2026.2.0]\n#\nFROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault\n\n########################## ALPINE BUILD IMAGES ##########################\n## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64\n## And for Alpine we define all build images here, they will only be loaded when actually used\nFROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.94.0 AS build_amd64\nFROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.94.0 AS build_arm64\nFROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.94.0 AS build_armv7\nFROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.94.0 AS build_armv6\n\n########################## BUILD IMAGE ##########################\n# hadolint ignore=DL3006\nFROM --platform=$BUILDPLATFORM build_${TARGETARCH}${TARGETVARIANT} AS build\nARG TARGETARCH\nARG TARGETVARIANT\nARG TARGETPLATFORM\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Build time options to avoid dpkg warnings and help with reproducible builds.\nENV DEBIAN_FRONTEND=noninteractive \\\n    LANG=C.UTF-8 \\\n    TZ=UTC \\\n    TERM=xterm-256color \\\n    CARGO_HOME=\"/root/.cargo\" \\\n    USER=\"root\" \\\n    # Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16\n    # Debian Trixie uses libpq v17\n    PQ_LIB_DIR=\"/usr/local/musl/pq17/lib\"\n\n\n# Create CARGO_HOME folder and don't download rust docs\nRUN mkdir -pv \"${CARGO_HOME}\" && \\\n    rustup set profile minimal\n\n# Creates a dummy project used to grab dependencies\nRUN USER=root cargo new --bin /app\nWORKDIR /app\n\n# Environment variables for Cargo on Alpine based builds\nRUN echo \"export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}\" >> /env-cargo && \\\n    # Output the current contents of the file\n    cat /env-cargo\n\nRUN source /env-cargo && \\\n    rustup target add \"${CARGO_TARGET}\"\n\n# Copies over *only* your manifests and build files\nCOPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./\nCOPY ./macros ./macros\n\nARG CARGO_PROFILE=release\n\n# Configure the DB ARG as late as possible to not invalidate the cached layers above\n# Enable MiMalloc to improve performance on Alpine builds\nARG DB=sqlite,mysql,postgresql,enable_mimalloc\n\n# Builds your dependencies and removes the\n# dummy project, except the target folder\n# This folder contains the compiled dependencies\nRUN source /env-cargo && \\\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    find . -not -path \"./target*\" -delete\n\n# Copies the complete project\n# To avoid copying unneeded files, use .dockerignore\nCOPY . .\n\nARG VW_VERSION\n\n# Builds again, this time it will be the actual source files being build\nRUN source /env-cargo && \\\n    # Make sure that we actually build the project by updating the src/main.rs timestamp\n    # Also do this for build.rs to ensure the version is rechecked\n    touch build.rs src/main.rs && \\\n    # Create a symlink to the binary target folder to easy copy the binary in the final stage\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    if [[ \"${CARGO_PROFILE}\" == \"dev\" ]] ; then \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/debug\" /app/target/final ; \\\n    else \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/${CARGO_PROFILE}\" /app/target/final ; \\\n    fi\n\n\n######################## RUNTIME IMAGE  ########################\n# Create a new stage with a minimal image\n# because we already have a binary built\n#\n# To build these images you need to have qemu binfmt support.\n# See the following pages to help install these tools locally\n# Ubuntu/Debian: https://wiki.debian.org/QemuUserEmulation\n# Arch Linux: https://wiki.archlinux.org/title/QEMU#Chrooting_into_arm/arm64_environment_from_x86_64\n#\n# Or use a Docker image which modifies your host system to support this.\n# The GitHub Actions Workflow uses the same image as used below.\n# See: https://github.com/tonistiigi/binfmt\n# Usage: docker run --privileged --rm tonistiigi/binfmt --install arm64,arm\n# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'\n#\n# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742\nFROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.23\n\nENV ROCKET_PROFILE=\"release\" \\\n    ROCKET_ADDRESS=0.0.0.0 \\\n    ROCKET_PORT=80 \\\n    SSL_CERT_DIR=/etc/ssl/certs\n\n# Create data folder and Install needed libraries\nRUN mkdir /data && \\\n    apk --no-cache add \\\n        ca-certificates \\\n        curl \\\n        openssl \\\n        tzdata\n\nVOLUME /data\nEXPOSE 80\n\n# Copies the files from the context (Rocket.toml file and web-vault)\n# and the binary from the \"build\" stage to the current stage\nWORKDIR /\n\nCOPY docker/healthcheck.sh docker/start.sh /\n\nCOPY --from=vault /web-vault ./web-vault\nCOPY --from=build /app/target/final/vaultwarden .\n\nHEALTHCHECK --interval=60s --timeout=10s CMD [\"/healthcheck.sh\"]\n\nCMD [\"/start.sh\"]\n"
  },
  {
    "path": "docker/Dockerfile.debian",
    "content": "# syntax=docker/dockerfile:1\n# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform\n\n# This file was generated using a Jinja2 template.\n# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`\n# This will generate two Dockerfile's `Dockerfile.debian` and `Dockerfile.alpine`\n\n# Using multistage build:\n# \thttps://docs.docker.com/develop/develop-images/multistage-build/\n# \thttps://whitfin.io/speeding-up-rust-docker-builds/\n\n####################### VAULT BUILD IMAGE #######################\n# The web-vault digest specifies a particular web-vault build on Docker Hub.\n# Using the digest instead of the tag name provides better security,\n# as the digest of an image is immutable, whereas a tag name can later\n# be changed to point to a malicious image.\n#\n# To verify the current digest for a given tag name:\n# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,\n#   click the tag name to view the digest of the image it currently points to.\n# - From the command line:\n#     $ docker pull docker.io/vaultwarden/web-vault:v2026.2.0\n#     $ docker image inspect --format \"{{.RepoDigests}}\" docker.io/vaultwarden/web-vault:v2026.2.0\n#     [docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447]\n#\n# - Conversely, to get the tag name from the digest:\n#     $ docker image inspect --format \"{{.RepoTags}}\" docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447\n#     [docker.io/vaultwarden/web-vault:v2026.2.0]\n#\nFROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:37c8661fa59dcdfbd3baa8366b6e950ef292b15adfeff1f57812b075c1fd3447 AS vault\n\n########################## Cross Compile Docker Helper Scripts ##########################\n## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts\n## And these bash scripts do not have any significant difference if at all\nFROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx\n\n########################## BUILD IMAGE ##########################\n# hadolint ignore=DL3006\nFROM --platform=$BUILDPLATFORM docker.io/library/rust:1.94.0-slim-trixie AS build\nCOPY --from=xx / /\nARG TARGETARCH\nARG TARGETVARIANT\nARG TARGETPLATFORM\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Build time options to avoid dpkg warnings and help with reproducible builds.\nENV DEBIAN_FRONTEND=noninteractive \\\n    LANG=C.UTF-8 \\\n    TZ=UTC \\\n    TERM=xterm-256color \\\n    CARGO_HOME=\"/root/.cargo\" \\\n    USER=\"root\"\n# Install clang to get `xx-cargo` working\n# Install pkg-config to allow amd64 builds to find all libraries\n# Install git so build.rs can determine the correct version\n# Install the libc cross packages based upon the debian-arch\nRUN apt-get update && \\\n    apt-get install -y \\\n        --no-install-recommends \\\n        clang \\\n        pkg-config \\\n        git \\\n        \"libc6-$(xx-info debian-arch)-cross\" \\\n        \"libc6-dev-$(xx-info debian-arch)-cross\" \\\n        \"linux-libc-dev-$(xx-info debian-arch)-cross\" && \\\n    xx-apt-get install -y \\\n        --no-install-recommends \\\n        gcc \\\n        libpq-dev \\\n        libpq5 \\\n        libssl-dev \\\n        libmariadb-dev \\\n        zlib1g-dev && \\\n    # Run xx-cargo early, since it sometimes seems to break when run at a later stage\n    echo \"export CARGO_TARGET=$(xx-cargo --print-target-triple)\" >> /env-cargo\n\n# Create CARGO_HOME folder and don't download rust docs\nRUN mkdir -pv \"${CARGO_HOME}\" && \\\n    rustup set profile minimal\n\n# Creates a dummy project used to grab dependencies\nRUN USER=root cargo new --bin /app\nWORKDIR /app\n\n# Environment variables for Cargo on Debian based builds\nARG TARGET_PKG_CONFIG_PATH\n\nRUN source /env-cargo && \\\n    if xx-info is-cross ; then \\\n        # We can't use xx-cargo since that uses clang, which doesn't work for our libraries.\n        # Because of this we generate the needed environment variables here which we can load in the needed steps.\n        echo \"export CC_$(echo \"${CARGO_TARGET}\" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc\" >> /env-cargo && \\\n        echo \"export CARGO_TARGET_$(echo \"${CARGO_TARGET}\" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc\" >> /env-cargo && \\\n        echo \"export CROSS_COMPILE=1\" >> /env-cargo && \\\n        echo \"export PKG_CONFIG_ALLOW_CROSS=1\" >> /env-cargo && \\\n        # For some architectures `xx-info` returns a triple which doesn't matches the path on disk\n        # In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg\n        if [[ -n \"${TARGET_PKG_CONFIG_PATH}\" ]]; then \\\n            echo \"export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}\" >> /env-cargo ; \\\n        else \\\n            echo \"export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig\" >> /env-cargo ; \\\n        fi && \\\n        echo \"# End of env-cargo\" >> /env-cargo ; \\\n    fi && \\\n    # Output the current contents of the file\n    cat /env-cargo\n\nRUN source /env-cargo && \\\n    rustup target add \"${CARGO_TARGET}\"\n\n# Copies over *only* your manifests and build files\nCOPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./\nCOPY ./macros ./macros\n\nARG CARGO_PROFILE=release\n\n# Configure the DB ARG as late as possible to not invalidate the cached layers above\nARG DB=sqlite,mysql,postgresql\n\n# Builds your dependencies and removes the\n# dummy project, except the target folder\n# This folder contains the compiled dependencies\nRUN source /env-cargo && \\\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    find . -not -path \"./target*\" -delete\n\n# Copies the complete project\n# To avoid copying unneeded files, use .dockerignore\nCOPY . .\n\nARG VW_VERSION\n\n# Builds again, this time it will be the actual source files being build\nRUN source /env-cargo && \\\n    # Make sure that we actually build the project by updating the src/main.rs timestamp\n    # Also do this for build.rs to ensure the version is rechecked\n    touch build.rs src/main.rs && \\\n    # Create a symlink to the binary target folder to easy copy the binary in the final stage\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    if [[ \"${CARGO_PROFILE}\" == \"dev\" ]] ; then \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/debug\" /app/target/final ; \\\n    else \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/${CARGO_PROFILE}\" /app/target/final ; \\\n    fi\n\n\n######################## RUNTIME IMAGE  ########################\n# Create a new stage with a minimal image\n# because we already have a binary built\n#\n# To build these images you need to have qemu binfmt support.\n# See the following pages to help install these tools locally\n# Ubuntu/Debian: https://wiki.debian.org/QemuUserEmulation\n# Arch Linux: https://wiki.archlinux.org/title/QEMU#Chrooting_into_arm/arm64_environment_from_x86_64\n#\n# Or use a Docker image which modifies your host system to support this.\n# The GitHub Actions Workflow uses the same image as used below.\n# See: https://github.com/tonistiigi/binfmt\n# Usage: docker run --privileged --rm tonistiigi/binfmt --install arm64,arm\n# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'\n#\n# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742\nFROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim\n\nENV ROCKET_PROFILE=\"release\" \\\n    ROCKET_ADDRESS=0.0.0.0 \\\n    ROCKET_PORT=80 \\\n    DEBIAN_FRONTEND=noninteractive\n\n# Create data folder and Install needed libraries\nRUN mkdir /data && \\\n    apt-get update && apt-get install -y \\\n        --no-install-recommends \\\n        ca-certificates \\\n        curl \\\n        libmariadb3 \\\n        libpq5 \\\n        openssl && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n\nVOLUME /data\nEXPOSE 80\n\n# Copies the files from the context (Rocket.toml file and web-vault)\n# and the binary from the \"build\" stage to the current stage\nWORKDIR /\n\nCOPY docker/healthcheck.sh docker/start.sh /\n\nCOPY --from=vault /web-vault ./web-vault\nCOPY --from=build /app/target/final/vaultwarden .\n\nHEALTHCHECK --interval=60s --timeout=10s CMD [\"/healthcheck.sh\"]\n\nCMD [\"/start.sh\"]\n"
  },
  {
    "path": "docker/Dockerfile.j2",
    "content": "# syntax=docker/dockerfile:1\n# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform\n\n# This file was generated using a Jinja2 template.\n# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`\n# This will generate two Dockerfile's `Dockerfile.debian` and `Dockerfile.alpine`\n\n# Using multistage build:\n# \thttps://docs.docker.com/develop/develop-images/multistage-build/\n# \thttps://whitfin.io/speeding-up-rust-docker-builds/\n\n####################### VAULT BUILD IMAGE #######################\n# The web-vault digest specifies a particular web-vault build on Docker Hub.\n# Using the digest instead of the tag name provides better security,\n# as the digest of an image is immutable, whereas a tag name can later\n# be changed to point to a malicious image.\n#\n# To verify the current digest for a given tag name:\n# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,\n#   click the tag name to view the digest of the image it currently points to.\n# - From the command line:\n#     $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}\n#     $ docker image inspect --format \"{{ '{{' }}.RepoDigests}}\" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}\n#     [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}]\n#\n# - Conversely, to get the tag name from the digest:\n#     $ docker image inspect --format \"{{ '{{' }}.RepoTags}}\" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}\n#     [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}]\n#\nFROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault\n\n{% if base == \"debian\" %}\n########################## Cross Compile Docker Helper Scripts ##########################\n## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts\n## And these bash scripts do not have any significant difference if at all\nFROM --platform=linux/amd64 docker.io/tonistiigi/xx@{{ xx_image_digest }} AS xx\n{% elif base == \"alpine\" %}\n########################## ALPINE BUILD IMAGES ##########################\n## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64\n## And for Alpine we define all build images here, they will only be loaded when actually used\n{% for arch in build_stage_image[base].arch_image %}\nFROM --platform=$BUILDPLATFORM {{ build_stage_image[base].arch_image[arch] }} AS build_{{ arch }}\n{% endfor %}\n{% endif %}\n\n########################## BUILD IMAGE ##########################\n# hadolint ignore=DL3006\nFROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build\n{% if base == \"debian\" %}\nCOPY --from=xx / /\n{% endif %}\nARG TARGETARCH\nARG TARGETVARIANT\nARG TARGETPLATFORM\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Build time options to avoid dpkg warnings and help with reproducible builds.\nENV DEBIAN_FRONTEND=noninteractive \\\n    LANG=C.UTF-8 \\\n    TZ=UTC \\\n    TERM=xterm-256color \\\n    CARGO_HOME=\"/root/.cargo\" \\\n    USER=\"root\"\n{%- if base == \"alpine\" %} \\\n    # Use PostgreSQL v17 during Alpine/MUSL builds instead of the default v16\n    # Debian Trixie uses libpq v17\n    PQ_LIB_DIR=\"/usr/local/musl/pq17/lib\"\n{% endif %}\n\n{% if base == \"debian\" %}\n# Install clang to get `xx-cargo` working\n# Install pkg-config to allow amd64 builds to find all libraries\n# Install git so build.rs can determine the correct version\n# Install the libc cross packages based upon the debian-arch\nRUN apt-get update && \\\n    apt-get install -y \\\n        --no-install-recommends \\\n        clang \\\n        pkg-config \\\n        git \\\n        \"libc6-$(xx-info debian-arch)-cross\" \\\n        \"libc6-dev-$(xx-info debian-arch)-cross\" \\\n        \"linux-libc-dev-$(xx-info debian-arch)-cross\" && \\\n    xx-apt-get install -y \\\n        --no-install-recommends \\\n        gcc \\\n        libpq-dev \\\n        libpq5 \\\n        libssl-dev \\\n        libmariadb-dev \\\n        zlib1g-dev && \\\n    # Run xx-cargo early, since it sometimes seems to break when run at a later stage\n    echo \"export CARGO_TARGET=$(xx-cargo --print-target-triple)\" >> /env-cargo\n{% endif %}\n\n# Create CARGO_HOME folder and don't download rust docs\nRUN mkdir -pv \"${CARGO_HOME}\" && \\\n    rustup set profile minimal\n\n# Creates a dummy project used to grab dependencies\nRUN USER=root cargo new --bin /app\nWORKDIR /app\n\n{% if base == \"debian\" %}\n# Environment variables for Cargo on Debian based builds\nARG TARGET_PKG_CONFIG_PATH\n\nRUN source /env-cargo && \\\n    if xx-info is-cross ; then \\\n        # We can't use xx-cargo since that uses clang, which doesn't work for our libraries.\n        # Because of this we generate the needed environment variables here which we can load in the needed steps.\n        echo \"export CC_$(echo \"${CARGO_TARGET}\" | tr '[:upper:]' '[:lower:]' | tr - _)=/usr/bin/$(xx-info)-gcc\" >> /env-cargo && \\\n        echo \"export CARGO_TARGET_$(echo \"${CARGO_TARGET}\" | tr '[:lower:]' '[:upper:]' | tr - _)_LINKER=/usr/bin/$(xx-info)-gcc\" >> /env-cargo && \\\n        echo \"export CROSS_COMPILE=1\" >> /env-cargo && \\\n        echo \"export PKG_CONFIG_ALLOW_CROSS=1\" >> /env-cargo && \\\n        # For some architectures `xx-info` returns a triple which doesn't matches the path on disk\n        # In those cases you can override this by setting the `TARGET_PKG_CONFIG_PATH` build-arg\n        if [[ -n \"${TARGET_PKG_CONFIG_PATH}\" ]]; then \\\n            echo \"export TARGET_PKG_CONFIG_PATH=${TARGET_PKG_CONFIG_PATH}\" >> /env-cargo ; \\\n        else \\\n            echo \"export PKG_CONFIG_PATH=/usr/lib/$(xx-info)/pkgconfig\" >> /env-cargo ; \\\n        fi && \\\n        echo \"# End of env-cargo\" >> /env-cargo ; \\\n    fi && \\\n    # Output the current contents of the file\n    cat /env-cargo\n\n{% elif base == \"alpine\" %}\n# Environment variables for Cargo on Alpine based builds\nRUN echo \"export CARGO_TARGET=${RUST_MUSL_CROSS_TARGET}\" >> /env-cargo && \\\n    # Output the current contents of the file\n    cat /env-cargo\n\n{% endif %}\nRUN source /env-cargo && \\\n    rustup target add \"${CARGO_TARGET}\"\n\n# Copies over *only* your manifests and build files\nCOPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./\nCOPY ./macros ./macros\n\nARG CARGO_PROFILE=release\n\n# Configure the DB ARG as late as possible to not invalidate the cached layers above\n{% if base == \"debian\" %}\nARG DB=sqlite,mysql,postgresql\n{% elif base == \"alpine\" %}\n# Enable MiMalloc to improve performance on Alpine builds\nARG DB=sqlite,mysql,postgresql,enable_mimalloc\n{% endif %}\n\n# Builds your dependencies and removes the\n# dummy project, except the target folder\n# This folder contains the compiled dependencies\nRUN source /env-cargo && \\\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    find . -not -path \"./target*\" -delete\n\n# Copies the complete project\n# To avoid copying unneeded files, use .dockerignore\nCOPY . .\n\nARG VW_VERSION\n\n# Builds again, this time it will be the actual source files being build\nRUN source /env-cargo && \\\n    # Make sure that we actually build the project by updating the src/main.rs timestamp\n    # Also do this for build.rs to ensure the version is rechecked\n    touch build.rs src/main.rs && \\\n    # Create a symlink to the binary target folder to easy copy the binary in the final stage\n    cargo build --features ${DB} --profile \"${CARGO_PROFILE}\" --target=\"${CARGO_TARGET}\" && \\\n    if [[ \"${CARGO_PROFILE}\" == \"dev\" ]] ; then \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/debug\" /app/target/final ; \\\n    else \\\n        ln -vfsr \"/app/target/${CARGO_TARGET}/${CARGO_PROFILE}\" /app/target/final ; \\\n    fi\n\n\n######################## RUNTIME IMAGE  ########################\n# Create a new stage with a minimal image\n# because we already have a binary built\n#\n# To build these images you need to have qemu binfmt support.\n# See the following pages to help install these tools locally\n# Ubuntu/Debian: https://wiki.debian.org/QemuUserEmulation\n# Arch Linux: https://wiki.archlinux.org/title/QEMU#Chrooting_into_arm/arm64_environment_from_x86_64\n#\n# Or use a Docker image which modifies your host system to support this.\n# The GitHub Actions Workflow uses the same image as used below.\n# See: https://github.com/tonistiigi/binfmt\n# Usage: docker run --privileged --rm tonistiigi/binfmt --install arm64,arm\n# To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'\n#\n# We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742\nFROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }}\n\nENV ROCKET_PROFILE=\"release\" \\\n    ROCKET_ADDRESS=0.0.0.0 \\\n    ROCKET_PORT=80\n{%- if base == \"debian\" %} \\\n    DEBIAN_FRONTEND=noninteractive\n{% elif base == \"alpine\" %} \\\n    SSL_CERT_DIR=/etc/ssl/certs\n{% endif %}\n\n# Create data folder and Install needed libraries\nRUN mkdir /data && \\\n{% if base == \"debian\" %}\n    apt-get update && apt-get install -y \\\n        --no-install-recommends \\\n        ca-certificates \\\n        curl \\\n        libmariadb3 \\\n        libpq5 \\\n        openssl && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n{% elif base == \"alpine\" %}\n    apk --no-cache add \\\n        ca-certificates \\\n        curl \\\n        openssl \\\n        tzdata\n{% endif %}\n\nVOLUME /data\nEXPOSE 80\n\n# Copies the files from the context (Rocket.toml file and web-vault)\n# and the binary from the \"build\" stage to the current stage\nWORKDIR /\n\nCOPY docker/healthcheck.sh docker/start.sh /\n\nCOPY --from=vault /web-vault ./web-vault\nCOPY --from=build /app/target/final/vaultwarden .\n\nHEALTHCHECK --interval=60s --timeout=10s CMD [\"/healthcheck.sh\"]\n\nCMD [\"/start.sh\"]\n"
  },
  {
    "path": "docker/Makefile",
    "content": "all:\n\t./render_template Dockerfile.j2 '{\"base\": \"debian\"}' > Dockerfile.debian\n\t./render_template Dockerfile.j2 '{\"base\": \"alpine\"}' > Dockerfile.alpine\n.PHONY: all\n"
  },
  {
    "path": "docker/README.md",
    "content": "# Vaultwarden Container Building\n\nTo build and release new testing and stable releases of Vaultwarden we use `docker buildx bake`.<br>\nThis can be used locally by running the command yourself, but it is also used by GitHub Actions.\n\nThis makes it easier for us to test and maintain the different architectures we provide.<br>\nWe also just have two Dockerfile's one for Debian and one for Alpine based images.<br>\nWith just these two files we can build both Debian and Alpine images for the following platforms:\n - amd64 (linux/amd64)\n - arm64 (linux/arm64)\n - armv7 (linux/arm/v7)\n - armv6 (linux/arm/v6)\n\nSome unsupported platforms for Debian based images. These are not built and tested by default and are only provided to make it easier for users to build for these architectures.\n- 386     (linux/386)\n- ppc64le (linux/ppc64le)\n- s390x   (linux/s390x)\n\nTo build these containers you need to enable QEMU binfmt support to be able to run/emulate architectures which are different then your host.<br>\nThis ensures the container build process can run binaries from other architectures.<br>\n\n**NOTE**: Run all the examples below from the root of the repo.<br>\n\n\n## How to install QEMU binfmt support\n\nThis is different per host OS, but most support this in some way.<br>\n\n### Ubuntu/Debian\n```bash\napt install binfmt-support qemu-user-static\n```\n\n### Arch Linux (others based upon it)\n```bash\npacman -S qemu-user-static qemu-user-static-binfmt\n```\n\n### Fedora\n```bash\ndnf install qemu-user-static\n```\n\n### Others\nThere also is an option to use an other docker container to provide support for this.\n```bash\n# To install and activate\ndocker run --privileged --rm tonistiigi/binfmt --install arm64,arm\n# To uninstall\ndocker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*'\n```\n\n\n## Single architecture container building\n\nYou can build a container per supported architecture as long as you have QEMU binfmt support installed on your system.<br>\n\n```bash\n# Default bake triggers a Debian build using the hosts architecture\ndocker buildx bake --file docker/docker-bake.hcl\n\n# Bake Debian ARM64 using a debug build\nCARGO_PROFILE=dev \\\nSOURCE_COMMIT=\"$(git rev-parse HEAD)\" \\\ndocker buildx bake --file docker/docker-bake.hcl debian-arm64\n\n# Bake Alpine ARMv6 as a release build\nSOURCE_COMMIT=\"$(git rev-parse HEAD)\" \\\ndocker buildx bake --file docker/docker-bake.hcl alpine-armv6\n```\n\n\n## Local Multi Architecture container building\n\nStart the initialization, this only needs to be done once.\n\n```bash\n# Create and use a new buildx builder instance which connects to the host network\ndocker buildx create --name vaultwarden --use --driver-opt network=host\n\n# Validate it runs\ndocker buildx inspect --bootstrap\n\n# Create a local container registry directly reachable on the localhost\ndocker run -d --name registry --network host registry:2\n```\n\nAfter that is done, you should be able to build and push to the local registry.<br>\nUse the following command with the modified variables to bake the Alpine images.<br>\nReplace `alpine` with `debian` if you want to build the debian multi arch images.\n\n```bash\n# Start a buildx bake using a debug build\nCARGO_PROFILE=dev \\\nSOURCE_COMMIT=\"$(git rev-parse HEAD)\" \\\nCONTAINER_REGISTRIES=\"localhost:5000/vaultwarden/server\" \\\ndocker buildx bake --file docker/docker-bake.hcl alpine-multi\n```\n\n\n## Using the `bake.sh` script\n\nTo make it a bit more easier to trigger a build, there also is a `bake.sh` script.<br>\nThis script calls `docker buildx bake` with all the right parameters and also generates the `SOURCE_COMMIT` and `SOURCE_VERSION` variables.<br>\nThis script can be called from both the repo root or within the docker directory.\n\nSo, if you want to build a Multi Arch Alpine container pushing to your localhost registry you can run this from within the docker directory. (Just make sure you executed the initialization steps above first)\n```bash\nCONTAINER_REGISTRIES=\"localhost:5000/vaultwarden/server\" \\\n./bake.sh alpine-multi\n```\n\nOr if you want to just build a Debian container from the repo root, you can run this.\n```bash\ndocker/bake.sh\n```\n\nYou can append both `alpine` and `debian` with `-amd64`, `-arm64`, `-armv7` or `-armv6`, which will trigger a build for that specific platform.<br>\nThis will also append those values to the tag so you can see the built container when running `docker images`.\n\nYou can also append extra arguments after the target if you want. This can be useful for example to print what bake will use.\n```bash\ndocker/bake.sh alpine-all --print\n```\n\n### Testing baked images\n\nTo test these images you can run these images by using the correct tag and provide the platform.<br>\nFor example, after you have build an arm64 image via `./bake.sh debian-arm64` you can run:\n```bash\ndocker run --rm -it \\\n  -e DISABLE_ADMIN_TOKEN=true \\\n  -e I_REALLY_WANT_VOLATILE_STORAGE=true \\\n  -p8080:80 --platform=linux/arm64 \\\n  vaultwarden/server:testing-arm64\n```\n\n\n## Using the `podman-bake.sh` script\n\nTo also make building easier using podman, there is a `podman-bake.sh` script.<br>\nThis script calls `podman buildx build` with the needed parameters and the same as `bake.sh`, it will generate some variables automatically.<br>\nThis script can be called from both the repo root or within the docker directory.\n\n**NOTE:** Unlike the `bake.sh` script, this only supports a single `CONTAINER_REGISTRIES`, and a single `BASE_TAGS` value, no comma separated values. It also only supports building separate architectures, no Multi Arch containers.\n\nTo build an Alpine arm64 image with only sqlite support and mimalloc, run this:\n```bash\nDB=\"sqlite,enable_mimalloc\" \\\n./podman-bake.sh alpine-arm64\n```\n\nOr if you want to just build a Debian container from the repo root, you can run this.\n```bash\ndocker/podman-bake.sh\n```\n\nYou can append extra arguments after the target if you want. This can be useful for example to disable cache like this.\n```bash\n./podman-bake.sh alpine-arm64 --no-cache\n```\n\nFor the podman builds you can, just like the `bake.sh` script, also append the architecture to build for that specific platform.<br>\n\n### Testing podman built images\n\nThe command to start a podman built container is almost the same as for the docker/bake built containers. The images start with `localhost/`, so you need to prepend that.\n\n```bash\npodman run --rm -it \\\n  -e DISABLE_ADMIN_TOKEN=true \\\n  -e I_REALLY_WANT_VOLATILE_STORAGE=true \\\n  -p8080:80 --platform=linux/arm64 \\\n  localhost/vaultwarden/server:testing-arm64\n```\n\n\n## Variables supported\n| Variable              | default | description |\n| --------------------- | ------------------ | ----------- |\n| CARGO_PROFILE         | null               | Which cargo profile to use. `null` means what is defined in the Dockerfile                                         |\n| DB                    | null               | Which `features` to build. `null` means what is defined in the Dockerfile                                          |\n| SOURCE_REPOSITORY_URL | null               | The source repository form where this build is triggered                                                           |\n| SOURCE_COMMIT         | null               | The commit hash of the current commit for this build                                                               |\n| SOURCE_VERSION        | null               | The current exact tag of this commit, else the last tag and the first 8 chars of the source commit                 |\n| BASE_TAGS             | testing            | Tags to be used. Can be a comma separated value like \"latest,1.29.2\"                                               |\n| CONTAINER_REGISTRIES  | vaultwarden/server | Comma separated value of container registries. Like `ghcr.io/dani-garcia/vaultwarden,docker.io/vaultwarden/server` |\n| VW_VERSION            | null               | To override the `SOURCE_VERSION` value. This is also used by the `build.rs` code for example                       |\n"
  },
  {
    "path": "docker/bake.sh",
    "content": "#!/usr/bin/env bash\n\n# Determine the basedir of this script.\n# It should be located in the same directory as the docker-bake.hcl\n# This ensures you can run this script from both inside and outside of the docker directory\nBASEDIR=$(RL=$(readlink -n \"$0\"); SP=\"${RL:-$0}\"; dirname \"$(cd \"$(dirname \"${SP}\")\" || exit; pwd)/$(basename \"${SP}\")\")\n\n# Load build env's\nsource \"${BASEDIR}/bake_env.sh\"\n\n# Be verbose on what is being executed\nset -x\n\n# Make sure we set the context to `..` so it will go up one directory\ndocker buildx bake --progress plain --set \"*.context=${BASEDIR}/..\" -f \"${BASEDIR}/docker-bake.hcl\" \"$@\"\n"
  },
  {
    "path": "docker/bake_env.sh",
    "content": "#!/usr/bin/env bash\n\n# If SOURCE_COMMIT is provided via env skip this\nif [ -z \"${SOURCE_COMMIT+x}\" ]; then\n    SOURCE_COMMIT=\"$(git rev-parse HEAD)\"\nfi\n\n# If VW_VERSION is provided via env use it as SOURCE_VERSION\n# Else define it using git\nif [[ -n \"${VW_VERSION}\" ]]; then\n    SOURCE_VERSION=\"${VW_VERSION}\"\nelse\n    GIT_EXACT_TAG=\"$(git describe --tags --abbrev=0 --exact-match 2>/dev/null)\"\n    if [[ -n \"${GIT_EXACT_TAG}\" ]]; then\n        SOURCE_VERSION=\"${GIT_EXACT_TAG}\"\n    else\n        GIT_LAST_TAG=\"$(git describe --tags --abbrev=0)\"\n        SOURCE_VERSION=\"${GIT_LAST_TAG}-${SOURCE_COMMIT:0:8}\"\n        GIT_BRANCH=\"$(git rev-parse --abbrev-ref HEAD)\"\n        case \"${GIT_BRANCH}\" in\n            main|master|HEAD)\n                # Do not add the branch name for these branches\n                ;;\n            *)\n                SOURCE_VERSION=\"${SOURCE_VERSION} (${GIT_BRANCH})\"\n                ;;\n        esac\n    fi\nfi\n\n# Export the rendered variables above so bake will use them\nexport SOURCE_COMMIT\nexport SOURCE_VERSION\n"
  },
  {
    "path": "docker/docker-bake.hcl",
    "content": "// ==== Baking Variables ====\n\n// Set which cargo profile to use, dev or release for example\n// Use the value provided in the Dockerfile as default\nvariable \"CARGO_PROFILE\" {\n  default = null\n}\n\n// Set which DB's (features) to enable\n// Use the value provided in the Dockerfile as default\nvariable \"DB\" {\n  default = null\n}\n\n// The repository this build was triggered from\nvariable \"SOURCE_REPOSITORY_URL\" {\n  default = null\n}\n\n// The commit hash of the current commit this build was triggered on\nvariable \"SOURCE_COMMIT\" {\n  default = null\n}\n\n// The version of this build\n// Typically the current exact tag of this commit,\n// else the last tag and the first 8 characters of the source commit\nvariable \"SOURCE_VERSION\" {\n  default = null\n}\n\n// This can be used to overwrite SOURCE_VERSION\n// It will be used during the build.rs building stage\nvariable \"VW_VERSION\" {\n  default = null\n}\n\n// The base tag(s) to use\n// This can be a comma separated value like \"testing,1.29.2\"\nvariable \"BASE_TAGS\" {\n  default = \"testing\"\n}\n\n// Which container registries should be used for the tagging\n// This can be a comma separated value\n// Use a full URI like `ghcr.io/dani-garcia/vaultwarden,docker.io/vaultwarden/server`\nvariable \"CONTAINER_REGISTRIES\" {\n  default = \"vaultwarden/server\"\n}\n\n\n// ==== Baking Groups ====\n\ngroup \"default\" {\n  targets = [\"debian\"]\n}\n\n\n// ==== Shared Baking ====\nfunction \"labels\" {\n  params = []\n  result = {\n    \"org.opencontainers.image.description\" = \"Unofficial Bitwarden compatible server written in Rust - ${SOURCE_VERSION}\"\n    \"org.opencontainers.image.licenses\" = \"AGPL-3.0-only\"\n    \"org.opencontainers.image.documentation\" = \"https://github.com/dani-garcia/vaultwarden/wiki\"\n    \"org.opencontainers.image.url\" = \"https://github.com/dani-garcia/vaultwarden\"\n    \"org.opencontainers.image.created\" =  \"${formatdate(\"YYYY-MM-DD'T'hh:mm:ssZZZZZ\", timestamp())}\"\n    \"org.opencontainers.image.source\" = \"${SOURCE_REPOSITORY_URL}\"\n    \"org.opencontainers.image.revision\" = \"${SOURCE_COMMIT}\"\n    \"org.opencontainers.image.version\" = \"${SOURCE_VERSION}\"\n  }\n}\n\ntarget \"_default_attributes\" {\n  labels = labels()\n  args = {\n    DB = \"${DB}\"\n    CARGO_PROFILE = \"${CARGO_PROFILE}\"\n    VW_VERSION = \"${VW_VERSION}\"\n  }\n}\n\n\n// ==== Debian Baking ====\n\n// Default Debian target, will build a container using the hosts platform architecture\ntarget \"debian\" {\n  inherits = [\"_default_attributes\"]\n  dockerfile = \"docker/Dockerfile.debian\"\n  tags = generate_tags(\"\", platform_tag())\n  output = [\"type=docker\"]\n}\n\n// Multi Platform target, will build one tagged manifest with all supported architectures\n// This is mainly used by GitHub Actions to build and push new containers\ntarget \"debian-multi\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/amd64\", \"linux/arm64\", \"linux/arm/v7\", \"linux/arm/v6\"]\n  tags = generate_tags(\"\", \"\")\n  output = [join(\",\", flatten([[\"type=registry\"], image_index_annotations()]))]\n}\n\n// Per platform targets, to individually test building per platform locally\ntarget \"debian-amd64\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/amd64\"]\n  tags = generate_tags(\"\", \"-amd64\")\n}\n\ntarget \"debian-arm64\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/arm64\"]\n  tags = generate_tags(\"\", \"-arm64\")\n}\n\ntarget \"debian-armv7\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/arm/v7\"]\n  tags = generate_tags(\"\", \"-armv7\")\n}\n\ntarget \"debian-armv6\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/arm/v6\"]\n  tags = generate_tags(\"\", \"-armv6\")\n}\n\n// ==== Start of unsupported Debian architecture targets ===\n// These are provided just to help users build for these rare platforms\n// They will not be built by default\ntarget \"debian-386\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/386\"]\n  tags = generate_tags(\"\", \"-386\")\n  args = {\n    TARGET_PKG_CONFIG_PATH = \"/usr/lib/i386-linux-gnu/pkgconfig\"\n  }\n}\n\ntarget \"debian-ppc64le\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/ppc64le\"]\n  tags = generate_tags(\"\", \"-ppc64le\")\n}\n\ntarget \"debian-s390x\" {\n  inherits = [\"debian\"]\n  platforms = [\"linux/s390x\"]\n  tags = generate_tags(\"\", \"-s390x\")\n}\n// ==== End of unsupported Debian architecture targets ===\n\n// A Group to build all platforms individually for local testing\ngroup \"debian-all\" {\n  targets = [\"debian-amd64\", \"debian-arm64\", \"debian-armv7\", \"debian-armv6\"]\n}\n\n\n// ==== Alpine Baking ====\n\n// Default Alpine target, will build a container using the hosts platform architecture\ntarget \"alpine\" {\n  inherits = [\"_default_attributes\"]\n  dockerfile = \"docker/Dockerfile.alpine\"\n  tags = generate_tags(\"-alpine\", platform_tag())\n  output = [\"type=docker\"]\n}\n\n// Multi Platform target, will build one tagged manifest with all supported architectures\n// This is mainly used by GitHub Actions to build and push new containers\ntarget \"alpine-multi\" {\n  inherits = [\"alpine\"]\n  platforms = [\"linux/amd64\", \"linux/arm64\", \"linux/arm/v7\", \"linux/arm/v6\"]\n  tags = generate_tags(\"-alpine\", \"\")\n  output = [join(\",\", flatten([[\"type=registry\"], image_index_annotations()]))]\n}\n\n// Per platform targets, to individually test building per platform locally\ntarget \"alpine-amd64\" {\n  inherits = [\"alpine\"]\n  platforms = [\"linux/amd64\"]\n  tags = generate_tags(\"-alpine\", \"-amd64\")\n}\n\ntarget \"alpine-arm64\" {\n  inherits = [\"alpine\"]\n  platforms = [\"linux/arm64\"]\n  tags = generate_tags(\"-alpine\", \"-arm64\")\n}\n\ntarget \"alpine-armv7\" {\n  inherits = [\"alpine\"]\n  platforms = [\"linux/arm/v7\"]\n  tags = generate_tags(\"-alpine\", \"-armv7\")\n}\n\ntarget \"alpine-armv6\" {\n  inherits = [\"alpine\"]\n  platforms = [\"linux/arm/v6\"]\n  tags = generate_tags(\"-alpine\", \"-armv6\")\n}\n\n// A Group to build all platforms individually for local testing\ngroup \"alpine-all\" {\n  targets = [\"alpine-amd64\", \"alpine-arm64\", \"alpine-armv7\", \"alpine-armv6\"]\n}\n\n\n// ==== Bake everything locally ====\n\ngroup \"all\" {\n  targets = [\"debian-all\", \"alpine-all\"]\n}\n\n\n// ==== Baking functions ====\n\n// This will return the local platform as amd64, arm64 or armv7 for example\n// It can be used for creating a local image tag\nfunction \"platform_tag\" {\n  params = []\n  result = \"-${replace(replace(BAKE_LOCAL_PLATFORM, \"linux/\", \"\"), \"/\", \"\")}\"\n}\n\n\nfunction \"get_container_registries\" {\n  params = []\n  result = flatten(split(\",\", CONTAINER_REGISTRIES))\n}\n\nfunction \"get_base_tags\" {\n  params = []\n  result = flatten(split(\",\", BASE_TAGS))\n}\n\nfunction \"generate_tags\" {\n  params = [\n    suffix,   // What to append to the BASE_TAG when needed, like `-alpine` for example\n    platform  // the platform we are building for if needed\n  ]\n  result = flatten([\n    for registry in get_container_registries() :\n      [for base_tag in get_base_tags() :\n        concat(\n          # If the base_tag contains latest, and the suffix contains `-alpine` add a `:alpine` tag too\n          base_tag == \"latest\" ? suffix == \"-alpine\" ? [\"${registry}:alpine${platform}\"] : [] : [],\n          # The default tagging strategy\n          [\"${registry}:${base_tag}${suffix}${platform}\"]\n        )\n      ]\n  ])\n}\n\nfunction \"image_index_annotations\" {\n  params = []\n  result = flatten([\n    for key, value in labels() :\n      value != null ? formatlist(\"annotation-index.%s=%s\", \"${key}\", \"${value}\") : []\n  ])\n}\n"
  },
  {
    "path": "docker/healthcheck.sh",
    "content": "#!/usr/bin/env sh\n\n# Use the value of the corresponding env var (if present),\n# or a default value otherwise.\n: \"${DATA_FOLDER:=\"/data\"}\"\n: \"${ROCKET_PORT:=\"80\"}\"\n: \"${ENV_FILE:=\"/.env\"}\"\n\nCONFIG_FILE=\"${DATA_FOLDER}\"/config.json\n\n# Check if the $ENV_FILE file exist and is readable\n# If that is the case, load it into the environment before running any check\nif [ -r \"${ENV_FILE}\" ]; then\n    # shellcheck disable=SC1090\n    . \"${ENV_FILE}\"\nfi\n\n# Given a config key, return the corresponding config value from the\n# config file. If the key doesn't exist, return an empty string.\nget_config_val() {\n    key=\"$1\"\n    # Extract a line of the form:\n    #   \"domain\": \"https://bw.example.com/path\",\n    grep \"\\\"${key}\\\":\" \"${CONFIG_FILE}\" |\n    # To extract just the value (https://bw.example.com/path), delete:\n    # (1) everything up to and including the first ':',\n    # (2) whitespace and '\"' from the front,\n    # (3) ',' and '\"' from the back.\n    sed -e 's/[^:]\\+://' -e 's/^[ \"]\\+//' -e 's/[,\"]\\+$//'\n}\n\n# Extract the base path from a domain URL. For example:\n# - `` -> ``\n# - `https://bw.example.com` -> ``\n# - `https://bw.example.com/` -> ``\n# - `https://bw.example.com/path` -> `/path`\n# - `https://bw.example.com/multi/path` -> `/multi/path`\nget_base_path() {\n    echo \"$1\" |\n    # Delete:\n    # (1) everything up to and including '://',\n    # (2) everything up to '/',\n    # (3) trailing '/' from the back.\n    sed -e 's|.*://||' -e 's|[^/]\\+||' -e 's|/*$||'\n}\n\n# Read domain URL from config.json, if present.\nif [ -r \"${CONFIG_FILE}\" ]; then\n    domain=\"$(get_config_val 'domain')\"\n    if [ -n \"${domain}\" ]; then\n        # config.json 'domain' overrides the DOMAIN env var.\n        DOMAIN=\"${domain}\"\n    fi\nfi\n\naddr=\"${ROCKET_ADDRESS}\"\nif [ -z \"${addr}\" ] || [ \"${addr}\" = '0.0.0.0' ] || [ \"${addr}\" = '::' ]; then\n    addr='localhost'\nfi\nbase_path=\"$(get_base_path \"${DOMAIN}\")\"\nif [ -n \"${ROCKET_TLS}\" ]; then\n    s='s'\nfi\ncurl --insecure --fail --silent --show-error \\\n     \"http${s}://${addr}:${ROCKET_PORT}${base_path}/alive\" || exit 1\n"
  },
  {
    "path": "docker/podman-bake.sh",
    "content": "#!/usr/bin/env bash\n\n# Determine the basedir of this script.\n# It should be located in the same directory as the docker-bake.hcl\n# This ensures you can run this script from both inside and outside of the docker directory\nBASEDIR=$(RL=$(readlink -n \"$0\"); SP=\"${RL:-$0}\"; dirname \"$(cd \"$(dirname \"${SP}\")\" || exit; pwd)/$(basename \"${SP}\")\")\n\n# Load build env's\nsource \"${BASEDIR}/bake_env.sh\"\n\n# Check if a target is given as first argument\n# If not we assume the defaults and pass the given arguments to the podman command\ncase \"${1}\" in\n    alpine*|debian*)\n        TARGET=\"${1}\"\n        # Now shift the $@ array so we only have the rest of the arguments\n        # This allows us too append these as extra arguments too the podman buildx build command\n        shift\n    ;;\nesac\n\nLABEL_ARGS=(\n    --label org.opencontainers.image.description=\"Unofficial Bitwarden compatible server written in Rust\"\n    --label org.opencontainers.image.licenses=\"AGPL-3.0-only\"\n    --label org.opencontainers.image.documentation=\"https://github.com/dani-garcia/vaultwarden/wiki\"\n    --label org.opencontainers.image.url=\"https://github.com/dani-garcia/vaultwarden\"\n    --label org.opencontainers.image.created=\"$(date --utc --iso-8601=seconds)\"\n)\nif [[ -n \"${SOURCE_REPOSITORY_URL}\" ]]; then\n    LABEL_ARGS+=(--label org.opencontainers.image.source=\"${SOURCE_REPOSITORY_URL}\")\nfi\nif [[ -n \"${SOURCE_COMMIT}\" ]]; then\n    LABEL_ARGS+=(--label org.opencontainers.image.revision=\"${SOURCE_COMMIT}\")\nfi\nif [[ -n \"${SOURCE_VERSION}\" ]]; then\n    LABEL_ARGS+=(--label org.opencontainers.image.version=\"${SOURCE_VERSION}\")\nfi\n\n# Check if and which --build-arg arguments we need to configure\nBUILD_ARGS=()\nif [[ -n \"${DB}\" ]]; then\n    BUILD_ARGS+=(--build-arg DB=\"${DB}\")\nfi\nif [[ -n \"${CARGO_PROFILE}\" ]]; then\n    BUILD_ARGS+=(--build-arg CARGO_PROFILE=\"${CARGO_PROFILE}\")\nfi\nif [[ -n \"${VW_VERSION}\" ]]; then\n    BUILD_ARGS+=(--build-arg VW_VERSION=\"${VW_VERSION}\")\nfi\n\n# Set the default BASE_TAGS if non are provided\nif [[ -z \"${BASE_TAGS}\" ]]; then\n    BASE_TAGS=\"testing\"\nfi\n\n# Set the default CONTAINER_REGISTRIES if non are provided\nif [[ -z \"${CONTAINER_REGISTRIES}\" ]]; then\n    CONTAINER_REGISTRIES=\"vaultwarden/server\"\nfi\n\n# Check which Dockerfile we need to use, default is debian\ncase \"${TARGET}\" in\n    alpine*)\n        BASE_TAGS=\"${BASE_TAGS}-alpine\"\n        DOCKERFILE=\"Dockerfile.alpine\"\n        ;;\n    *)\n        DOCKERFILE=\"Dockerfile.debian\"\n        ;;\nesac\n\n# Check which platform we need to build and append the BASE_TAGS with the architecture\ncase \"${TARGET}\" in\n    *-arm64)\n        BASE_TAGS=\"${BASE_TAGS}-arm64\"\n        PLATFORM=\"linux/arm64\"\n        ;;\n    *-armv7)\n        BASE_TAGS=\"${BASE_TAGS}-armv7\"\n        PLATFORM=\"linux/arm/v7\"\n        ;;\n    *-armv6)\n        BASE_TAGS=\"${BASE_TAGS}-armv6\"\n        PLATFORM=\"linux/arm/v6\"\n        ;;\n    *)\n        BASE_TAGS=\"${BASE_TAGS}-amd64\"\n        PLATFORM=\"linux/amd64\"\n        ;;\nesac\n\n# Be verbose on what is being executed\nset -x\n\n# Build the image with podman\n# We use the docker format here since we are using `SHELL`, which is not supported by OCI\n# shellcheck disable=SC2086\npodman buildx build \\\n  --platform=\"${PLATFORM}\" \\\n  --tag=\"${CONTAINER_REGISTRIES}:${BASE_TAGS}\" \\\n  --format=docker \\\n  \"${LABEL_ARGS[@]}\" \\\n  \"${BUILD_ARGS[@]}\" \\\n  --file=\"${BASEDIR}/${DOCKERFILE}\" \"$@\" \\\n  \"${BASEDIR}/..\"\n"
  },
  {
    "path": "docker/render_template",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport argparse\nimport json\nimport yaml\nimport jinja2\n\n# Load settings file\nwith open(\"DockerSettings.yaml\", 'r') as yaml_file:\n\tyaml_data = yaml.safe_load(yaml_file)\n\nsettings_env = jinja2.Environment(\n\tloader=jinja2.FileSystemLoader(os.getcwd()),\n)\nsettings_yaml = yaml.safe_load(settings_env.get_template(\"DockerSettings.yaml\").render(yaml_data))\n\nargs_parser = argparse.ArgumentParser()\nargs_parser.add_argument('template_file', help='Jinja2 template file to render.')\nargs_parser.add_argument('render_vars', help='JSON-encoded data to pass to the templating engine.')\ncli_args = args_parser.parse_args()\n\n# Merge the default config yaml with the json arguments given.\nrender_vars = json.loads(cli_args.render_vars)\nsettings_yaml.update(render_vars)\n\nenvironment = jinja2.Environment(\n\tloader=jinja2.FileSystemLoader(os.getcwd()),\n\ttrim_blocks=True,\n)\nprint(environment.get_template(cli_args.template_file).render(settings_yaml))\n"
  },
  {
    "path": "docker/start.sh",
    "content": "#!/bin/sh\n\nif [ -n \"${UMASK}\" ]; then\n    umask \"${UMASK}\"\nfi\n\nif [ -r /etc/vaultwarden.sh ]; then\n    . /etc/vaultwarden.sh\nelif [ -r /etc/bitwarden_rs.sh ]; then\n    echo \"### You are using the old /etc/bitwarden_rs.sh script, please migrate to /etc/vaultwarden.sh ###\"\n    . /etc/bitwarden_rs.sh\nfi\n\nif [ -d /etc/vaultwarden.d ]; then\n    for f in /etc/vaultwarden.d/*.sh; do\n        if [ -r \"${f}\" ]; then\n            . \"${f}\"\n        fi\n    done\nelif [ -d /etc/bitwarden_rs.d ]; then\n    echo \"### You are using the old /etc/bitwarden_rs.d script directory, please migrate to /etc/vaultwarden.d ###\"\n    for f in /etc/bitwarden_rs.d/*.sh; do\n        if [ -r \"${f}\" ]; then\n            . \"${f}\"\n        fi\n    done\nfi\n\nexec /vaultwarden \"${@}\"\n"
  },
  {
    "path": "macros/Cargo.toml",
    "content": "[package]\nname = \"macros\"\nversion = \"0.1.0\"\nrepository.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nlicense.workspace = true\npublish.workspace = true\n\n[lib]\nname = \"macros\"\npath = \"src/lib.rs\"\nproc-macro = true\n\n[dependencies]\nquote = \"1.0.45\"\nsyn = \"2.0.117\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "macros/src/lib.rs",
    "content": "use proc_macro::TokenStream;\nuse quote::quote;\n\n#[proc_macro_derive(UuidFromParam)]\npub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {\n    let ast = syn::parse(input).unwrap();\n\n    impl_derive_uuid_macro(&ast)\n}\n\nfn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {\n    let name = &ast.ident;\n    let gen_derive = quote! {\n        #[automatically_derived]\n        impl<'r> rocket::request::FromParam<'r> for #name {\n            type Error = ();\n\n            #[inline(always)]\n            fn from_param(param: &'r str) -> Result<Self, Self::Error> {\n                if uuid::Uuid::parse_str(param).is_ok() {\n                    Ok(Self(param.to_string()))\n                } else {\n                    Err(())\n                }\n            }\n        }\n    };\n    gen_derive.into()\n}\n\n#[proc_macro_derive(IdFromParam)]\npub fn derive_id_from_param(input: TokenStream) -> TokenStream {\n    let ast = syn::parse(input).unwrap();\n\n    impl_derive_safestring_macro(&ast)\n}\n\nfn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {\n    let name = &ast.ident;\n    let gen_derive = quote! {\n        #[automatically_derived]\n        impl<'r> rocket::request::FromParam<'r> for #name {\n            type Error = ();\n\n            #[inline(always)]\n            fn from_param(param: &'r str) -> Result<Self, Self::Error> {\n                if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {\n                    Ok(Self(param.to_string()))\n                } else {\n                    Err(())\n                }\n            }\n        }\n    };\n    gen_derive.into()\n}\n"
  },
  {
    "path": "migrations/mysql/2018-01-14-171611_create_tables/down.sql",
    "content": "DROP TABLE users;\n\nDROP TABLE devices;\n\nDROP TABLE ciphers;\n\nDROP TABLE attachments;\n\nDROP TABLE folders;"
  },
  {
    "path": "migrations/mysql/2018-01-14-171611_create_tables/up.sql",
    "content": "CREATE TABLE users (\n  uuid                CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL,\n  updated_at          DATETIME NOT NULL,\n  email               VARCHAR(255) NOT NULL UNIQUE,\n  name                TEXT     NOT NULL,\n  password_hash       BLOB     NOT NULL,\n  salt                BLOB     NOT NULL,\n  password_iterations INTEGER  NOT NULL,\n  password_hint       TEXT,\n  `key`               TEXT     NOT NULL,\n  private_key         TEXT,\n  public_key          TEXT,\n  totp_secret         TEXT,\n  totp_recover        TEXT,\n  security_stamp      TEXT     NOT NULL,\n  equivalent_domains  TEXT     NOT NULL,\n  excluded_globals    TEXT     NOT NULL\n);\n\nCREATE TABLE devices (\n  uuid          CHAR(36) NOT NULL PRIMARY KEY,\n  created_at    DATETIME NOT NULL,\n  updated_at    DATETIME NOT NULL,\n  user_uuid     CHAR(36) NOT NULL REFERENCES users (uuid),\n  name          TEXT     NOT NULL,\n  type          INTEGER  NOT NULL,\n  push_token    TEXT,\n  refresh_token TEXT     NOT NULL\n);\n\nCREATE TABLE ciphers (\n  uuid              CHAR(36) NOT NULL PRIMARY KEY,\n  created_at        DATETIME NOT NULL,\n  updated_at        DATETIME NOT NULL,\n  user_uuid         CHAR(36) NOT NULL REFERENCES users (uuid),\n  folder_uuid       CHAR(36) REFERENCES folders (uuid),\n  organization_uuid CHAR(36),\n  type              INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  favorite          BOOLEAN  NOT NULL\n);\n\nCREATE TABLE attachments (\n  id          CHAR(36) NOT NULL PRIMARY KEY,\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  file_name   TEXT    NOT NULL,\n  file_size   INTEGER NOT NULL\n\n);\n\nCREATE TABLE folders (\n  uuid       CHAR(36) NOT NULL PRIMARY KEY,\n  created_at DATETIME NOT NULL,\n  updated_at DATETIME NOT NULL,\n  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),\n  name       TEXT     NOT NULL\n);\n  \n"
  },
  {
    "path": "migrations/mysql/2018-02-17-205753_create_collections_and_orgs/down.sql",
    "content": "DROP TABLE collections;\n\nDROP TABLE organizations;\n\n\nDROP TABLE users_collections;\n\nDROP TABLE users_organizations;\n"
  },
  {
    "path": "migrations/mysql/2018-02-17-205753_create_collections_and_orgs/up.sql",
    "content": "CREATE TABLE collections (\n  uuid     VARCHAR(40) NOT NULL PRIMARY KEY,\n  org_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),\n  name     TEXT NOT NULL\n);\n\nCREATE TABLE organizations (\n  uuid          VARCHAR(40) NOT NULL PRIMARY KEY,\n  name          TEXT NOT NULL,\n  billing_email TEXT NOT NULL\n);\n\nCREATE TABLE users_collections (\n  user_uuid       CHAR(36) NOT NULL REFERENCES users (uuid),\n  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),\n  PRIMARY KEY (user_uuid, collection_uuid)\n);\n\nCREATE TABLE users_organizations (\n  uuid       CHAR(36) NOT NULL PRIMARY KEY,\n  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),\n  org_uuid   CHAR(36) NOT NULL REFERENCES organizations (uuid),\n\n  access_all BOOLEAN NOT NULL,\n  `key`      TEXT    NOT NULL,\n  status     INTEGER NOT NULL,\n  type       INTEGER NOT NULL,\n\n  UNIQUE (user_uuid, org_uuid)\n);\n"
  },
  {
    "path": "migrations/mysql/2018-04-27-155151_create_users_ciphers/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2018-04-27-155151_create_users_ciphers/up.sql",
    "content": "ALTER TABLE ciphers RENAME TO oldCiphers;\n\nCREATE TABLE ciphers (\n  uuid              CHAR(36) NOT NULL PRIMARY KEY,\n  created_at        DATETIME NOT NULL,\n  updated_at        DATETIME NOT NULL,\n  user_uuid         CHAR(36) REFERENCES users (uuid), -- Make this optional\n  organization_uuid CHAR(36) REFERENCES organizations (uuid), -- Add reference to orgs table\n  -- Remove folder_uuid\n  type              INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  favorite          BOOLEAN  NOT NULL\n);\n\nCREATE TABLE folders_ciphers (\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  folder_uuid CHAR(36) NOT NULL REFERENCES folders (uuid),\n\n  PRIMARY KEY (cipher_uuid, folder_uuid)\n);\n\nINSERT INTO ciphers (uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite) \nSELECT uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite FROM oldCiphers;\n\nINSERT INTO folders_ciphers (cipher_uuid, folder_uuid)\nSELECT uuid, folder_uuid FROM oldCiphers WHERE folder_uuid IS NOT NULL;\n\n\nDROP TABLE oldCiphers;\n\nALTER TABLE users_collections ADD COLUMN read_only BOOLEAN NOT NULL DEFAULT 0; -- False\n"
  },
  {
    "path": "migrations/mysql/2018-05-08-161616_create_collection_cipher_map/down.sql",
    "content": "DROP TABLE ciphers_collections;"
  },
  {
    "path": "migrations/mysql/2018-05-08-161616_create_collection_cipher_map/up.sql",
    "content": "CREATE TABLE ciphers_collections (\n  cipher_uuid       CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),\n  PRIMARY KEY (cipher_uuid, collection_uuid)\n);\n"
  },
  {
    "path": "migrations/mysql/2018-05-25-232323_update_attachments_reference/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2018-05-25-232323_update_attachments_reference/up.sql",
    "content": "ALTER TABLE attachments RENAME TO oldAttachments;\n\nCREATE TABLE attachments (\n  id          CHAR(36) NOT NULL PRIMARY KEY,\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  file_name   TEXT    NOT NULL,\n  file_size   INTEGER NOT NULL\n\n);\n\nINSERT INTO attachments (id, cipher_uuid, file_name, file_size) \nSELECT id, cipher_uuid, file_name, file_size FROM oldAttachments;\n\nDROP TABLE oldAttachments;\n"
  },
  {
    "path": "migrations/mysql/2018-06-01-112529_update_devices_twofactor_remember/down.sql",
    "content": "-- This file should undo anything in `up.sql`"
  },
  {
    "path": "migrations/mysql/2018-06-01-112529_update_devices_twofactor_remember/up.sql",
    "content": "ALTER TABLE devices\n    ADD COLUMN\n    twofactor_remember TEXT;"
  },
  {
    "path": "migrations/mysql/2018-07-11-181453_create_u2f_twofactor/down.sql",
    "content": "UPDATE users\nSET totp_secret = (\n    SELECT twofactor.data FROM twofactor\n    WHERE twofactor.type = 0 \n    AND twofactor.user_uuid = users.uuid\n);\n\nDROP TABLE twofactor;"
  },
  {
    "path": "migrations/mysql/2018-07-11-181453_create_u2f_twofactor/up.sql",
    "content": "CREATE TABLE twofactor (\n  uuid      CHAR(36) NOT NULL PRIMARY KEY,\n  user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),\n  type      INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n\n  UNIQUE (user_uuid, type)\n);\n\n\nINSERT INTO twofactor (uuid, user_uuid, type, enabled, data) \nSELECT UUID(), uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;\n\nUPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty\n"
  },
  {
    "path": "migrations/mysql/2018-08-27-172114_update_ciphers/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2018-08-27-172114_update_ciphers/up.sql",
    "content": "ALTER TABLE ciphers\n    ADD COLUMN\n    password_history TEXT;"
  },
  {
    "path": "migrations/mysql/2018-09-10-111213_add_invites/down.sql",
    "content": "DROP TABLE invitations;"
  },
  {
    "path": "migrations/mysql/2018-09-10-111213_add_invites/up.sql",
    "content": "CREATE TABLE invitations (\n    email   VARCHAR(255) NOT NULL PRIMARY KEY\n);\n"
  },
  {
    "path": "migrations/mysql/2018-09-19-144557_add_kdf_columns/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2018-09-19-144557_add_kdf_columns/up.sql",
    "content": "ALTER TABLE users\n    ADD COLUMN\n    client_kdf_type INTEGER NOT NULL DEFAULT 0; -- PBKDF2\n\nALTER TABLE users\n    ADD COLUMN\n    client_kdf_iter INTEGER NOT NULL DEFAULT 100000;\n"
  },
  {
    "path": "migrations/mysql/2018-11-27-152651_add_att_key_columns/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2018-11-27-152651_add_att_key_columns/up.sql",
    "content": "ALTER TABLE attachments\n    ADD COLUMN\n    `key` TEXT;\n"
  },
  {
    "path": "migrations/mysql/2019-05-26-216651_rename_key_and_type_columns/down.sql",
    "content": "ALTER TABLE attachments CHANGE COLUMN akey `key` TEXT;\nALTER TABLE ciphers CHANGE COLUMN atype type INTEGER NOT NULL;\nALTER TABLE devices CHANGE COLUMN atype type INTEGER NOT NULL;\nALTER TABLE twofactor CHANGE COLUMN atype type INTEGER NOT NULL;\nALTER TABLE users CHANGE COLUMN akey `key` TEXT;\nALTER TABLE users_organizations CHANGE COLUMN akey `key` TEXT;\nALTER TABLE users_organizations CHANGE COLUMN atype type INTEGER NOT NULL;"
  },
  {
    "path": "migrations/mysql/2019-05-26-216651_rename_key_and_type_columns/up.sql",
    "content": "ALTER TABLE attachments CHANGE COLUMN `key` akey TEXT;\nALTER TABLE ciphers CHANGE COLUMN type atype INTEGER NOT NULL;\nALTER TABLE devices CHANGE COLUMN type atype INTEGER NOT NULL;\nALTER TABLE twofactor CHANGE COLUMN type atype INTEGER NOT NULL;\nALTER TABLE users CHANGE COLUMN `key` akey TEXT;\nALTER TABLE users_organizations CHANGE COLUMN `key` akey TEXT;\nALTER TABLE users_organizations CHANGE COLUMN type atype INTEGER NOT NULL;"
  },
  {
    "path": "migrations/mysql/2019-10-10-083032_add_column_to_twofactor/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2019-10-10-083032_add_column_to_twofactor/up.sql",
    "content": "ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;"
  },
  {
    "path": "migrations/mysql/2019-11-17-011009_add_email_verification/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/mysql/2019-11-17-011009_add_email_verification/up.sql",
    "content": "ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;\nALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;\nALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;\nALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;\nALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;\n"
  },
  {
    "path": "migrations/mysql/2020-03-13-205045_add_policy_table/down.sql",
    "content": "DROP TABLE org_policies;\n"
  },
  {
    "path": "migrations/mysql/2020-03-13-205045_add_policy_table/up.sql",
    "content": "CREATE TABLE org_policies (\n  uuid      CHAR(36) NOT NULL PRIMARY KEY,\n  org_uuid  CHAR(36) NOT NULL REFERENCES organizations (uuid),\n  atype     INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n\n  UNIQUE (org_uuid, atype)\n);\n"
  },
  {
    "path": "migrations/mysql/2020-04-09-235005_add_cipher_delete_date/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/mysql/2020-04-09-235005_add_cipher_delete_date/up.sql",
    "content": "ALTER TABLE ciphers\n    ADD COLUMN\n    deleted_at DATETIME;\n"
  },
  {
    "path": "migrations/mysql/2020-07-01-214531_add_hide_passwords/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2020-07-01-214531_add_hide_passwords/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;\n"
  },
  {
    "path": "migrations/mysql/2020-08-02-025025_add_favorites_table/down.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- Transfer favorite status for user-owned ciphers.\nUPDATE ciphers\nSET favorite = TRUE\nWHERE EXISTS (\n  SELECT * FROM favorites\n  WHERE favorites.user_uuid = ciphers.user_uuid\n    AND favorites.cipher_uuid = ciphers.uuid\n);\n\nDROP TABLE favorites;\n"
  },
  {
    "path": "migrations/mysql/2020-08-02-025025_add_favorites_table/up.sql",
    "content": "CREATE TABLE favorites (\n  user_uuid   CHAR(36) NOT NULL REFERENCES users(uuid),\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers(uuid),\n\n  PRIMARY KEY (user_uuid, cipher_uuid)\n);\n\n-- Transfer favorite status for user-owned ciphers.\nINSERT INTO favorites(user_uuid, cipher_uuid)\nSELECT user_uuid, uuid\nFROM ciphers\nWHERE favorite = TRUE\n  AND user_uuid IS NOT NULL;\n\nALTER TABLE ciphers\nDROP COLUMN favorite;\n"
  },
  {
    "path": "migrations/mysql/2020-11-30-224000_add_user_enabled/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2020-11-30-224000_add_user_enabled/up.sql",
    "content": "ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1;\n"
  },
  {
    "path": "migrations/mysql/2020-12-09-173101_add_stamp_exception/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2020-12-09-173101_add_stamp_exception/up.sql",
    "content": "ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;"
  },
  {
    "path": "migrations/mysql/2021-03-11-190243_add_sends/down.sql",
    "content": "DROP TABLE sends;\n"
  },
  {
    "path": "migrations/mysql/2021-03-11-190243_add_sends/up.sql",
    "content": "CREATE TABLE sends (\n  uuid              CHAR(36) NOT NULL   PRIMARY KEY,\n  user_uuid         CHAR(36)            REFERENCES users (uuid),\n  organization_uuid CHAR(36)            REFERENCES organizations (uuid),\n\n  name              TEXT    NOT NULL,\n  notes             TEXT,\n\n  atype             INTEGER NOT NULL,\n  data              TEXT    NOT NULL,\n  akey              TEXT    NOT NULL,\n  password_hash     BLOB,\n  password_salt     BLOB,\n  password_iter     INTEGER,\n\n  max_access_count  INTEGER,\n  access_count      INTEGER NOT NULL,\n\n  creation_date     DATETIME NOT NULL,\n  revision_date     DATETIME NOT NULL,\n  expiration_date   DATETIME,\n  deletion_date     DATETIME NOT NULL,\n\n  disabled          BOOLEAN NOT NULL\n);"
  },
  {
    "path": "migrations/mysql/2021-04-30-233251_add_reprompt/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2021-04-30-233251_add_reprompt/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN reprompt INTEGER;\n"
  },
  {
    "path": "migrations/mysql/2021-05-11-205202_add_hide_email/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2021-05-11-205202_add_hide_email/up.sql",
    "content": "ALTER TABLE sends\nADD COLUMN hide_email BOOLEAN;\n"
  },
  {
    "path": "migrations/mysql/2021-07-01-203140_add_password_reset_keys/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2021-07-01-203140_add_password_reset_keys/up.sql",
    "content": "ALTER TABLE organizations\n  ADD COLUMN private_key TEXT;\n\nALTER TABLE organizations\n  ADD COLUMN public_key TEXT;\n"
  },
  {
    "path": "migrations/mysql/2021-08-30-193501_create_emergency_access/down.sql",
    "content": "DROP TABLE emergency_access;\n"
  },
  {
    "path": "migrations/mysql/2021-08-30-193501_create_emergency_access/up.sql",
    "content": "CREATE TABLE emergency_access (\n  uuid                      CHAR(36)     NOT NULL PRIMARY KEY,\n  grantor_uuid              CHAR(36)     REFERENCES users (uuid),\n  grantee_uuid              CHAR(36)     REFERENCES users (uuid),\n  email                     VARCHAR(255),\n  key_encrypted             TEXT,\n  atype                     INTEGER  NOT NULL,\n  status                    INTEGER  NOT NULL,\n  wait_time_days            INTEGER  NOT NULL,\n  recovery_initiated_at     DATETIME,\n  last_notification_at      DATETIME,\n  updated_at                DATETIME NOT NULL,\n  created_at                DATETIME NOT NULL\n);\n"
  },
  {
    "path": "migrations/mysql/2021-10-24-164321_add_2fa_incomplete/down.sql",
    "content": "DROP TABLE twofactor_incomplete;\n"
  },
  {
    "path": "migrations/mysql/2021-10-24-164321_add_2fa_incomplete/up.sql",
    "content": "CREATE TABLE twofactor_incomplete (\n  user_uuid   CHAR(36) NOT NULL REFERENCES users(uuid),\n  device_uuid CHAR(36) NOT NULL,\n  device_name TEXT     NOT NULL,\n  login_time  DATETIME NOT NULL,\n  ip_address  TEXT     NOT NULL,\n\n  PRIMARY KEY (user_uuid, device_uuid)\n);\n"
  },
  {
    "path": "migrations/mysql/2022-01-17-234911_add_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2022-01-17-234911_add_api_key/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN api_key VARCHAR(255);\n"
  },
  {
    "path": "migrations/mysql/2022-03-02-210038_update_devices_primary_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2022-03-02-210038_update_devices_primary_key/up.sql",
    "content": "-- First remove the previous primary key\nALTER TABLE devices DROP PRIMARY KEY;\n-- Add a new combined one\nALTER TABLE devices ADD PRIMARY KEY (uuid, user_uuid);\n"
  },
  {
    "path": "migrations/mysql/2022-07-27-110000_add_group_support/down.sql",
    "content": "DROP TABLE `groups`;\nDROP TABLE groups_users;\nDROP TABLE collections_groups;"
  },
  {
    "path": "migrations/mysql/2022-07-27-110000_add_group_support/up.sql",
    "content": "CREATE TABLE `groups` (\n  uuid                              CHAR(36) NOT NULL PRIMARY KEY,\n  organizations_uuid                VARCHAR(40) NOT NULL REFERENCES organizations (uuid),\n  name                              VARCHAR(100) NOT NULL,\n  access_all                        BOOLEAN NOT NULL,\n  external_id                       VARCHAR(300) NULL,\n  creation_date                     DATETIME NOT NULL,\n  revision_date                     DATETIME NOT NULL\n);\n\nCREATE TABLE groups_users (\n  groups_uuid                       CHAR(36) NOT NULL REFERENCES `groups` (uuid),\n  users_organizations_uuid          VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),\n  UNIQUE (groups_uuid, users_organizations_uuid)\n);\n\nCREATE TABLE collections_groups (\n  collections_uuid                  VARCHAR(40) NOT NULL REFERENCES collections (uuid),\n  groups_uuid                       CHAR(36) NOT NULL REFERENCES `groups` (uuid),\n  read_only                         BOOLEAN NOT NULL,\n  hide_passwords                    BOOLEAN NOT NULL,\n  UNIQUE (collections_uuid, groups_uuid)\n);"
  },
  {
    "path": "migrations/mysql/2022-10-18-170602_add_events/down.sql",
    "content": "DROP TABLE event;\n"
  },
  {
    "path": "migrations/mysql/2022-10-18-170602_add_events/up.sql",
    "content": "CREATE TABLE event (\n  uuid               CHAR(36)    NOT NULL PRIMARY KEY,\n  event_type         INTEGER     NOT NULL,\n  user_uuid          CHAR(36),\n  org_uuid           CHAR(36),\n  cipher_uuid        CHAR(36),\n  collection_uuid    CHAR(36),\n  group_uuid         CHAR(36),\n  org_user_uuid      CHAR(36),\n  act_user_uuid      CHAR(36),\n  device_type        INTEGER,\n  ip_address         TEXT,\n  event_date         DATETIME    NOT NULL,\n  policy_uuid        CHAR(36),\n  provider_uuid      CHAR(36),\n  provider_user_uuid CHAR(36),\n  provider_org_uuid  CHAR(36),\n  UNIQUE (uuid)\n);\n"
  },
  {
    "path": "migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql",
    "content": "ALTER TABLE users_organizations\nADD COLUMN reset_password_key TEXT;\n"
  },
  {
    "path": "migrations/mysql/2023-01-11-205851_add_avatar_color/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-01-11-205851_add_avatar_color/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN avatar_color VARCHAR(7);\n"
  },
  {
    "path": "migrations/mysql/2023-01-31-222222_add_argon2/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-01-31-222222_add_argon2/up.sql",
    "content": "ALTER TABLE users\n    ADD COLUMN\n    client_kdf_memory INTEGER DEFAULT NULL;\n\nALTER TABLE users\n    ADD COLUMN\n    client_kdf_parallelism INTEGER DEFAULT NULL;\n"
  },
  {
    "path": "migrations/mysql/2023-02-18-125735_push_uuid_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-02-18-125735_push_uuid_table/up.sql",
    "content": "ALTER TABLE devices ADD COLUMN push_uuid TEXT;"
  },
  {
    "path": "migrations/mysql/2023-06-02-200424_create_organization_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql",
    "content": "CREATE TABLE organization_api_key (\n\tuuid\t\t\tCHAR(36) NOT NULL,\n\torg_uuid\t\tCHAR(36) NOT NULL REFERENCES organizations(uuid),\n\tatype\t\t\tINTEGER NOT NULL,\n\tapi_key\t\t\tVARCHAR(255) NOT NULL,\n\trevision_date\tDATETIME NOT NULL,\n\tPRIMARY KEY(uuid, org_uuid)\n);\n\nALTER TABLE users ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/mysql/2023-06-17-200424_create_auth_requests_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-06-17-200424_create_auth_requests_table/up.sql",
    "content": "CREATE TABLE auth_requests (\n\tuuid            CHAR(36) NOT NULL PRIMARY KEY,\n\tuser_uuid\t    CHAR(36) NOT NULL,\n\torganization_uuid           CHAR(36),\n\trequest_device_identifier         CHAR(36) NOT NULL,\n\tdevice_type         INTEGER NOT NULL,\n\trequest_ip         TEXT NOT NULL,\n\tresponse_device_id         CHAR(36),\n\taccess_code         TEXT NOT NULL,\n\tpublic_key         TEXT NOT NULL,\n\tenc_key         TEXT NOT NULL,\n\tmaster_password_hash         TEXT NOT NULL,\n\tapproved         BOOLEAN,\n\tcreation_date         DATETIME NOT NULL,\n\tresponse_date         DATETIME,\n\tauthentication_date         DATETIME,\n\tFOREIGN KEY(user_uuid) REFERENCES users(uuid),\n\tFOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)\n);"
  },
  {
    "path": "migrations/mysql/2023-06-28-133700_add_collection_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-06-28-133700_add_collection_external_id/up.sql",
    "content": "ALTER TABLE collections ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/mysql/2023-09-01-170620_update_auth_request_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-09-01-170620_update_auth_request_table/up.sql",
    "content": "ALTER TABLE auth_requests\nMODIFY master_password_hash TEXT;\n\nALTER TABLE auth_requests\nMODIFY enc_key TEXT;\n"
  },
  {
    "path": "migrations/mysql/2023-09-02-212336_move_user_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-09-02-212336_move_user_external_id/up.sql",
    "content": "ALTER TABLE users_organizations\nADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/mysql/2023-09-10-133000_add_sso/down.sql",
    "content": "DROP TABLE sso_nonce;\n"
  },
  {
    "path": "migrations/mysql/2023-09-10-133000_add_sso/up.sql",
    "content": "CREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/mysql/2023-09-14-133000_add_users_organizations_invited_by_email/down.sql",
    "content": "ALTER TABLE users_organizations DROP COLUMN invited_by_email;\n"
  },
  {
    "path": "migrations/mysql/2023-09-14-133000_add_users_organizations_invited_by_email/up.sql",
    "content": "ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;\n"
  },
  {
    "path": "migrations/mysql/2023-10-21-221242_add_cipher_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2023-10-21-221242_add_cipher_key/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN `key` TEXT;\n"
  },
  {
    "path": "migrations/mysql/2024-01-12-210182_change_attachment_size/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2024-01-12-210182_change_attachment_size/up.sql",
    "content": "ALTER TABLE attachments MODIFY file_size BIGINT NOT NULL;\n"
  },
  {
    "path": "migrations/mysql/2024-02-14-135828_change_time_stamp_data_type/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2024-02-14-135828_change_time_stamp_data_type/up.sql",
    "content": "ALTER TABLE twofactor MODIFY last_used BIGINT NOT NULL;\n"
  },
  {
    "path": "migrations/mysql/2024-02-14-170000_add_state_to_sso_nonce/down.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/mysql/2024-02-14-170000_add_state_to_sso_nonce/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n\tstate               VARCHAR(512) NOT NULL PRIMARY KEY,\n  \tnonce               TEXT NOT NULL,\n  \tredirect_uri \t\tTEXT NOT NULL,\n  \tcreated_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/mysql/2024-02-26-170000_add_pkce_to_sso_nonce/down.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n    state               VARCHAR(512) NOT NULL PRIMARY KEY,\n    nonce               TEXT NOT NULL,\n    redirect_uri        TEXT NOT NULL,\n    created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/mysql/2024-02-26-170000_add_pkce_to_sso_nonce/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n    state               VARCHAR(512) NOT NULL PRIMARY KEY,\n  \tnonce               TEXT NOT NULL,\n    verifier            TEXT,\n  \tredirect_uri \t\tTEXT NOT NULL,\n  \tcreated_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/mysql/2024-03-06-170000_add_sso_users/down.sql",
    "content": "DROP TABLE IF EXISTS sso_users;\n"
  },
  {
    "path": "migrations/mysql/2024-03-06-170000_add_sso_users/up.sql",
    "content": "CREATE TABLE sso_users (\n  user_uuid           CHAR(36) NOT NULL PRIMARY KEY,\n  identifier          VARCHAR(768) NOT NULL UNIQUE,\n  created_at          TIMESTAMP NOT NULL DEFAULT now(),\n\n  FOREIGN KEY(user_uuid) REFERENCES users(uuid)\n);\n"
  },
  {
    "path": "migrations/mysql/2024-03-13-170000_sso_users_cascade/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2024-03-13-170000_sso_users_cascade/up.sql",
    "content": "-- Dynamically create DROP FOREIGN KEY\n-- Some versions of MySQL or MariaDB might fail if the key doesn't exists\n-- This checks if the key exists, and if so, will drop it.\nSET @drop_sso_fk = IF((SELECT true FROM information_schema.TABLE_CONSTRAINTS WHERE\n    CONSTRAINT_SCHEMA = DATABASE() AND\n    TABLE_NAME = 'sso_users' AND\n    CONSTRAINT_NAME = 'sso_users_ibfk_1' AND\n    CONSTRAINT_TYPE = 'FOREIGN KEY') = true,\n    'ALTER TABLE sso_users DROP FOREIGN KEY sso_users_ibfk_1',\n    'SELECT 1');\nPREPARE stmt FROM @drop_sso_fk;\nEXECUTE stmt;\nDEALLOCATE PREPARE stmt;\n\nALTER TABLE sso_users ADD FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;\n"
  },
  {
    "path": "migrations/mysql/2024-06-05-131359_add_2fa_duo_store/down.sql",
    "content": "DROP TABLE twofactor_duo_ctx;"
  },
  {
    "path": "migrations/mysql/2024-06-05-131359_add_2fa_duo_store/up.sql",
    "content": "CREATE TABLE twofactor_duo_ctx (\n    state      VARCHAR(64)  NOT NULL,\n    user_email VARCHAR(255) NOT NULL,\n    nonce      VARCHAR(64)  NOT NULL,\n    exp        BIGINT       NOT NULL,\n\n    PRIMARY KEY (state)\n);"
  },
  {
    "path": "migrations/mysql/2024-09-04-091351_use_device_type_for_mails/down.sql",
    "content": "ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;\n"
  },
  {
    "path": "migrations/mysql/2024-09-04-091351_use_device_type_for_mails/up.sql",
    "content": "ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser\n"
  },
  {
    "path": "migrations/mysql/2025-01-09-172300_add_manage/down.sql",
    "content": ""
  },
  {
    "path": "migrations/mysql/2025-01-09-172300_add_manage/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;\n\nALTER TABLE collections_groups\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;\n"
  },
  {
    "path": "migrations/mysql/2025-08-20-120000_sso_nonce_to_auth/down.sql",
    "content": "DROP TABLE IF EXISTS sso_auth;\n\nCREATE TABLE sso_nonce (\n    state               VARCHAR(512) NOT NULL PRIMARY KEY,\n    nonce               TEXT NOT NULL,\n    verifier            TEXT,\n    redirect_uri        TEXT NOT NULL,\n    created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/mysql/2025-08-20-120000_sso_nonce_to_auth/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_auth (\n    state               VARCHAR(512) NOT NULL PRIMARY KEY,\n    client_challenge    TEXT NOT NULL,\n    nonce               TEXT NOT NULL,\n    redirect_uri        TEXT NOT NULL,\n    code_response       TEXT,\n    auth_response       TEXT,\n    created_at          TIMESTAMP NOT NULL DEFAULT now(),\n    updated_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2019-09-12-100000_create_tables/down.sql",
    "content": "DROP TABLE devices;\nDROP TABLE attachments;\nDROP TABLE users_collections;\nDROP TABLE users_organizations;\nDROP TABLE folders_ciphers;\nDROP TABLE ciphers_collections;\nDROP TABLE twofactor;\nDROP TABLE invitations;\nDROP TABLE collections;\nDROP TABLE folders;\nDROP TABLE ciphers;\nDROP TABLE users;\nDROP TABLE organizations;\n"
  },
  {
    "path": "migrations/postgresql/2019-09-12-100000_create_tables/up.sql",
    "content": "CREATE TABLE users (\n  uuid                CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          TIMESTAMP NOT NULL,\n  updated_at          TIMESTAMP NOT NULL,\n  email               VARCHAR(255) NOT NULL UNIQUE,\n  name                TEXT     NOT NULL,\n  password_hash       BYTEA     NOT NULL,\n  salt                BYTEA     NOT NULL,\n  password_iterations INTEGER  NOT NULL,\n  password_hint       TEXT,\n  akey                TEXT     NOT NULL,\n  private_key         TEXT,\n  public_key          TEXT,\n  totp_secret         TEXT,\n  totp_recover        TEXT,\n  security_stamp      TEXT     NOT NULL,\n  equivalent_domains  TEXT     NOT NULL,\n  excluded_globals    TEXT     NOT NULL,\n  client_kdf_type     INTEGER NOT NULL DEFAULT 0,\n  client_kdf_iter INTEGER NOT NULL DEFAULT 100000\n);\n\nCREATE TABLE devices (\n  uuid          CHAR(36) NOT NULL PRIMARY KEY,\n  created_at    TIMESTAMP NOT NULL,\n  updated_at    TIMESTAMP NOT NULL,\n  user_uuid     CHAR(36) NOT NULL REFERENCES users (uuid),\n  name          TEXT     NOT NULL,\n  atype         INTEGER  NOT NULL,\n  push_token    TEXT,\n  refresh_token TEXT     NOT NULL,\n  twofactor_remember TEXT\n);\n\nCREATE TABLE organizations (\n  uuid          VARCHAR(40) NOT NULL PRIMARY KEY,\n  name          TEXT NOT NULL,\n  billing_email TEXT NOT NULL\n);\n\nCREATE TABLE ciphers (\n  uuid              CHAR(36) NOT NULL PRIMARY KEY,\n  created_at        TIMESTAMP NOT NULL,\n  updated_at        TIMESTAMP NOT NULL,\n  user_uuid         CHAR(36) REFERENCES users (uuid),\n  organization_uuid CHAR(36) REFERENCES organizations (uuid),\n  atype             INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  favorite          BOOLEAN  NOT NULL,\n  password_history  TEXT\n);\n\nCREATE TABLE attachments (\n  id          CHAR(36) NOT NULL PRIMARY KEY,\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  file_name   TEXT    NOT NULL,\n  file_size   INTEGER NOT NULL,\n  akey        TEXT\n);\n\nCREATE TABLE folders (\n  uuid       CHAR(36) NOT NULL PRIMARY KEY,\n  created_at TIMESTAMP NOT NULL,\n  updated_at TIMESTAMP NOT NULL,\n  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),\n  name       TEXT     NOT NULL\n);\n\nCREATE TABLE collections (\n  uuid     VARCHAR(40) NOT NULL PRIMARY KEY,\n  org_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),\n  name     TEXT NOT NULL\n);\n\nCREATE TABLE users_collections (\n  user_uuid       CHAR(36) NOT NULL REFERENCES users (uuid),\n  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),\n  read_only       BOOLEAN NOT NULL DEFAULT false,\n  PRIMARY KEY (user_uuid, collection_uuid)\n);\n\nCREATE TABLE users_organizations (\n  uuid       CHAR(36) NOT NULL PRIMARY KEY,\n  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),\n  org_uuid   CHAR(36) NOT NULL REFERENCES organizations (uuid),\n\n  access_all BOOLEAN NOT NULL,\n  akey       TEXT    NOT NULL,\n  status     INTEGER NOT NULL,\n  atype      INTEGER NOT NULL,\n\n  UNIQUE (user_uuid, org_uuid)\n);\n\nCREATE TABLE folders_ciphers (\n  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  folder_uuid CHAR(36) NOT NULL REFERENCES folders (uuid),\n  PRIMARY KEY (cipher_uuid, folder_uuid)\n);\n\nCREATE TABLE ciphers_collections (\n  cipher_uuid       CHAR(36) NOT NULL REFERENCES ciphers (uuid),\n  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),\n  PRIMARY KEY (cipher_uuid, collection_uuid)\n);\n\nCREATE TABLE twofactor (\n  uuid      CHAR(36) NOT NULL PRIMARY KEY,\n  user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),\n  atype     INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n  UNIQUE (user_uuid, atype)\n);\n\nCREATE TABLE invitations (\n    email   VARCHAR(255) NOT NULL PRIMARY KEY\n);"
  },
  {
    "path": "migrations/postgresql/2019-09-16-150000_fix_attachments/down.sql",
    "content": "ALTER TABLE attachments ALTER COLUMN id TYPE CHAR(36);\nALTER TABLE attachments ALTER COLUMN cipher_uuid TYPE CHAR(36);\nALTER TABLE users ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);\nALTER TABLE devices ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE devices ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE organizations ALTER COLUMN uuid TYPE CHAR(40);\nALTER TABLE ciphers ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE ciphers ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE ciphers ALTER COLUMN organization_uuid TYPE CHAR(36);\nALTER TABLE folders ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE folders ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE collections ALTER COLUMN uuid TYPE CHAR(40);\nALTER TABLE collections ALTER COLUMN org_uuid TYPE CHAR(40);\nALTER TABLE users_collections ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE users_collections ALTER COLUMN collection_uuid TYPE CHAR(36);\nALTER TABLE users_organizations ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE users_organizations ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE users_organizations ALTER COLUMN org_uuid TYPE CHAR(36);\nALTER TABLE folders_ciphers ALTER COLUMN cipher_uuid TYPE CHAR(36);\nALTER TABLE folders_ciphers ALTER COLUMN folder_uuid TYPE CHAR(36);\nALTER TABLE ciphers_collections ALTER COLUMN cipher_uuid TYPE CHAR(36);\nALTER TABLE ciphers_collections ALTER COLUMN collection_uuid TYPE CHAR(36);\nALTER TABLE twofactor ALTER COLUMN uuid TYPE CHAR(36);\nALTER TABLE twofactor ALTER COLUMN user_uuid TYPE CHAR(36);\nALTER TABLE invitations ALTER COLUMN email TYPE VARCHAR(255);"
  },
  {
    "path": "migrations/postgresql/2019-09-16-150000_fix_attachments/up.sql",
    "content": "-- Switch from CHAR() types to VARCHAR() types to avoid padding issues.\nALTER TABLE attachments ALTER COLUMN id TYPE TEXT;\nALTER TABLE attachments ALTER COLUMN cipher_uuid TYPE VARCHAR(40);\nALTER TABLE users ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE users ALTER COLUMN email TYPE TEXT;\nALTER TABLE devices ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE devices ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE organizations ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE ciphers ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE ciphers ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE ciphers ALTER COLUMN organization_uuid TYPE VARCHAR(40);\nALTER TABLE folders ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE folders ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE collections ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE collections ALTER COLUMN org_uuid TYPE VARCHAR(40);\nALTER TABLE users_collections ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE users_collections ALTER COLUMN collection_uuid TYPE VARCHAR(40);\nALTER TABLE users_organizations ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE users_organizations ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE users_organizations ALTER COLUMN org_uuid TYPE VARCHAR(40);\nALTER TABLE folders_ciphers ALTER COLUMN cipher_uuid TYPE VARCHAR(40);\nALTER TABLE folders_ciphers ALTER COLUMN folder_uuid TYPE VARCHAR(40);\nALTER TABLE ciphers_collections ALTER COLUMN cipher_uuid TYPE VARCHAR(40);\nALTER TABLE ciphers_collections ALTER COLUMN collection_uuid TYPE VARCHAR(40);\nALTER TABLE twofactor ALTER COLUMN uuid TYPE VARCHAR(40);\nALTER TABLE twofactor ALTER COLUMN user_uuid TYPE VARCHAR(40);\nALTER TABLE invitations ALTER COLUMN email TYPE TEXT;"
  },
  {
    "path": "migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/up.sql",
    "content": "ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;"
  },
  {
    "path": "migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql",
    "content": "ALTER TABLE users ADD COLUMN verified_at TIMESTAMP DEFAULT NULL;\nALTER TABLE users ADD COLUMN last_verifying_at TIMESTAMP DEFAULT NULL;\nALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;\nALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL;\nALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2020-03-13-205045_add_policy_table/down.sql",
    "content": "DROP TABLE org_policies;\n"
  },
  {
    "path": "migrations/postgresql/2020-03-13-205045_add_policy_table/up.sql",
    "content": "CREATE TABLE org_policies (\n  uuid      CHAR(36) NOT NULL PRIMARY KEY,\n  org_uuid  CHAR(36) NOT NULL REFERENCES organizations (uuid),\n  atype     INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n  \n  UNIQUE (org_uuid, atype)\n);\n"
  },
  {
    "path": "migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/up.sql",
    "content": "ALTER TABLE ciphers\n    ADD COLUMN\n    deleted_at TIMESTAMP;\n"
  },
  {
    "path": "migrations/postgresql/2020-07-01-214531_add_hide_passwords/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2020-07-01-214531_add_hide_passwords/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;\n"
  },
  {
    "path": "migrations/postgresql/2020-08-02-025025_add_favorites_table/down.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- Transfer favorite status for user-owned ciphers.\nUPDATE ciphers\nSET favorite = TRUE\nWHERE EXISTS (\n  SELECT * FROM favorites\n  WHERE favorites.user_uuid = ciphers.user_uuid\n    AND favorites.cipher_uuid = ciphers.uuid\n);\n\nDROP TABLE favorites;\n"
  },
  {
    "path": "migrations/postgresql/2020-08-02-025025_add_favorites_table/up.sql",
    "content": "CREATE TABLE favorites (\n  user_uuid   VARCHAR(40) NOT NULL REFERENCES users(uuid),\n  cipher_uuid VARCHAR(40) NOT NULL REFERENCES ciphers(uuid),\n\n  PRIMARY KEY (user_uuid, cipher_uuid)\n);\n\n-- Transfer favorite status for user-owned ciphers.\nINSERT INTO favorites(user_uuid, cipher_uuid)\nSELECT user_uuid, uuid\nFROM ciphers\nWHERE favorite = TRUE\n  AND user_uuid IS NOT NULL;\n\nALTER TABLE ciphers\nDROP COLUMN favorite;\n"
  },
  {
    "path": "migrations/postgresql/2020-11-30-224000_add_user_enabled/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2020-11-30-224000_add_user_enabled/up.sql",
    "content": "ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true;\n"
  },
  {
    "path": "migrations/postgresql/2020-12-09-173101_add_stamp_exception/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2020-12-09-173101_add_stamp_exception/up.sql",
    "content": "ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;"
  },
  {
    "path": "migrations/postgresql/2021-03-11-190243_add_sends/down.sql",
    "content": "DROP TABLE sends;\n"
  },
  {
    "path": "migrations/postgresql/2021-03-11-190243_add_sends/up.sql",
    "content": "CREATE TABLE sends (\n  uuid              CHAR(36) NOT NULL   PRIMARY KEY,\n  user_uuid         CHAR(36)            REFERENCES users (uuid),\n  organization_uuid CHAR(36)            REFERENCES organizations (uuid),\n\n  name              TEXT    NOT NULL,\n  notes             TEXT,\n\n  atype             INTEGER NOT NULL,\n  data              TEXT    NOT NULL,\n  key               TEXT    NOT NULL,\n  password_hash     BYTEA,\n  password_salt     BYTEA,\n  password_iter     INTEGER,\n\n  max_access_count  INTEGER,\n  access_count      INTEGER NOT NULL,\n\n  creation_date     TIMESTAMP NOT NULL,\n  revision_date     TIMESTAMP NOT NULL,\n  expiration_date   TIMESTAMP,\n  deletion_date     TIMESTAMP NOT NULL,\n\n  disabled          BOOLEAN NOT NULL\n);"
  },
  {
    "path": "migrations/postgresql/2021-03-15-163412_rename_send_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2021-03-15-163412_rename_send_key/up.sql",
    "content": "ALTER TABLE sends RENAME COLUMN key TO akey;\n"
  },
  {
    "path": "migrations/postgresql/2021-04-30-233251_add_reprompt/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2021-04-30-233251_add_reprompt/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN reprompt INTEGER;\n"
  },
  {
    "path": "migrations/postgresql/2021-05-11-205202_add_hide_email/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2021-05-11-205202_add_hide_email/up.sql",
    "content": "ALTER TABLE sends\nADD COLUMN hide_email BOOLEAN;\n"
  },
  {
    "path": "migrations/postgresql/2021-07-01-203140_add_password_reset_keys/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2021-07-01-203140_add_password_reset_keys/up.sql",
    "content": "ALTER TABLE organizations\n  ADD COLUMN private_key TEXT;\n\nALTER TABLE organizations\n  ADD COLUMN public_key TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2021-08-30-193501_create_emergency_access/down.sql",
    "content": "DROP TABLE emergency_access;\n"
  },
  {
    "path": "migrations/postgresql/2021-08-30-193501_create_emergency_access/up.sql",
    "content": "CREATE TABLE emergency_access (\n  uuid                      CHAR(36)     NOT NULL PRIMARY KEY,\n  grantor_uuid              CHAR(36)     REFERENCES users (uuid),\n  grantee_uuid              CHAR(36)     REFERENCES users (uuid),\n  email                     VARCHAR(255),\n  key_encrypted             TEXT,\n  atype                     INTEGER  NOT NULL,\n  status                    INTEGER  NOT NULL,\n  wait_time_days            INTEGER  NOT NULL,\n  recovery_initiated_at     TIMESTAMP,\n  last_notification_at      TIMESTAMP,\n  updated_at                TIMESTAMP NOT NULL,\n  created_at                TIMESTAMP NOT NULL\n);\n"
  },
  {
    "path": "migrations/postgresql/2021-10-24-164321_add_2fa_incomplete/down.sql",
    "content": "DROP TABLE twofactor_incomplete;\n"
  },
  {
    "path": "migrations/postgresql/2021-10-24-164321_add_2fa_incomplete/up.sql",
    "content": "CREATE TABLE twofactor_incomplete (\n  user_uuid   VARCHAR(40) NOT NULL REFERENCES users(uuid),\n  device_uuid VARCHAR(40) NOT NULL,\n  device_name TEXT        NOT NULL,\n  login_time  TIMESTAMP   NOT NULL,\n  ip_address  TEXT        NOT NULL,\n\n  PRIMARY KEY (user_uuid, device_uuid)\n);\n"
  },
  {
    "path": "migrations/postgresql/2022-01-17-234911_add_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2022-01-17-234911_add_api_key/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN api_key TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2022-03-02-210038_update_devices_primary_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2022-03-02-210038_update_devices_primary_key/up.sql",
    "content": "-- First remove the previous primary key\nALTER TABLE devices DROP CONSTRAINT devices_pkey;\n-- Add a new combined one\nALTER TABLE devices ADD PRIMARY KEY (uuid, user_uuid);\n"
  },
  {
    "path": "migrations/postgresql/2022-07-27-110000_add_group_support/down.sql",
    "content": "DROP TABLE groups;\nDROP TABLE groups_users;\nDROP TABLE collections_groups;"
  },
  {
    "path": "migrations/postgresql/2022-07-27-110000_add_group_support/up.sql",
    "content": "CREATE TABLE groups (\n  uuid                              CHAR(36) NOT NULL PRIMARY KEY,\n  organizations_uuid                 VARCHAR(40) NOT NULL REFERENCES organizations (uuid),\n  name                              VARCHAR(100) NOT NULL,\n  access_all                        BOOLEAN NOT NULL,\n  external_id                       VARCHAR(300) NULL,\n  creation_date                     TIMESTAMP NOT NULL,\n  revision_date                     TIMESTAMP NOT NULL\n);\n\nCREATE TABLE groups_users (\n  groups_uuid                       CHAR(36) NOT NULL REFERENCES groups (uuid),\n  users_organizations_uuid          VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),\n  PRIMARY KEY (groups_uuid, users_organizations_uuid)\n);\n\nCREATE TABLE collections_groups (\n  collections_uuid                  VARCHAR(40) NOT NULL REFERENCES collections (uuid),\n  groups_uuid                       CHAR(36) NOT NULL REFERENCES groups (uuid),\n  read_only                         BOOLEAN NOT NULL,\n  hide_passwords                    BOOLEAN NOT NULL,\n  PRIMARY KEY (collections_uuid, groups_uuid)\n);"
  },
  {
    "path": "migrations/postgresql/2022-10-18-170602_add_events/down.sql",
    "content": "DROP TABLE event;\n"
  },
  {
    "path": "migrations/postgresql/2022-10-18-170602_add_events/up.sql",
    "content": "CREATE TABLE event (\n  uuid               CHAR(36)        NOT NULL PRIMARY KEY,\n  event_type         INTEGER     NOT NULL,\n  user_uuid          CHAR(36),\n  org_uuid           CHAR(36),\n  cipher_uuid        CHAR(36),\n  collection_uuid    CHAR(36),\n  group_uuid         CHAR(36),\n  org_user_uuid      CHAR(36),\n  act_user_uuid      CHAR(36),\n  device_type        INTEGER,\n  ip_address         TEXT,\n  event_date         TIMESTAMP    NOT NULL,\n  policy_uuid        CHAR(36),\n  provider_uuid      CHAR(36),\n  provider_user_uuid CHAR(36),\n  provider_org_uuid  CHAR(36),\n  UNIQUE (uuid)\n);\n"
  },
  {
    "path": "migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql",
    "content": "ALTER TABLE users_organizations\nADD COLUMN reset_password_key TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2023-01-11-205851_add_avatar_color/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-01-11-205851_add_avatar_color/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN avatar_color TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2023-01-31-222222_add_argon2/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-01-31-222222_add_argon2/up.sql",
    "content": "ALTER TABLE users\n    ADD COLUMN\n    client_kdf_memory INTEGER DEFAULT NULL;\n\nALTER TABLE users\n    ADD COLUMN\n    client_kdf_parallelism INTEGER DEFAULT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2023-02-18-125735_push_uuid_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-02-18-125735_push_uuid_table/up.sql",
    "content": "ALTER TABLE devices ADD COLUMN push_uuid TEXT;"
  },
  {
    "path": "migrations/postgresql/2023-06-02-200424_create_organization_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql",
    "content": "CREATE TABLE organization_api_key (\n\tuuid\t\t\tCHAR(36) NOT NULL,\n\torg_uuid\t\tCHAR(36) NOT NULL REFERENCES organizations(uuid),\n\tatype\t\t\tINTEGER NOT NULL,\n\tapi_key\t\t\tVARCHAR(255),\n\trevision_date\tTIMESTAMP NOT NULL,\n\tPRIMARY KEY(uuid, org_uuid)\n);\n\nALTER TABLE users ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2023-06-17-200424_create_auth_requests_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-06-17-200424_create_auth_requests_table/up.sql",
    "content": "CREATE TABLE auth_requests (\n\tuuid            CHAR(36) NOT NULL PRIMARY KEY,\n\tuser_uuid\t    CHAR(36) NOT NULL,\n\torganization_uuid           CHAR(36),\n\trequest_device_identifier         CHAR(36) NOT NULL,\n\tdevice_type         INTEGER NOT NULL,\n\trequest_ip         TEXT NOT NULL,\n\tresponse_device_id         CHAR(36),\n\taccess_code         TEXT NOT NULL,\n\tpublic_key         TEXT NOT NULL,\n\tenc_key         TEXT NOT NULL,\n\tmaster_password_hash         TEXT NOT NULL,\n\tapproved         BOOLEAN,\n\tcreation_date         TIMESTAMP NOT NULL,\n\tresponse_date         TIMESTAMP,\n\tauthentication_date         TIMESTAMP,\n\tFOREIGN KEY(user_uuid) REFERENCES users(uuid),\n\tFOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)\n);"
  },
  {
    "path": "migrations/postgresql/2023-06-28-133700_add_collection_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-06-28-133700_add_collection_external_id/up.sql",
    "content": "ALTER TABLE collections ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2023-09-01-170620_update_auth_request_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-09-01-170620_update_auth_request_table/up.sql",
    "content": "ALTER TABLE auth_requests\nALTER COLUMN master_password_hash DROP NOT NULL;\n\nALTER TABLE auth_requests\nALTER COLUMN enc_key DROP NOT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2023-09-02-212336_move_user_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-09-02-212336_move_user_external_id/up.sql",
    "content": "ALTER TABLE users_organizations\nADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2023-09-10-133000_add_sso/down.sql",
    "content": "DROP TABLE sso_nonce;\n"
  },
  {
    "path": "migrations/postgresql/2023-09-10-133000_add_sso/up.sql",
    "content": "CREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2023-09-14-133000_add_users_organizations_invited_by_email/down.sql",
    "content": "ALTER TABLE users_organizations DROP COLUMN invited_by_email;\n"
  },
  {
    "path": "migrations/postgresql/2023-09-14-133000_add_users_organizations_invited_by_email/up.sql",
    "content": "ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2023-10-21-221242_add_cipher_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2023-10-21-221242_add_cipher_key/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN \"key\" TEXT;\n"
  },
  {
    "path": "migrations/postgresql/2024-01-12-210182_change_attachment_size/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2024-01-12-210182_change_attachment_size/up.sql",
    "content": "ALTER TABLE attachments\nALTER COLUMN file_size TYPE BIGINT,\nALTER COLUMN file_size SET NOT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2024-02-14-135953_change_time_stamp_data_type/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2024-02-14-135953_change_time_stamp_data_type/up.sql",
    "content": "ALTER TABLE twofactor\nALTER COLUMN last_used TYPE BIGINT,\nALTER COLUMN last_used SET NOT NULL;\n"
  },
  {
    "path": "migrations/postgresql/2024-02-14-170000_add_state_to_sso_nonce/down.sql",
    "content": "DROP TABLE sso_nonce;\n\nCREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2024-02-14-170000_add_state_to_sso_nonce/up.sql",
    "content": "DROP TABLE sso_nonce;\n\nCREATE TABLE sso_nonce (\n\tstate               TEXT NOT NULL PRIMARY KEY,\n  \tnonce               TEXT NOT NULL,\n  \tredirect_uri \t\tTEXT NOT NULL,\n  \tcreated_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2024-02-26-170000_add_pkce_to_sso_nonce/down.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n    state               TEXT NOT NULL PRIMARY KEY,\n    nonce               TEXT NOT NULL,\n    redirect_uri        TEXT NOT NULL,\n    created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2024-02-26-170000_add_pkce_to_sso_nonce/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n    state               TEXT NOT NULL PRIMARY KEY,\n    nonce               TEXT NOT NULL,\n    verifier            TEXT,\n    redirect_uri        TEXT NOT NULL,\n    created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2024-03-06-170000_add_sso_users/down.sql",
    "content": "DROP TABLE IF EXISTS sso_users;\n"
  },
  {
    "path": "migrations/postgresql/2024-03-06-170000_add_sso_users/up.sql",
    "content": "CREATE TABLE sso_users (\n  user_uuid           CHAR(36) NOT NULL PRIMARY KEY,\n  identifier          TEXT NOT NULL UNIQUE,\n  created_at          TIMESTAMP NOT NULL DEFAULT now(),\n\n  FOREIGN KEY(user_uuid) REFERENCES users(uuid)\n);\n"
  },
  {
    "path": "migrations/postgresql/2024-03-13-170000_sso_users_cascade/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2024-03-13-170000_sso_users_cascade/up.sql",
    "content": "ALTER TABLE sso_users\n  DROP CONSTRAINT \"sso_users_user_uuid_fkey\",\n  ADD CONSTRAINT \"sso_users_user_uuid_fkey\" FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE;\n"
  },
  {
    "path": "migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/down.sql",
    "content": "DROP TABLE twofactor_duo_ctx;\n"
  },
  {
    "path": "migrations/postgresql/2024-06-05-131359_add_2fa_duo_store/up.sql",
    "content": "CREATE TABLE twofactor_duo_ctx (\n    state      VARCHAR(64) NOT NULL,\n    user_email VARCHAR(255)  NOT NULL,\n    nonce      VARCHAR(64) NOT NULL,\n    exp        BIGINT        NOT NULL,\n\n    PRIMARY KEY (state)\n);"
  },
  {
    "path": "migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/down.sql",
    "content": "ALTER TABLE twofactor_incomplete DROP COLUMN device_type;\n"
  },
  {
    "path": "migrations/postgresql/2024-09-04-091351_use_device_type_for_mails/up.sql",
    "content": "ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser\n"
  },
  {
    "path": "migrations/postgresql/2025-01-09-172300_add_manage/down.sql",
    "content": ""
  },
  {
    "path": "migrations/postgresql/2025-01-09-172300_add_manage/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;\n\nALTER TABLE collections_groups\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;\n"
  },
  {
    "path": "migrations/postgresql/2025-08-20-120000_sso_nonce_to_auth/down.sql",
    "content": "DROP TABLE IF EXISTS sso_auth;\n\nCREATE TABLE sso_nonce (\n    state               TEXT NOT NULL PRIMARY KEY,\n    nonce               TEXT NOT NULL,\n    verifier            TEXT,\n    redirect_uri        TEXT NOT NULL,\n    created_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/postgresql/2025-08-20-120000_sso_nonce_to_auth/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_auth (\n    state               TEXT NOT NULL PRIMARY KEY,\n    client_challenge    TEXT NOT NULL,\n    nonce               TEXT NOT NULL,\n    redirect_uri        TEXT NOT NULL,\n    code_response       TEXT,\n    auth_response       TEXT,\n    created_at          TIMESTAMP NOT NULL DEFAULT now(),\n    updated_at          TIMESTAMP NOT NULL DEFAULT now()\n);\n"
  },
  {
    "path": "migrations/sqlite/2018-01-14-171611_create_tables/down.sql",
    "content": "DROP TABLE users;\n\nDROP TABLE devices;\n\nDROP TABLE ciphers;\n\nDROP TABLE attachments;\n\nDROP TABLE folders;"
  },
  {
    "path": "migrations/sqlite/2018-01-14-171611_create_tables/up.sql",
    "content": "CREATE TABLE users (\n  uuid                TEXT     NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL,\n  updated_at          DATETIME NOT NULL,\n  email               TEXT     NOT NULL UNIQUE,\n  name                TEXT     NOT NULL,\n  password_hash       BLOB     NOT NULL,\n  salt                BLOB     NOT NULL,\n  password_iterations INTEGER  NOT NULL,\n  password_hint       TEXT,\n  key                 TEXT     NOT NULL,\n  private_key         TEXT,\n  public_key          TEXT,\n  totp_secret         TEXT,\n  totp_recover        TEXT,\n  security_stamp      TEXT     NOT NULL,\n  equivalent_domains  TEXT     NOT NULL,\n  excluded_globals    TEXT     NOT NULL\n);\n\nCREATE TABLE devices (\n  uuid          TEXT     NOT NULL PRIMARY KEY,\n  created_at    DATETIME NOT NULL,\n  updated_at    DATETIME NOT NULL,\n  user_uuid     TEXT     NOT NULL REFERENCES users (uuid),\n  name          TEXT     NOT NULL,\n  type          INTEGER  NOT NULL,\n  push_token    TEXT,\n  refresh_token TEXT     NOT NULL\n);\n\nCREATE TABLE ciphers (\n  uuid              TEXT     NOT NULL PRIMARY KEY,\n  created_at        DATETIME NOT NULL,\n  updated_at        DATETIME NOT NULL,\n  user_uuid         TEXT     NOT NULL REFERENCES users (uuid),\n  folder_uuid       TEXT REFERENCES folders (uuid),\n  organization_uuid TEXT,\n  type              INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  favorite          BOOLEAN  NOT NULL\n);\n\nCREATE TABLE attachments (\n  id          TEXT    NOT NULL PRIMARY KEY,\n  cipher_uuid TEXT    NOT NULL REFERENCES ciphers (uuid),\n  file_name   TEXT    NOT NULL,\n  file_size   INTEGER NOT NULL\n\n);\n\nCREATE TABLE folders (\n  uuid       TEXT     NOT NULL PRIMARY KEY,\n  created_at DATETIME NOT NULL,\n  updated_at DATETIME NOT NULL,\n  user_uuid  TEXT     NOT NULL REFERENCES users (uuid),\n  name       TEXT     NOT NULL\n);\n  "
  },
  {
    "path": "migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/down.sql",
    "content": "DROP TABLE collections;\n\nDROP TABLE organizations;\n\n\nDROP TABLE users_collections;\n\nDROP TABLE users_organizations;\n"
  },
  {
    "path": "migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/up.sql",
    "content": "CREATE TABLE collections (\n  uuid     TEXT NOT NULL PRIMARY KEY,\n  org_uuid TEXT NOT NULL REFERENCES organizations (uuid),\n  name     TEXT NOT NULL\n);\n\nCREATE TABLE organizations (\n  uuid          TEXT NOT NULL PRIMARY KEY,\n  name          TEXT NOT NULL,\n  billing_email TEXT NOT NULL\n);\n\n\nCREATE TABLE users_collections (\n  user_uuid       TEXT NOT NULL REFERENCES users (uuid),\n  collection_uuid TEXT NOT NULL REFERENCES collections (uuid),\n  PRIMARY KEY (user_uuid, collection_uuid)\n);\n\nCREATE TABLE users_organizations (\n  uuid       TEXT    NOT NULL PRIMARY KEY,\n  user_uuid  TEXT    NOT NULL REFERENCES users (uuid),\n  org_uuid   TEXT    NOT NULL REFERENCES organizations (uuid),\n\n  access_all BOOLEAN NOT NULL,\n  key        TEXT    NOT NULL,\n  status     INTEGER NOT NULL,\n  type       INTEGER NOT NULL,\n\n  UNIQUE (user_uuid, org_uuid)\n);\n"
  },
  {
    "path": "migrations/sqlite/2018-04-27-155151_create_users_ciphers/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2018-04-27-155151_create_users_ciphers/up.sql",
    "content": "ALTER TABLE ciphers RENAME TO oldCiphers;\n\nCREATE TABLE ciphers (\n  uuid              TEXT     NOT NULL PRIMARY KEY,\n  created_at        DATETIME NOT NULL,\n  updated_at        DATETIME NOT NULL,\n  user_uuid         TEXT     REFERENCES users (uuid), -- Make this optional\n  organization_uuid TEXT     REFERENCES organizations (uuid), -- Add reference to orgs table\n  -- Remove folder_uuid\n  type              INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  favorite          BOOLEAN  NOT NULL\n);\n\nCREATE TABLE folders_ciphers (\n  cipher_uuid TEXT NOT NULL REFERENCES ciphers (uuid),\n  folder_uuid TEXT NOT NULL REFERENCES folders (uuid),\n\n  PRIMARY KEY (cipher_uuid, folder_uuid)\n);\n\nINSERT INTO ciphers (uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite) \nSELECT uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite FROM oldCiphers;\n\nINSERT INTO folders_ciphers (cipher_uuid, folder_uuid)\nSELECT uuid, folder_uuid FROM oldCiphers WHERE folder_uuid IS NOT NULL;\n\n\nDROP TABLE oldCiphers;\n\nALTER TABLE users_collections ADD COLUMN read_only BOOLEAN NOT NULL DEFAULT 0; -- False\n"
  },
  {
    "path": "migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/down.sql",
    "content": "DROP TABLE ciphers_collections;"
  },
  {
    "path": "migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/up.sql",
    "content": "CREATE TABLE ciphers_collections (\n  cipher_uuid       TEXT NOT NULL REFERENCES ciphers (uuid),\n  collection_uuid TEXT NOT NULL REFERENCES collections (uuid),\n  PRIMARY KEY (cipher_uuid, collection_uuid)\n);"
  },
  {
    "path": "migrations/sqlite/2018-05-25-232323_update_attachments_reference/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2018-05-25-232323_update_attachments_reference/up.sql",
    "content": "ALTER TABLE attachments RENAME TO oldAttachments;\n\nCREATE TABLE attachments (\n  id          TEXT    NOT NULL PRIMARY KEY,\n  cipher_uuid TEXT    NOT NULL REFERENCES ciphers (uuid),\n  file_name   TEXT    NOT NULL,\n  file_size   INTEGER NOT NULL\n\n);\n\nINSERT INTO attachments (id, cipher_uuid, file_name, file_size) \nSELECT id, cipher_uuid, file_name, file_size FROM oldAttachments;\n\nDROP TABLE oldAttachments;"
  },
  {
    "path": "migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/down.sql",
    "content": "-- This file should undo anything in `up.sql`"
  },
  {
    "path": "migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/up.sql",
    "content": "ALTER TABLE devices\n    ADD COLUMN\n    twofactor_remember TEXT;"
  },
  {
    "path": "migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/down.sql",
    "content": "UPDATE users\nSET totp_secret = (\n    SELECT twofactor.data FROM twofactor\n    WHERE twofactor.type = 0 \n    AND twofactor.user_uuid = users.uuid\n);\n\nDROP TABLE twofactor;"
  },
  {
    "path": "migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/up.sql",
    "content": "CREATE TABLE twofactor (\n  uuid      TEXT     NOT NULL PRIMARY KEY,\n  user_uuid TEXT     NOT NULL REFERENCES users (uuid),\n  type      INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n\n  UNIQUE (user_uuid, type)\n);\n\n\nINSERT INTO twofactor (uuid, user_uuid, type, enabled, data) \nSELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;\n\nUPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty"
  },
  {
    "path": "migrations/sqlite/2018-08-27-172114_update_ciphers/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2018-08-27-172114_update_ciphers/up.sql",
    "content": "ALTER TABLE ciphers\n    ADD COLUMN\n    password_history TEXT;"
  },
  {
    "path": "migrations/sqlite/2018-09-10-111213_add_invites/down.sql",
    "content": "DROP TABLE invitations;"
  },
  {
    "path": "migrations/sqlite/2018-09-10-111213_add_invites/up.sql",
    "content": "CREATE TABLE invitations (\n    email   TEXT NOT NULL PRIMARY KEY\n);"
  },
  {
    "path": "migrations/sqlite/2018-09-19-144557_add_kdf_columns/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2018-09-19-144557_add_kdf_columns/up.sql",
    "content": "ALTER TABLE users\n    ADD COLUMN\n    client_kdf_type INTEGER NOT NULL DEFAULT 0; -- PBKDF2\n\nALTER TABLE users\n    ADD COLUMN\n    client_kdf_iter INTEGER NOT NULL DEFAULT 100000;\n"
  },
  {
    "path": "migrations/sqlite/2018-11-27-152651_add_att_key_columns/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2018-11-27-152651_add_att_key_columns/up.sql",
    "content": "ALTER TABLE attachments\n    ADD COLUMN\n    key TEXT;"
  },
  {
    "path": "migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/down.sql",
    "content": "ALTER TABLE attachments RENAME COLUMN akey TO key;\nALTER TABLE ciphers RENAME COLUMN atype TO type;\nALTER TABLE devices RENAME COLUMN atype TO type;\nALTER TABLE twofactor RENAME COLUMN atype TO type;\nALTER TABLE users RENAME COLUMN akey TO key;\nALTER TABLE users_organizations RENAME COLUMN akey TO key;\nALTER TABLE users_organizations RENAME COLUMN atype TO type;"
  },
  {
    "path": "migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/up.sql",
    "content": "ALTER TABLE attachments RENAME COLUMN key TO akey;\nALTER TABLE ciphers RENAME COLUMN type TO atype;\nALTER TABLE devices RENAME COLUMN type TO atype;\nALTER TABLE twofactor RENAME COLUMN type TO atype;\nALTER TABLE users RENAME COLUMN key TO akey;\nALTER TABLE users_organizations RENAME COLUMN key TO akey;\nALTER TABLE users_organizations RENAME COLUMN type TO atype;"
  },
  {
    "path": "migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/up.sql",
    "content": "ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;\n"
  },
  {
    "path": "migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql",
    "content": "ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL;\nALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL;\nALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0;\nALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL;\nALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL;\n"
  },
  {
    "path": "migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql",
    "content": "DROP TABLE org_policies;\n"
  },
  {
    "path": "migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql",
    "content": "CREATE TABLE org_policies (\n  uuid      TEXT     NOT NULL PRIMARY KEY,\n  org_uuid  TEXT     NOT NULL REFERENCES organizations (uuid),\n  atype     INTEGER  NOT NULL,\n  enabled   BOOLEAN  NOT NULL,\n  data      TEXT     NOT NULL,\n\n  UNIQUE (org_uuid, atype)\n);\n"
  },
  {
    "path": "migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql",
    "content": "\n"
  },
  {
    "path": "migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql",
    "content": "ALTER TABLE ciphers\n    ADD COLUMN\n    deleted_at DATETIME;\n"
  },
  {
    "path": "migrations/sqlite/2020-07-01-214531_add_hide_passwords/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2020-07-01-214531_add_hide_passwords/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT 0; -- FALSE\n"
  },
  {
    "path": "migrations/sqlite/2020-08-02-025025_add_favorites_table/down.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN favorite BOOLEAN NOT NULL DEFAULT 0; -- FALSE\n\n-- Transfer favorite status for user-owned ciphers.\nUPDATE ciphers\nSET favorite = 1\nWHERE EXISTS (\n  SELECT * FROM favorites\n  WHERE favorites.user_uuid = ciphers.user_uuid\n    AND favorites.cipher_uuid = ciphers.uuid\n);\n\nDROP TABLE favorites;\n"
  },
  {
    "path": "migrations/sqlite/2020-08-02-025025_add_favorites_table/up.sql",
    "content": "CREATE TABLE favorites (\n  user_uuid   TEXT NOT NULL REFERENCES users(uuid),\n  cipher_uuid TEXT NOT NULL REFERENCES ciphers(uuid),\n\n  PRIMARY KEY (user_uuid, cipher_uuid)\n);\n\n-- Transfer favorite status for user-owned ciphers.\nINSERT INTO favorites(user_uuid, cipher_uuid)\nSELECT user_uuid, uuid\nFROM ciphers\nWHERE favorite = 1\n  AND user_uuid IS NOT NULL;\n\n-- Drop the `favorite` column from the `ciphers` table, using the 12-step\n-- procedure from <https://www.sqlite.org/lang_altertable.html#altertabrename>.\n-- Note that some steps aren't applicable and are omitted.\n\n-- 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF.\n--\n-- Diesel runs each migration in its own transaction. `PRAGMA foreign_keys`\n-- is a no-op within a transaction, so this step must be done outside of this\n-- file, before starting the Diesel migrations.\n\n-- 2. Start a transaction.\n--\n-- Diesel already runs each migration in its own transaction.\n\n-- 4. Use CREATE TABLE to construct a new table \"new_X\" that is in the\n--    desired revised format of table X. Make sure that the name \"new_X\" does\n--    not collide with any existing table name, of course.\n\nCREATE TABLE new_ciphers(\n  uuid              TEXT     NOT NULL PRIMARY KEY,\n  created_at        DATETIME NOT NULL,\n  updated_at        DATETIME NOT NULL,\n  user_uuid         TEXT     REFERENCES users(uuid),\n  organization_uuid TEXT     REFERENCES organizations(uuid),\n  atype             INTEGER  NOT NULL,\n  name              TEXT     NOT NULL,\n  notes             TEXT,\n  fields            TEXT,\n  data              TEXT     NOT NULL,\n  password_history  TEXT,\n  deleted_at        DATETIME\n);\n\n-- 5. Transfer content from X into new_X using a statement like:\n--    INSERT INTO new_X SELECT ... FROM X.\n\nINSERT INTO new_ciphers(uuid, created_at, updated_at, user_uuid, organization_uuid, atype,\n                        name, notes, fields, data, password_history, deleted_at)\nSELECT uuid, created_at, updated_at, user_uuid, organization_uuid, atype,\n       name, notes, fields, data, password_history, deleted_at\nFROM ciphers;\n\n-- 6. Drop the old table X: DROP TABLE X.\n\nDROP TABLE ciphers;\n\n-- 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X.\n\nALTER TABLE new_ciphers RENAME TO ciphers;\n\n-- 11. Commit the transaction started in step 2.\n\n-- 12. If foreign keys constraints were originally enabled, reenable them now.\n--\n-- `PRAGMA foreign_keys` is scoped to a database connection, and Diesel\n-- migrations are run in a separate database connection that is closed once\n-- the migrations finish.\n"
  },
  {
    "path": "migrations/sqlite/2020-11-30-224000_add_user_enabled/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2020-11-30-224000_add_user_enabled/up.sql",
    "content": "ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1;\n"
  },
  {
    "path": "migrations/sqlite/2020-12-09-173101_add_stamp_exception/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2020-12-09-173101_add_stamp_exception/up.sql",
    "content": "ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL;"
  },
  {
    "path": "migrations/sqlite/2021-03-11-190243_add_sends/down.sql",
    "content": "DROP TABLE sends;\n"
  },
  {
    "path": "migrations/sqlite/2021-03-11-190243_add_sends/up.sql",
    "content": "CREATE TABLE sends (\n  uuid              TEXT NOT NULL   PRIMARY KEY,\n  user_uuid         TEXT            REFERENCES users (uuid),\n  organization_uuid TEXT            REFERENCES organizations (uuid),\n\n  name              TEXT    NOT NULL,\n  notes             TEXT,\n\n  atype             INTEGER NOT NULL,\n  data              TEXT    NOT NULL,\n  key               TEXT    NOT NULL,\n  password_hash     BLOB,\n  password_salt     BLOB,\n  password_iter     INTEGER,\n\n  max_access_count  INTEGER,\n  access_count      INTEGER NOT NULL,\n\n  creation_date     DATETIME NOT NULL,\n  revision_date     DATETIME NOT NULL,\n  expiration_date   DATETIME,\n  deletion_date     DATETIME NOT NULL,\n\n  disabled          BOOLEAN NOT NULL\n);"
  },
  {
    "path": "migrations/sqlite/2021-03-15-163412_rename_send_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql",
    "content": "ALTER TABLE sends RENAME COLUMN key TO akey;\n"
  },
  {
    "path": "migrations/sqlite/2021-04-30-233251_add_reprompt/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2021-04-30-233251_add_reprompt/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN reprompt INTEGER;\n"
  },
  {
    "path": "migrations/sqlite/2021-05-11-205202_add_hide_email/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2021-05-11-205202_add_hide_email/up.sql",
    "content": "ALTER TABLE sends\nADD COLUMN hide_email BOOLEAN;\n"
  },
  {
    "path": "migrations/sqlite/2021-07-01-203140_add_password_reset_keys/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2021-07-01-203140_add_password_reset_keys/up.sql",
    "content": "ALTER TABLE organizations\n  ADD COLUMN private_key TEXT;\n\nALTER TABLE organizations\n  ADD COLUMN public_key TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2021-08-30-193501_create_emergency_access/down.sql",
    "content": "DROP TABLE emergency_access;\n"
  },
  {
    "path": "migrations/sqlite/2021-08-30-193501_create_emergency_access/up.sql",
    "content": "CREATE TABLE emergency_access (\n  uuid                      TEXT     NOT NULL PRIMARY KEY,\n  grantor_uuid              TEXT     REFERENCES users (uuid),\n  grantee_uuid              TEXT     REFERENCES users (uuid),\n  email                     TEXT,\n  key_encrypted             TEXT,\n  atype                     INTEGER  NOT NULL,\n  status                    INTEGER  NOT NULL,\n  wait_time_days            INTEGER  NOT NULL,\n  recovery_initiated_at     DATETIME,\n  last_notification_at      DATETIME,\n  updated_at                DATETIME NOT NULL,\n  created_at                DATETIME NOT NULL\n);\n"
  },
  {
    "path": "migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/down.sql",
    "content": "DROP TABLE twofactor_incomplete;\n"
  },
  {
    "path": "migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/up.sql",
    "content": "CREATE TABLE twofactor_incomplete (\n  user_uuid   TEXT     NOT NULL REFERENCES users(uuid),\n  device_uuid TEXT     NOT NULL,\n  device_name TEXT     NOT NULL,\n  login_time  DATETIME NOT NULL,\n  ip_address  TEXT     NOT NULL,\n\n  PRIMARY KEY (user_uuid, device_uuid)\n);\n"
  },
  {
    "path": "migrations/sqlite/2022-01-17-234911_add_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2022-01-17-234911_add_api_key/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN api_key TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2022-03-02-210038_update_devices_primary_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2022-03-02-210038_update_devices_primary_key/up.sql",
    "content": "-- Create new devices table with primary keys on both uuid and user_uuid\nCREATE TABLE devices_new (\n\tuuid\tTEXT NOT NULL,\n\tcreated_at\tDATETIME NOT NULL,\n\tupdated_at\tDATETIME NOT NULL,\n\tuser_uuid\tTEXT NOT NULL,\n\tname\tTEXT NOT NULL,\n\tatype\tINTEGER NOT NULL,\n\tpush_token\tTEXT,\n\trefresh_token\tTEXT NOT NULL,\n\ttwofactor_remember\tTEXT,\n\tPRIMARY KEY(uuid, user_uuid),\n\tFOREIGN KEY(user_uuid) REFERENCES users(uuid)\n);\n\n-- Transfer current data to new table\nINSERT INTO devices_new SELECT * FROM devices;\n\n-- Drop the old table\nDROP TABLE devices;\n\n-- Rename the new table to the original name\nALTER TABLE devices_new RENAME TO devices;\n"
  },
  {
    "path": "migrations/sqlite/2022-07-27-110000_add_group_support/down.sql",
    "content": "DROP TABLE groups;\nDROP TABLE groups_users;\nDROP TABLE collections_groups;"
  },
  {
    "path": "migrations/sqlite/2022-07-27-110000_add_group_support/up.sql",
    "content": "CREATE TABLE groups (\n  uuid                              TEXT NOT NULL PRIMARY KEY,\n  organizations_uuid                TEXT NOT NULL REFERENCES organizations (uuid),\n  name                              TEXT NOT NULL,\n  access_all                        BOOLEAN NOT NULL,\n  external_id                       TEXT NULL,\n  creation_date                     TIMESTAMP NOT NULL,\n  revision_date                     TIMESTAMP NOT NULL\n);\n\nCREATE TABLE groups_users (\n  groups_uuid                       TEXT NOT NULL REFERENCES groups (uuid),\n  users_organizations_uuid          TEXT NOT NULL REFERENCES users_organizations (uuid),\n  UNIQUE (groups_uuid, users_organizations_uuid)\n);\n\nCREATE TABLE collections_groups (\n  collections_uuid                  TEXT NOT NULL REFERENCES collections (uuid),\n  groups_uuid                       TEXT NOT NULL REFERENCES groups (uuid),\n  read_only                         BOOLEAN NOT NULL,\n  hide_passwords                    BOOLEAN NOT NULL,\n  UNIQUE (collections_uuid, groups_uuid)\n);"
  },
  {
    "path": "migrations/sqlite/2022-10-18-170602_add_events/down.sql",
    "content": "DROP TABLE event;\n"
  },
  {
    "path": "migrations/sqlite/2022-10-18-170602_add_events/up.sql",
    "content": "CREATE TABLE event (\n  uuid               TEXT        NOT NULL PRIMARY KEY,\n  event_type         INTEGER     NOT NULL,\n  user_uuid          TEXT,\n  org_uuid           TEXT,\n  cipher_uuid        TEXT,\n  collection_uuid    TEXT,\n  group_uuid         TEXT,\n  org_user_uuid      TEXT,\n  act_user_uuid      TEXT,\n  device_type        INTEGER,\n  ip_address         TEXT,\n  event_date         DATETIME    NOT NULL,\n  policy_uuid        TEXT,\n  provider_uuid      TEXT,\n  provider_user_uuid TEXT,\n  provider_org_uuid  TEXT,\n  UNIQUE (uuid)\n);\n"
  },
  {
    "path": "migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql",
    "content": "ALTER TABLE users_organizations\nADD COLUMN reset_password_key TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2023-01-11-205851_add_avatar_color/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-01-11-205851_add_avatar_color/up.sql",
    "content": "ALTER TABLE users\nADD COLUMN avatar_color TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2023-01-31-222222_add_argon2/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-01-31-222222_add_argon2/up.sql",
    "content": "ALTER TABLE users\n    ADD COLUMN\n    client_kdf_memory INTEGER DEFAULT NULL;\n\nALTER TABLE users\n    ADD COLUMN\n    client_kdf_parallelism INTEGER DEFAULT NULL;\n"
  },
  {
    "path": "migrations/sqlite/2023-02-18-125735_push_uuid_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-02-18-125735_push_uuid_table/up.sql",
    "content": "ALTER TABLE devices ADD COLUMN push_uuid TEXT;"
  },
  {
    "path": "migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql",
    "content": "CREATE TABLE organization_api_key (\n\tuuid            TEXT NOT NULL,\n    org_uuid\t    TEXT NOT NULL,\n    atype           INTEGER NOT NULL,\n    api_key         TEXT NOT NULL,\n\trevision_date   DATETIME NOT NULL,\n\tPRIMARY KEY(uuid, org_uuid),\n\tFOREIGN KEY(org_uuid) REFERENCES organizations(uuid)\n);\n\nALTER TABLE users ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2023-06-17-200424_create_auth_requests_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-06-17-200424_create_auth_requests_table/up.sql",
    "content": "CREATE TABLE auth_requests (\n\tuuid            TEXT NOT NULL PRIMARY KEY,\n\tuser_uuid\t    TEXT NOT NULL,\n\torganization_uuid           TEXT,\n\trequest_device_identifier         TEXT NOT NULL,\n\tdevice_type         INTEGER NOT NULL,\n\trequest_ip         TEXT NOT NULL,\n\tresponse_device_id         TEXT,\n\taccess_code         TEXT NOT NULL,\n\tpublic_key         TEXT NOT NULL,\n\tenc_key         TEXT NOT NULL,\n\tmaster_password_hash         TEXT NOT NULL,\n\tapproved         BOOLEAN,\n\tcreation_date         DATETIME NOT NULL,\n\tresponse_date         DATETIME,\n\tauthentication_date         DATETIME,\n\tFOREIGN KEY(user_uuid) REFERENCES users(uuid),\n\tFOREIGN KEY(organization_uuid) REFERENCES organizations(uuid)\n);"
  },
  {
    "path": "migrations/sqlite/2023-06-28-133700_add_collection_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-06-28-133700_add_collection_external_id/up.sql",
    "content": "ALTER TABLE collections ADD COLUMN external_id TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2023-09-01-170620_update_auth_request_table/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-09-01-170620_update_auth_request_table/up.sql",
    "content": "-- Create new auth_requests table with master_password_hash as nullable column\nCREATE TABLE auth_requests_new (\n    uuid                        TEXT NOT NULL PRIMARY KEY,\n    user_uuid                   TEXT NOT NULL,\n    organization_uuid           TEXT,\n    request_device_identifier   TEXT NOT NULL,\n    device_type                 INTEGER NOT NULL,\n    request_ip                  TEXT NOT NULL,\n    response_device_id          TEXT,\n    access_code                 TEXT NOT NULL,\n    public_key                  TEXT NOT NULL,\n    enc_key                     TEXT,\n    master_password_hash        TEXT,\n    approved                    BOOLEAN,\n    creation_date               DATETIME NOT NULL,\n    response_date               DATETIME,\n    authentication_date         DATETIME,\n    FOREIGN KEY (user_uuid) REFERENCES users (uuid),\n    FOREIGN KEY (organization_uuid) REFERENCES organizations (uuid)\n);\n\n-- Transfer current data to new table\nINSERT INTO\tauth_requests_new SELECT * FROM auth_requests;\n\n-- Drop the old table\nDROP TABLE auth_requests;\n\n-- Rename the new table to the original name\nALTER TABLE auth_requests_new RENAME TO auth_requests;\n"
  },
  {
    "path": "migrations/sqlite/2023-09-02-212336_move_user_external_id/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-09-02-212336_move_user_external_id/up.sql",
    "content": "-- Add the external_id to the users_organizations table\nALTER TABLE \"users_organizations\" ADD COLUMN \"external_id\" TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2023-09-10-133000_add_sso/down.sql",
    "content": "DROP TABLE sso_nonce;\n"
  },
  {
    "path": "migrations/sqlite/2023-09-10-133000_add_sso/up.sql",
    "content": "CREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2023-09-14-133000_add_users_organizations_invited_by_email/down.sql",
    "content": "ALTER TABLE users_organizations DROP COLUMN invited_by_email;\n"
  },
  {
    "path": "migrations/sqlite/2023-09-14-133000_add_users_organizations_invited_by_email/up.sql",
    "content": "ALTER TABLE users_organizations ADD COLUMN invited_by_email TEXT DEFAULT NULL;\n"
  },
  {
    "path": "migrations/sqlite/2023-10-21-221242_add_cipher_key/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2023-10-21-221242_add_cipher_key/up.sql",
    "content": "ALTER TABLE ciphers\nADD COLUMN \"key\" TEXT;\n"
  },
  {
    "path": "migrations/sqlite/2024-01-12-210182_change_attachment_size/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2024-01-12-210182_change_attachment_size/up.sql",
    "content": "-- Integer size in SQLite is already i64, so we don't need to do anything\n"
  },
  {
    "path": "migrations/sqlite/2024-02-14-140000_change_time_stamp_data_type/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2024-02-14-140000_change_time_stamp_data_type/up.sql",
    "content": "-- Integer size in SQLite is already i64, so we don't need to do anything\n"
  },
  {
    "path": "migrations/sqlite/2024-02-14-170000_add_state_to_sso_nonce/down.sql",
    "content": "DROP TABLE sso_nonce;\n\nCREATE TABLE sso_nonce (\n  nonce               CHAR(36) NOT NULL PRIMARY KEY,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-02-14-170000_add_state_to_sso_nonce/up.sql",
    "content": "DROP TABLE sso_nonce;\n\nCREATE TABLE sso_nonce (\n  state               TEXT NOT NULL PRIMARY KEY,\n  nonce               TEXT NOT NULL,\n  redirect_uri        TEXT NOT NULL,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-02-26-170000_add_pkce_to_sso_nonce/down.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n  state               TEXT NOT NULL PRIMARY KEY,\n  nonce               TEXT NOT NULL,\n  redirect_uri        TEXT NOT NULL,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-02-26-170000_add_pkce_to_sso_nonce/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_nonce (\n  state               TEXT NOT NULL PRIMARY KEY,\n  nonce               TEXT NOT NULL,\n  verifier            TEXT,\n  redirect_uri        TEXT NOT NULL,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-03-06-170000_add_sso_users/down.sql",
    "content": "DROP TABLE IF EXISTS sso_users;\n"
  },
  {
    "path": "migrations/sqlite/2024-03-06-170000_add_sso_users/up.sql",
    "content": "CREATE TABLE sso_users (\n  user_uuid           CHAR(36) NOT NULL PRIMARY KEY,\n  identifier          TEXT NOT NULL UNIQUE,\n  created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n  FOREIGN KEY(user_uuid) REFERENCES users(uuid)\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-03-13_170000_sso_userscascade/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2024-03-13_170000_sso_userscascade/up.sql",
    "content": "DROP TABLE IF EXISTS sso_users;\n\nCREATE TABLE sso_users (\n  user_uuid           CHAR(36) NOT NULL PRIMARY KEY,\n  identifier          TEXT NOT NULL UNIQUE,\n  created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n  FOREIGN KEY(user_uuid) REFERENCES users(uuid) ON UPDATE CASCADE ON DELETE CASCADE\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/down.sql",
    "content": "DROP TABLE twofactor_duo_ctx;"
  },
  {
    "path": "migrations/sqlite/2024-06-05-131359_add_2fa_duo_store/up.sql",
    "content": "CREATE TABLE twofactor_duo_ctx (\n    state      TEXT    NOT NULL,\n    user_email TEXT    NOT NULL,\n    nonce      TEXT    NOT NULL,\n    exp        INTEGER NOT NULL,\n\n    PRIMARY KEY (state)\n);\n"
  },
  {
    "path": "migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/down.sql",
    "content": "ALTER TABLE `twofactor_incomplete` DROP COLUMN `device_type`;\n"
  },
  {
    "path": "migrations/sqlite/2024-09-04-091351_use_device_type_for_mails/up.sql",
    "content": "ALTER TABLE twofactor_incomplete ADD COLUMN device_type INTEGER NOT NULL DEFAULT 14; -- 14 = Unknown Browser\n"
  },
  {
    "path": "migrations/sqlite/2025-01-09-172300_add_manage/down.sql",
    "content": ""
  },
  {
    "path": "migrations/sqlite/2025-01-09-172300_add_manage/up.sql",
    "content": "ALTER TABLE users_collections\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE\n\nALTER TABLE collections_groups\nADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE\n"
  },
  {
    "path": "migrations/sqlite/2025-08-20-120000_sso_nonce_to_auth/down.sql",
    "content": "DROP TABLE IF EXISTS sso_auth;\n\nCREATE TABLE sso_nonce (\n  state               TEXT NOT NULL PRIMARY KEY,\n  nonce               TEXT NOT NULL,\n  verifier            TEXT,\n  redirect_uri        TEXT NOT NULL,\n  created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "migrations/sqlite/2025-08-20-120000_sso_nonce_to_auth/up.sql",
    "content": "DROP TABLE IF EXISTS sso_nonce;\n\nCREATE TABLE sso_auth (\n    state               TEXT NOT NULL PRIMARY KEY,\n    client_challenge    TEXT NOT NULL,\n    nonce               TEXT NOT NULL,\n    redirect_uri        TEXT NOT NULL,\n    code_response       TEXT,\n    auth_response       TEXT,\n    created_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    updated_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "playwright/.gitignore",
    "content": "logs\nnode_modules/\n/test-results/\n/playwright-report/\n/playwright/.cache/\ntemp\n"
  },
  {
    "path": "playwright/README.md",
    "content": "# Integration tests\n\nThis allows running integration tests using [Playwright](https://playwright.dev/).\n\nIt uses its own `test.env` with different ports to not collide with a running dev instance.\n\n## Install\n\nThis relies on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).\nDatabases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in containers.\n\n### Running Playwright outside docker\n\nIt is possible to run `Playwright` outside of the container, this removes the need to rebuild the image for each change.\nYou will additionally need `nodejs` then run:\n\n```bash\nnpm ci --ignore-scripts\nnpx playwright install-deps\nnpx playwright install firefox\n```\n\n## Usage\n\nTo run all the tests:\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright\n```\n\nTo force a rebuild of the Playwright image:\n```bash\nDOCKER_BUILDKIT=1 docker compose --env-file test.env build Playwright\n```\n\nTo access the UI to easily run test individually and debug if needed (this will not work in docker):\n\n```bash\nnpx playwright test --ui\n```\n\n### DB\n\nProjects are configured to allow to run tests only on specific database.\n\nYou can use:\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mariadb\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mysql\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=postgres\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite\n```\n\n### SSO\n\nTo run the SSO tests:\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project sso-sqlite\n```\n\n### Keep services running\n\nIf you want you can keep the DB and Keycloak runnning (states are not impacted by the tests):\n\n```bash\nPW_KEEP_SERVICE_RUNNNING=true npx playwright test\n```\n\n### Running specific tests\n\nTo run a whole file you can :\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite tests/login.spec.ts\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login\n```\n\nTo run only a specifc test (It might fail if it has dependency):\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g \"Account creation\"\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite tests/login.spec.ts:16\n```\n\n## Writing scenario\n\nWhen creating new scenario use the recorder to more easily identify elements\n(in general try to rely on visible hint to identify elements and not hidden IDs).\nThis does not start the server, you will need to start it manually.\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden\nnpx playwright codegen \"http://127.0.0.1:8003\"\n```\n\n## Override web-vault\n\nIt is possible to change the `web-vault` used by referencing a different `vw_web_builds` commit.\n\nSimplest is to set and uncomment `PW_VW_REPO_URL` and `PW_VW_COMMIT_HASH` in the `test.env`.\nEnsure that the image is built with:\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Vaultwarden\n```\n\nYou can check the result running:\n\n```bash\nDOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden\n```\n\nThen check `http://127.0.0.1:8003/admin/diagnostics` with `admin`.\n\n# OpenID Connect test setup\n\nAdditionally this `docker-compose` template allows to run locally Vaultwarden,\n[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC.\n\n## Setup\n\nThis rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/).\nFirst create a copy of `.env.template` as `.env` (This is done to prevent committing your custom settings, Ex `SMTP_`).\n\n## Usage\n\nThen start the stack (the `profile` is required to run `Vaultwarden`) :\n\n```bash\n> docker compose --profile vaultwarden --env-file .env up\n....\nkeycloakSetup_1  | Logging into http://127.0.0.1:8080 as user admin of realm master\nkeycloakSetup_1  | Created new realm with id 'test'\nkeycloakSetup_1  | 74af4933-e386-4e64-ba15-a7b61212c45e\noidc_keycloakSetup_1 exited with code 0\n```\n\nWait until `oidc_keycloakSetup_1 exited with code 0` which indicates the correct setup of the Keycloak realm, client and user\n(It is normal for this container to stop once the configuration is done).\n\nThen you can access :\n\n- `Vaultwarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`.\n- `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin`\n- `Maildev` on http://0.0.0.0:1080\n\nTo proceed with an SSO login after you enter the email, on the screen prompting for `Master Password` the SSO button should be visible.\nTo use your computer external ip (for example when testing with a phone) you will have to configure `KC_HTTP_HOST` and `DOMAIN`.\n\n## Running only Keycloak\n\nYou can run just `Keycloak` with `--profile keycloak`:\n\n```bash\n> docker compose --profile keycloak --env-file .env up\n```\nWhen running with a local Vaultwarden, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).\n\n## Rebuilding the Vaultwarden\n\nTo force rebuilding the Vaultwarden image you can run\n\n```bash\ndocker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild Vaultwarden\n```\n\n## Configuration\n\nAll configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template).\nThe content of the file will be loaded as environment variables in all containers.\n\n- `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)).\n- All `Vaultwarden` configuration can be set (EX: `SMTP_*`)\n\n## Cleanup\n\nUse `docker compose --profile vaultwarden down`.\n"
  },
  {
    "path": "playwright/compose/keycloak/Dockerfile",
    "content": "FROM docker.io/library/debian:trixie-slim\n\nARG KEYCLOAK_VERSION\n\nENV DEBIAN_FRONTEND=noninteractive\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nRUN apt-get update && apt-get install -y ca-certificates curl jq openjdk-21-jdk-headless wget\n\nWORKDIR /\n\nRUN wget -c https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz -O - | tar -xz \\\n    && mkdir -p /opt/keycloak \\\n    && mv /keycloak-${KEYCLOAK_VERSION}/bin /opt/keycloak/bin \\\n    && rm -rf /keycloak-${KEYCLOAK_VERSION}\n\nCOPY setup.sh /setup.sh\n\nCMD \"/setup.sh\"\n"
  },
  {
    "path": "playwright/compose/keycloak/setup.sh",
    "content": "#!/bin/bash\n\nexport PATH=/opt/keycloak/bin:$PATH\n\nSTATUS_CODE=0\nwhile [[ \"$STATUS_CODE\" != \"404\" ]] ; do\n    echo \"Will retry in 2 seconds\"\n    sleep 2\n\n    STATUS_CODE=$(curl -s -o /dev/null -w \"%{http_code}\"  \"$DUMMY_AUTHORITY\")\n\n    if [[ \"$STATUS_CODE\" = \"200\" ]]; then\n        echo \"Setup should already be done. Will not run.\"\n        exit 0\n    fi\ndone\n\nset -e\n\nkcadm.sh config credentials --server \"http://${KC_HTTP_HOST}:${KC_HTTP_PORT}\" --realm master --user \"$KEYCLOAK_ADMIN\" --password \"$KEYCLOAK_ADMIN_PASSWORD\" --client admin-cli\n\nkcadm.sh create realms -s realm=\"$TEST_REALM\" -s enabled=true -s \"accessTokenLifespan=600\"\nkcadm.sh create clients -r test -s \"clientId=$SSO_CLIENT_ID\" -s \"secret=$SSO_CLIENT_SECRET\" -s \"redirectUris=[\\\"$DOMAIN/*\\\"]\" -i\n\nTEST_USER_ID=$(kcadm.sh create users -r \"$TEST_REALM\" -s \"username=$TEST_USER\" -s \"firstName=$TEST_USER\" -s \"lastName=$TEST_USER\" -s \"email=$TEST_USER_MAIL\"  -s emailVerified=true -s enabled=true -i)\nkcadm.sh update users/$TEST_USER_ID/reset-password -r \"$TEST_REALM\" -s type=password -s \"value=$TEST_USER_PASSWORD\" -n\n\nTEST_USER2_ID=$(kcadm.sh create users -r \"$TEST_REALM\" -s \"username=$TEST_USER2\" -s \"firstName=$TEST_USER2\" -s \"lastName=$TEST_USER2\" -s \"email=$TEST_USER2_MAIL\"  -s emailVerified=true -s enabled=true -i)\nkcadm.sh update users/$TEST_USER2_ID/reset-password -r \"$TEST_REALM\" -s type=password -s \"value=$TEST_USER2_PASSWORD\" -n\n\nTEST_USER3_ID=$(kcadm.sh create users -r \"$TEST_REALM\" -s \"username=$TEST_USER3\" -s \"firstName=$TEST_USER3\" -s \"lastName=$TEST_USER3\" -s \"email=$TEST_USER3_MAIL\"  -s emailVerified=true -s enabled=true -i)\nkcadm.sh update users/$TEST_USER3_ID/reset-password -r \"$TEST_REALM\" -s type=password -s \"value=$TEST_USER3_PASSWORD\" -n\n\n# Dummy realm to mark end of setup\nkcadm.sh create realms -s realm=\"$DUMMY_REALM\" -s enabled=true -s \"accessTokenLifespan=600\"\n\n# TO DEBUG uncomment the following line to keep the setup container running\n# sleep 3600\n# THEN in another terminal:\n# docker exec -it keycloakSetup-dev /bin/bash\n# export PATH=$PATH:/opt/keycloak/bin\n# kcadm.sh config credentials --server \"http://${KC_HTTP_HOST}:${KC_HTTP_PORT}\" --realm master --user \"$KEYCLOAK_ADMIN\" --password \"$KEYCLOAK_ADMIN_PASSWORD\" --client admin-cli\n# ENJOY\n# Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html\n"
  },
  {
    "path": "playwright/compose/playwright/Dockerfile",
    "content": "FROM docker.io/library/debian:trixie-slim\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN apt-get update \\\n    && apt-get install -y ca-certificates curl \\\n    && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \\\n    && chmod a+r /etc/apt/keyrings/docker.asc \\\n    && echo \"deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable\" | tee /etc/apt/sources.list.d/docker.list \\\n    && apt-get update \\\n    && apt-get install -y --no-install-recommends \\\n        containerd.io \\\n        docker-buildx-plugin \\\n        docker-ce \\\n        docker-ce-cli \\\n        docker-compose-plugin \\\n        git \\\n        libmariadb-dev-compat \\\n        libpq5 \\\n        nodejs \\\n        npm \\\n        openssl \\\n    && rm -rf /var/lib/apt/lists/*\n\nRUN mkdir /playwright\nWORKDIR /playwright\n\nCOPY package.json package-lock.json .\nRUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox\n\nCOPY docker-compose.yml test.env ./\nCOPY compose ./compose\n\nCOPY *.ts test.env ./\nCOPY tests ./tests\n\nENTRYPOINT [\"/usr/bin/npx\", \"playwright\"]\nCMD [\"test\"]\n"
  },
  {
    "path": "playwright/compose/warden/Dockerfile",
    "content": "FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt\n\nFROM node:22-trixie AS build\n\nARG REPO_URL\nARG COMMIT_HASH\n\nENV REPO_URL=$REPO_URL\nENV COMMIT_HASH=$COMMIT_HASH\n\nCOPY --from=prebuilt /web-vault /web-vault\n\nCOPY build.sh /build.sh\nRUN /build.sh\n\n######################## RUNTIME IMAGE  ########################\nFROM docker.io/library/debian:trixie-slim\n\nENV DEBIAN_FRONTEND=noninteractive\n\n# Create data folder and Install needed libraries\nRUN mkdir /data && \\\n    apt-get update && apt-get install -y \\\n        --no-install-recommends \\\n        ca-certificates \\\n        curl \\\n        libmariadb-dev \\\n        libpq5 \\\n        openssl && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Copies the files from the context (Rocket.toml file and web-vault)\n# and the binary from the \"build\" stage to the current stage\nWORKDIR /\n\nCOPY --from=prebuilt /start.sh .\nCOPY --from=prebuilt /vaultwarden .\nCOPY --from=build /web-vault ./web-vault\n\nENTRYPOINT [\"/start.sh\"]\n"
  },
  {
    "path": "playwright/compose/warden/build.sh",
    "content": "#!/bin/bash\n\necho $REPO_URL\necho $COMMIT_HASH\n\nif [[ ! -z \"$REPO_URL\" ]] && [[ ! -z \"$COMMIT_HASH\" ]] ; then\n    rm -rf /web-vault\n\n    mkdir -p vw_web_builds;\n    cd vw_web_builds;\n\n    git -c init.defaultBranch=main init\n    git remote add origin \"$REPO_URL\"\n    git fetch --depth 1 origin \"$COMMIT_HASH\"\n    git -c advice.detachedHead=false checkout FETCH_HEAD\n\n    npm ci --ignore-scripts\n\n    cd apps/web\n    npm run dist:oss:selfhost\n    printf '{\"version\":\"%s\"}' \"$COMMIT_HASH\" > build/vw-version.json\n\n    mv build /web-vault\nfi\n"
  },
  {
    "path": "playwright/docker-compose.yml",
    "content": "services:\n  VaultwardenPrebuild:\n    profiles: [\"playwright\", \"vaultwarden\"]\n    container_name: playwright_oidc_vaultwarden_prebuilt\n    image: playwright_oidc_vaultwarden_prebuilt\n    build:\n      context: ..\n      dockerfile: Dockerfile\n    entrypoint: /bin/bash\n    restart: \"no\"\n\n  Vaultwarden:\n    profiles: [\"playwright\", \"vaultwarden\"]\n    container_name: playwright_oidc_vaultwarden-${ENV:-dev}\n    image: playwright_oidc_vaultwarden-${ENV:-dev}\n    network_mode: \"host\"\n    build:\n      context: compose/warden\n      dockerfile: Dockerfile\n      args:\n        REPO_URL: ${PW_VW_REPO_URL:-}\n        COMMIT_HASH: ${PW_VW_COMMIT_HASH:-}\n    env_file: ${DC_ENV_FILE:-.env}\n    environment:\n      - ADMIN_TOKEN\n      - DATABASE_URL\n      - I_REALLY_WANT_VOLATILE_STORAGE\n      - LOG_LEVEL\n      - LOGIN_RATELIMIT_MAX_BURST\n      - SMTP_HOST\n      - SMTP_FROM\n      - SMTP_DEBUG\n      - SSO_DEBUG_TOKENS\n      - SSO_ENABLED\n      - SSO_FRONTEND\n      - SSO_ONLY\n      - SSO_SCOPES\n    restart: \"no\"\n    depends_on:\n      - VaultwardenPrebuild\n\n  Playwright:\n    profiles: [\"playwright\"]\n    container_name: playwright_oidc_playwright\n    image: playwright_oidc_playwright\n    network_mode: \"host\"\n    build:\n      context: .\n      dockerfile: compose/playwright/Dockerfile\n    environment:\n      - PW_WV_REPO_URL\n      - PW_WV_COMMIT_HASH\n    restart: \"no\"\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ..:/project\n\n  Mariadb:\n    profiles: [\"playwright\"]\n    container_name: playwright_mariadb\n    image: mariadb:11.2.4\n    env_file: test.env\n    healthcheck:\n      test: [\"CMD\", \"healthcheck.sh\", \"--connect\", \"--innodb_initialized\"]\n      start_period: 10s\n      interval: 10s\n    ports:\n      - ${MARIADB_PORT}:3306\n\n  Mysql:\n    profiles: [\"playwright\"]\n    container_name: playwright_mysql\n    image: mysql:8.4.1\n    env_file: test.env\n    healthcheck:\n      test: [\"CMD\", \"mysqladmin\" ,\"ping\", \"-h\", \"localhost\"]\n      start_period: 10s\n      interval: 10s\n    ports:\n      - ${MYSQL_PORT}:3306\n\n  Postgres:\n    profiles: [\"playwright\"]\n    container_name: playwright_postgres\n    image: postgres:16.3\n    env_file: test.env\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}\"]\n      start_period: 20s\n      interval: 30s\n    ports:\n      - ${POSTGRES_PORT}:5432\n\n  Maildev:\n    profiles: [\"vaultwarden\", \"maildev\"]\n    container_name: maildev\n    image: timshel/maildev:3.0.4\n    ports:\n      - ${SMTP_PORT}:1025\n      - 1080:1080\n\n  Keycloak:\n    profiles: [\"keycloak\", \"vaultwarden\"]\n    container_name: keycloak-${ENV:-dev}\n    image: quay.io/keycloak/keycloak:26.3.4\n    network_mode: \"host\"\n    command:\n      - start-dev\n    env_file: ${DC_ENV_FILE:-.env}\n\n  KeycloakSetup:\n    profiles: [\"keycloak\", \"vaultwarden\"]\n    container_name: keycloakSetup-${ENV:-dev}\n    image: keycloak_setup-${ENV:-dev}\n    build:\n      context: compose/keycloak\n      dockerfile: Dockerfile\n      args:\n        KEYCLOAK_VERSION: 26.3.4\n    network_mode: \"host\"\n    depends_on:\n      - Keycloak\n    restart: \"no\"\n    env_file: ${DC_ENV_FILE:-.env}\n"
  },
  {
    "path": "playwright/global-setup.ts",
    "content": "import { firefox, type FullConfig } from '@playwright/test';\nimport { execSync } from 'node:child_process';\nimport fs from 'fs';\n\nconst utils = require('./global-utils');\n\nutils.loadEnv();\n\nasync function globalSetup(config: FullConfig) {\n    // Are we running in docker and the project is mounted ?\n    const path = (fs.existsSync(\"/project/playwright/playwright.config.ts\") ? \"/project/playwright\" : \".\");\n    execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build VaultwardenPrebuild`, {\n        env: { ...process.env },\n        stdio: \"inherit\"\n    });\n    execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build Vaultwarden`, {\n        env: { ...process.env },\n        stdio: \"inherit\"\n    });\n}\n\nexport default globalSetup;\n"
  },
  {
    "path": "playwright/global-utils.ts",
    "content": "import { expect, type Browser, type TestInfo } from '@playwright/test';\nimport { EventEmitter } from \"events\";\nimport { type Mail, MailServer } from 'maildev';\nimport { execSync } from 'node:child_process';\n\nimport dotenv from 'dotenv';\nimport dotenvExpand from 'dotenv-expand';\n\nconst fs = require(\"fs\");\nconst { spawn } = require('node:child_process');\n\nexport function loadEnv(){\n    var myEnv = dotenv.config({ path: 'test.env', quiet: true });\n    dotenvExpand.expand(myEnv);\n\n    return {\n        user1: {\n            email: process.env.TEST_USER_MAIL,\n            name: process.env.TEST_USER,\n            password: process.env.TEST_USER_PASSWORD,\n        },\n        user2: {\n            email: process.env.TEST_USER2_MAIL,\n            name: process.env.TEST_USER2,\n            password: process.env.TEST_USER2_PASSWORD,\n        },\n        user3: {\n            email: process.env.TEST_USER3_MAIL,\n            name: process.env.TEST_USER3,\n            password: process.env.TEST_USER3_PASSWORD,\n        },\n    }\n}\n\nexport async function waitFor(url: String, browser: Browser) {\n    var ready = false;\n    var context;\n\n    do {\n        try {\n            context = await browser.newContext();\n            const page = await context.newPage();\n            await page.waitForTimeout(500);\n            const result = await page.goto(url);\n            ready = result.status() === 200;\n        } catch(e) {\n            if( !e.message.includes(\"CONNECTION_REFUSED\") ){\n                throw e;\n            }\n        } finally {\n            await context.close();\n        }\n    } while(!ready);\n}\n\nexport function startComposeService(serviceName: String){\n    console.log(`Starting ${serviceName}`);\n    execSync(`docker compose --profile playwright --env-file test.env  up -d ${serviceName}`);\n}\n\nexport function stopComposeService(serviceName: String){\n    console.log(`Stopping ${serviceName}`);\n    execSync(`docker compose --profile playwright --env-file test.env  stop ${serviceName}`);\n}\n\nfunction wipeSqlite(){\n    console.log(`Delete Vaultwarden container to wipe sqlite`);\n    execSync(`docker compose --env-file test.env stop Vaultwarden`);\n    execSync(`docker compose --env-file test.env rm -f Vaultwarden`);\n}\n\nasync function wipeMariaDB(){\n    var mysql = require('mysql2/promise');\n    var ready = false;\n    var connection;\n\n    do {\n        try {\n            connection = await mysql.createConnection({\n                user: process.env.MARIADB_USER,\n                host: \"127.0.0.1\",\n                database: process.env.MARIADB_DATABASE,\n                password: process.env.MARIADB_PASSWORD,\n                port: process.env.MARIADB_PORT,\n            });\n\n            await connection.execute(`DROP DATABASE ${process.env.MARIADB_DATABASE}`);\n            await connection.execute(`CREATE DATABASE ${process.env.MARIADB_DATABASE}`);\n            console.log('Successfully wiped mariadb');\n            ready = true;\n        } catch (err) {\n            console.log(`Error when wiping mariadb: ${err}`);\n        } finally {\n            if( connection ){\n                connection.end();\n            }\n        }\n        await new Promise(r => setTimeout(r, 1000));\n    } while(!ready);\n}\n\nasync function wipeMysqlDB(){\n    var mysql = require('mysql2/promise');\n    var ready = false;\n    var connection;\n\n    do{\n        try {\n            connection = await mysql.createConnection({\n                user: process.env.MYSQL_USER,\n                host: \"127.0.0.1\",\n                database: process.env.MYSQL_DATABASE,\n                password: process.env.MYSQL_PASSWORD,\n                port: process.env.MYSQL_PORT,\n            });\n\n            await connection.execute(`DROP DATABASE ${process.env.MYSQL_DATABASE}`);\n            await connection.execute(`CREATE DATABASE ${process.env.MYSQL_DATABASE}`);\n            console.log('Successfully wiped mysql');\n            ready = true;\n        } catch (err) {\n            console.log(`Error when wiping mysql: ${err}`);\n        } finally {\n            if( connection ){\n                connection.end();\n            }\n        }\n        await new Promise(r => setTimeout(r, 1000));\n    } while(!ready);\n}\n\nasync function wipePostgres(){\n    const { Client } = require('pg');\n\n    const client = new Client({\n        user: process.env.POSTGRES_USER,\n        host: \"127.0.0.1\",\n        database: \"postgres\",\n        password: process.env.POSTGRES_PASSWORD,\n        port: process.env.POSTGRES_PORT,\n    });\n\n    try {\n        await client.connect();\n        await client.query(`DROP DATABASE ${process.env.POSTGRES_DB}`);\n        await client.query(`CREATE DATABASE ${process.env.POSTGRES_DB}`);\n        console.log('Successfully wiped postgres');\n    } catch (err) {\n        console.log(`Error when wiping postgres: ${err}`);\n    } finally {\n        client.end();\n    }\n}\n\nfunction dbConfig(testInfo: TestInfo){\n    switch(testInfo.project.name) {\n        case \"postgres\":\n        case \"sso-postgres\":\n            return { DATABASE_URL: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@127.0.0.1:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}` };\n        case \"mariadb\":\n        case \"sso-mariadb\":\n            return { DATABASE_URL: `mysql://${process.env.MARIADB_USER}:${process.env.MARIADB_PASSWORD}@127.0.0.1:${process.env.MARIADB_PORT}/${process.env.MARIADB_DATABASE}` };\n        case \"mysql\":\n        case \"sso-mysql\":\n            return { DATABASE_URL: `mysql://${process.env.MYSQL_USER}:${process.env.MYSQL_PASSWORD}@127.0.0.1:${process.env.MYSQL_PORT}/${process.env.MYSQL_DATABASE}`};\n        case \"sqlite\":\n        case \"sso-sqlite\":\n            return { I_REALLY_WANT_VOLATILE_STORAGE: true };\n        default:\n            throw new Error(`Unknow database name: ${testInfo.project.name}`);\n    }\n}\n\n/**\n *  All parameters passed in `env` need to be added to the docker-compose.yml\n **/\nexport async function startVault(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {\n    if( resetDB ){\n        switch(testInfo.project.name) {\n            case \"postgres\":\n            case \"sso-postgres\":\n                await wipePostgres();\n                break;\n            case \"mariadb\":\n            case \"sso-mariadb\":\n                await wipeMariaDB();\n                break;\n            case \"mysql\":\n            case \"sso-mysql\":\n                await wipeMysqlDB();\n                break;\n            case \"sqlite\":\n            case \"sso-sqlite\":\n                wipeSqlite();\n                break;\n            default:\n                throw new Error(`Unknow database name: ${testInfo.project.name}`);\n        }\n    }\n\n    console.log(`Starting Vaultwarden`);\n    execSync(`docker compose --profile playwright --env-file test.env up -d Vaultwarden`, {\n        env: { ...env, ...dbConfig(testInfo) },\n    });\n    await waitFor(\"/\", browser);\n    console.log(`Vaultwarden running on: ${process.env.DOMAIN}`);\n}\n\nexport async function stopVault(force: boolean = false) {\n    if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === \"true\" ) {\n        console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);\n    } else {\n        console.log(`Vaultwarden stopping`);\n        execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);\n    }\n}\n\nexport async function restartVault(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {\n    stopVault(true);\n    return startVault(page.context().browser(), testInfo, env, resetDB);\n}\n\nexport async function checkNotification(page: Page, hasText: string) {\n    await expect(page.locator('bit-toast', { hasText })).toBeVisible();\n    try {\n        await page.locator('bit-toast', { hasText }).getByRole('button', { name: 'Close' }).click({force: true, timeout: 10_000});\n    } catch (error) {\n        console.log(`Closing notification failed but it should now be invisible (${error})`);\n    }\n    await expect(page.locator('bit-toast', { hasText })).toHaveCount(0);\n}\n\nexport async function cleanLanding(page: Page) {\n    await page.goto('/', { waitUntil: 'domcontentloaded' });\n    await expect(page.getByRole('button').nth(0)).toBeVisible();\n\n    const logged = await page.getByRole('button', { name: 'Log out' }).count();\n    if( logged > 0 ){\n        await page.getByRole('button', { name: 'Log out' }).click();\n        await page.getByRole('button', { name: 'Log out' }).click();\n    }\n}\n\nexport async function logout(test: Test, page: Page, user: { name: string }) {\n    await test.step('logout', async () => {\n        await page.getByRole('button', { name: user.name, exact: true }).click();\n        await page.getByRole('menuitem', { name: 'Log out' }).click();\n        await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();\n    });\n}\n\nexport async function ignoreExtension(page: Page) {\n    await page.waitForLoadState('domcontentloaded');\n\n    try {\n        await page.getByRole('button', { name: 'Add it later' }).click({timeout: 5_000});\n        await page.getByRole('link', { name: 'Skip to web app' }).click();\n    } catch (error) {\n        console.log('Extension setup not visible. Continuing');\n    }\n\n}\n"
  },
  {
    "path": "playwright/package.json",
    "content": "{\n    \"name\": \"scenarios\",\n    \"version\": \"1.0.0\",\n    \"description\": \"\",\n    \"main\": \"index.js\",\n    \"scripts\": {},\n    \"keywords\": [],\n    \"author\": \"\",\n    \"license\": \"ISC\",\n    \"devDependencies\": {\n        \"@playwright/test\": \"1.56.1\",\n        \"dotenv\": \"17.2.3\",\n        \"dotenv-expand\": \"12.0.3\",\n        \"maildev\": \"npm:@timshel_npm/maildev@3.2.5\"\n    },\n    \"dependencies\": {\n        \"mysql2\": \"3.15.3\",\n        \"otpauth\": \"9.4.1\",\n        \"pg\": \"8.16.3\"\n    }\n}\n"
  },
  {
    "path": "playwright/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\nimport { exec } from 'node:child_process';\n\nconst utils = require('./global-utils');\n\nutils.loadEnv();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n    testDir: './.',\n    /* Run tests in files in parallel */\n    fullyParallel: false,\n\n    /* Fail the build on CI if you accidentally left test.only in the source code. */\n    forbidOnly: !!process.env.CI,\n\n    retries: 0,\n    workers: 1,\n\n    /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n    reporter: 'html',\n\n    /* Long global timeout for complex tests\n     * But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).\n     */\n    timeout: 120 * 1000,\n    actionTimeout: 20 * 1000,\n    navigationTimeout: 20 * 1000,\n    expect: { timeout: 20 * 1000 },\n\n    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n    use: {\n        /* Base URL to use in actions like `await page.goto('/')`. */\n        baseURL: process.env.DOMAIN,\n        browserName: 'firefox',\n        locale: 'en-GB',\n        timezoneId: 'Europe/London',\n\n        /* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */\n        trace: 'on',\n        viewport: {\n            width: 1080,\n            height: 720,\n        },\n        video: \"on\",\n    },\n\n    /* Configure projects for major browsers */\n    projects: [\n        {\n            name: 'mariadb-setup',\n            testMatch: 'tests/setups/db-setup.ts',\n            use: { serviceName: \"Mariadb\" },\n            teardown: 'mariadb-teardown',\n        },\n        {\n            name: 'mysql-setup',\n            testMatch: 'tests/setups/db-setup.ts',\n            use: { serviceName: \"Mysql\" },\n            teardown: 'mysql-teardown',\n        },\n        {\n            name: 'postgres-setup',\n            testMatch: 'tests/setups/db-setup.ts',\n            use: { serviceName: \"Postgres\" },\n            teardown: 'postgres-teardown',\n        },\n        {\n            name: 'sso-setup',\n            testMatch: 'tests/setups/sso-setup.ts',\n            teardown: 'sso-teardown',\n        },\n\n        {\n            name: 'mariadb',\n            testMatch: 'tests/*.spec.ts',\n            testIgnore: 'tests/sso_*.spec.ts',\n            dependencies: ['mariadb-setup'],\n        },\n        {\n            name: 'mysql',\n            testMatch: 'tests/*.spec.ts',\n            testIgnore: 'tests/sso_*.spec.ts',\n            dependencies: ['mysql-setup'],\n        },\n        {\n            name: 'postgres',\n            testMatch: 'tests/*.spec.ts',\n            testIgnore: 'tests/sso_*.spec.ts',\n            dependencies: ['postgres-setup'],\n        },\n        {\n            name: 'sqlite',\n            testMatch: 'tests/*.spec.ts',\n            testIgnore: 'tests/sso_*.spec.ts',\n        },\n\n        {\n            name: 'sso-mariadb',\n            testMatch: 'tests/sso_*.spec.ts',\n            dependencies: ['sso-setup', 'mariadb-setup'],\n        },\n        {\n            name: 'sso-mysql',\n            testMatch: 'tests/sso_*.spec.ts',\n            dependencies: ['sso-setup', 'mysql-setup'],\n        },\n        {\n            name: 'sso-postgres',\n            testMatch: 'tests/sso_*.spec.ts',\n            dependencies: ['sso-setup', 'postgres-setup'],\n        },\n        {\n            name: 'sso-sqlite',\n            testMatch: 'tests/sso_*.spec.ts',\n            dependencies: ['sso-setup'],\n        },\n\n        {\n            name: 'mariadb-teardown',\n            testMatch: 'tests/setups/db-teardown.ts',\n            use: { serviceName: \"Mariadb\" },\n        },\n        {\n            name: 'mysql-teardown',\n            testMatch: 'tests/setups/db-teardown.ts',\n            use: { serviceName: \"Mysql\" },\n        },\n        {\n            name: 'postgres-teardown',\n            testMatch: 'tests/setups/db-teardown.ts',\n            use: { serviceName: \"Postgres\" },\n        },\n        {\n            name: 'sso-teardown',\n            testMatch: 'tests/setups/sso-teardown.ts',\n        },\n    ],\n\n    globalSetup: require.resolve('./global-setup'),\n});\n"
  },
  {
    "path": "playwright/test.env",
    "content": "##################################################################\n### Shared Playwright conf test file Vaultwarden and Databases ###\n##################################################################\n\nENV=test\nDC_ENV_FILE=test.env\nCOMPOSE_IGNORE_ORPHANS=True\nDOCKER_BUILDKIT=1\n\n#####################\n# Playwright Config #\n#####################\nPW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false}\nPW_SMTP_FROM=vaultwarden@playwright.test\n\n#####################\n# Maildev Config \t#\n#####################\nMAILDEV_HTTP_PORT=1081\nMAILDEV_SMTP_PORT=1026\nMAILDEV_HOST=127.0.0.1\n\n################\n# Users Config #\n################\nTEST_USER=test\nTEST_USER_PASSWORD=Master Password\nTEST_USER_MAIL=${TEST_USER}@example.com\n\nTEST_USER2=test2\nTEST_USER2_PASSWORD=Master Password\nTEST_USER2_MAIL=${TEST_USER2}@example.com\n\nTEST_USER3=test3\nTEST_USER3_PASSWORD=Master Password\nTEST_USER3_MAIL=${TEST_USER3}@example.com\n\n###################\n# Keycloak Config #\n###################\nKEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN}\nKC_HTTP_HOST=127.0.0.1\nKC_HTTP_PORT=8081\n\n# Script parameters (use Keycloak and Vaultwarden config too)\nTEST_REALM=test\nDUMMY_REALM=dummy\nDUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM}\n\n######################\n# Vaultwarden Config #\n######################\nROCKET_PORT=8003\nDOMAIN=http://localhost:${ROCKET_PORT}\nLOG_LEVEL=info,oidcwarden::sso=debug\nLOGIN_RATELIMIT_MAX_BURST=100\nADMIN_TOKEN=admin\n\nSMTP_SECURITY=off\nSMTP_PORT=${MAILDEV_SMTP_PORT}\nSMTP_FROM_NAME=Vaultwarden\nSMTP_TIMEOUT=5\n\nSSO_CLIENT_ID=warden\nSSO_CLIENT_SECRET=warden\nSSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}\nSSO_DEBUG_TOKENS=true\n\n# Custom web-vault build\n# PW_VW_REPO_URL=https://github.com/vaultwarden/vw_web_builds.git\n# PW_VW_COMMIT_HASH=b5f5b2157b9b64b5813bc334a75a277d0377b5d3\n\n###########################\n# Docker MariaDb container#\n###########################\nMARIADB_PORT=3307\nMARIADB_ROOT_PASSWORD=warden\nMARIADB_USER=warden\nMARIADB_PASSWORD=warden\nMARIADB_DATABASE=warden\n\n###########################\n# Docker Mysql container#\n###########################\nMYSQL_PORT=3309\nMYSQL_ROOT_PASSWORD=warden\nMYSQL_USER=warden\nMYSQL_PASSWORD=warden\nMYSQL_DATABASE=warden\n\n############################\n# Docker Postgres container#\n############################\nPOSTGRES_PORT=5433\nPOSTGRES_USER=warden\nPOSTGRES_PASSWORD=warden\nPOSTGRES_DB=warden\n"
  },
  {
    "path": "playwright/tests/collection.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\n\nimport * as utils from \"../global-utils\";\nimport { createAccount } from './setups/user';\n\nlet users = utils.loadEnv();\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    await utils.startVault(browser, testInfo);\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n});\n\ntest('Create', async ({ page }) => {\n    await createAccount(test, page, users.user1);\n\n    await test.step('Create Org', async () => {\n        await page.getByRole('link', { name: 'New organisation' }).click();\n        await page.getByLabel('Organisation name (required)').fill('Test');\n        await page.getByRole('button', { name: 'Submit' }).click();\n        await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();\n\n        await utils.checkNotification(page, 'Organisation created');\n    });\n\n    await test.step('Create Collection', async () => {\n        await page.getByRole('link', { name: 'Collections' }).click();\n        await page.getByRole('button', { name: 'New' }).click();\n        await page.getByRole('menuitem', { name: 'Collection' }).click();\n        await page.getByLabel('Name (required)').fill('RandomCollec');\n        await page.getByRole('button', { name: 'Save' }).click();\n        await utils.checkNotification(page, 'Created collection RandomCollec');\n        await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();\n    });\n});\n"
  },
  {
    "path": "playwright/tests/login.smtp.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nconst utils = require('../global-utils');\nimport { createAccount, logUser } from './setups/user';\nimport { activateEmail, retrieveEmailCode, disableEmail } from './setups/2fa';\n\nlet users = utils.loadEnv();\n\nlet mailserver;\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    mailserver = new MailDev({\n        port: process.env.MAILDEV_SMTP_PORT,\n        web: { port: process.env.MAILDEV_HTTP_PORT },\n    })\n\n    await mailserver.listen();\n\n    await utils.startVault(browser, testInfo, {\n        SMTP_HOST: process.env.MAILDEV_HOST,\n        SMTP_FROM: process.env.PW_SMTP_FROM,\n    });\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n    if( mailserver ){\n        await mailserver.close();\n    }\n});\n\ntest('Account creation', async ({ page }) => {\n    const mailBuffer = mailserver.buffer(users.user1.email);\n\n    await createAccount(test, page, users.user1, mailBuffer);\n\n    mailBuffer.close();\n});\n\ntest('Login', async ({ context, page }) => {\n    const mailBuffer = mailserver.buffer(users.user1.email);\n\n    await logUser(test, page, users.user1, mailBuffer);\n\n    await test.step('verify email', async () => {\n        await page.getByText('Verify your account\\'s email').click();\n        await expect(page.getByText('Verify your account\\'s email')).toBeVisible();\n        await page.getByRole('button', { name: 'Send email' }).click();\n\n        await utils.checkNotification(page, 'Check your email inbox for a verification link');\n\n        const verify = await mailBuffer.expect((m) => m.subject === \"Verify Your Email\");\n        expect(verify.from[0]?.address).toBe(process.env.PW_SMTP_FROM);\n\n        const page2 = await context.newPage();\n        await page2.setContent(verify.html);\n        const link = await page2.getByTestId(\"verify\").getAttribute(\"href\");\n        await page2.close();\n\n        await page.goto(link);\n        await utils.checkNotification(page, 'Account email verified');\n    });\n\n    mailBuffer.close();\n});\n\ntest('Activate 2fa', async ({ page }) => {\n    const emails = mailserver.buffer(users.user1.email);\n\n    await logUser(test, page, users.user1);\n\n    await activateEmail(test, page, users.user1, emails);\n\n    emails.close();\n});\n\ntest('2fa', async ({ page }) => {\n    const emails = mailserver.buffer(users.user1.email);\n\n    await test.step('login', async () => {\n        await page.goto('/');\n\n        await page.getByLabel(/Email address/).fill(users.user1.email);\n        await page.getByRole('button', { name: 'Continue' }).click();\n        await page.getByLabel('Master password').fill(users.user1.password);\n        await page.getByRole('button', { name: 'Log in with master password' }).click();\n\n        await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();\n        const code = await retrieveEmailCode(test, page, emails);\n        await page.getByLabel(/Verification code/).fill(code);\n        await page.getByRole('button', { name: 'Continue' }).click();\n\n        await page.getByRole('button', { name: 'Add it later' }).click();\n        await page.getByRole('link', { name: 'Skip to web app' }).click();\n\n        await expect(page).toHaveTitle(/Vaults/);\n    })\n\n    await disableEmail(test, page, users.user1);\n\n    emails.close();\n});\n"
  },
  {
    "path": "playwright/tests/login.spec.ts",
    "content": "import { test, expect, type Page, type TestInfo } from '@playwright/test';\nimport * as OTPAuth from \"otpauth\";\n\nimport * as utils from \"../global-utils\";\nimport { createAccount, logUser } from './setups/user';\nimport { activateTOTP, disableTOTP } from './setups/2fa';\n\nlet users = utils.loadEnv();\nlet totp;\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    await utils.startVault(browser, testInfo, {});\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n});\n\ntest('Account creation', async ({ page }) => {\n    await createAccount(test, page, users.user1);\n});\n\ntest('Master password login', async ({ page }) => {\n    await logUser(test, page, users.user1);\n});\n\ntest('Authenticator 2fa', async ({ page }) => {\n    await logUser(test, page, users.user1);\n\n    let totp = await activateTOTP(test, page, users.user1);\n\n    await utils.logout(test, page, users.user1);\n\n    await test.step('login', async () => {\n        let timestamp = Date.now(); // Needed to use the next token\n        timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;\n\n        await page.getByLabel(/Email address/).fill(users.user1.email);\n        await page.getByRole('button', { name: 'Continue' }).click();\n        await page.getByLabel('Master password').fill(users.user1.password);\n        await page.getByRole('button', { name: 'Log in with master password' }).click();\n\n        await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();\n        await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));\n        await page.getByRole('button', { name: 'Continue' }).click();\n\n        await expect(page).toHaveTitle(/Vaultwarden Web/);\n    });\n\n    await disableTOTP(test, page, users.user1);\n});\n"
  },
  {
    "path": "playwright/tests/organization.smtp.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nimport * as utils from '../global-utils';\nimport * as orgs from './setups/orgs';\nimport { createAccount, logUser } from './setups/user';\n\nlet users = utils.loadEnv();\n\nlet mailServer, mail1Buffer, mail2Buffer, mail3Buffer;\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    mailServer = new MailDev({\n        port: process.env.MAILDEV_SMTP_PORT,\n        web: { port: process.env.MAILDEV_HTTP_PORT },\n    })\n\n    await mailServer.listen();\n\n    await utils.startVault(browser, testInfo, {\n        SMTP_HOST: process.env.MAILDEV_HOST,\n        SMTP_FROM: process.env.PW_SMTP_FROM,\n    });\n\n    mail1Buffer = mailServer.buffer(users.user1.email);\n    mail2Buffer = mailServer.buffer(users.user2.email);\n    mail3Buffer = mailServer.buffer(users.user3.email);\n});\n\ntest.afterAll('Teardown', async ({}, testInfo: TestInfo) => {\n    utils.stopVault(testInfo);\n    [mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());\n});\n\ntest('Create user3', async ({ page }) => {\n    await createAccount(test, page, users.user3, mail3Buffer);\n});\n\ntest('Invite users', async ({ page }) => {\n    await createAccount(test, page, users.user1, mail1Buffer);\n\n    await orgs.create(test, page, 'Test');\n    await orgs.members(test, page, 'Test');\n    await orgs.invite(test, page, 'Test', users.user2.email);\n    await orgs.invite(test, page, 'Test', users.user3.email, {\n        navigate: false,\n    });\n});\n\ntest('invited with new account', async ({ page }) => {\n    const invited = await mail2Buffer.expect((mail) => mail.subject === 'Join Test');\n\n    await test.step('Create account', async () => {\n        await page.setContent(invited.html);\n        const link = await page.getByTestId('invite').getAttribute('href');\n        await page.goto(link);\n        await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);\n\n        //await page.getByLabel('Name').fill(users.user2.name);\n        await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);\n        await page.getByLabel('Confirm master password (').fill(users.user2.password);\n        await page.getByRole('button', { name: 'Create account' }).click();\n        await utils.checkNotification(page, 'Your new account has been created');\n\n        await utils.checkNotification(page, 'Invitation accepted');\n        await utils.ignoreExtension(page);\n\n        // Redirected to the vault\n        await expect(page).toHaveTitle('Vaults | Vaultwarden Web');\n        // await utils.checkNotification(page, 'You have been logged in!');\n    });\n\n    await test.step('Check mails', async () => {\n        await mail2Buffer.expect((m) => m.subject === 'Welcome');\n        await mail2Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');\n        await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));\n    });\n});\n\ntest('invited with existing account', async ({ page }) => {\n    const invited = await mail3Buffer.expect((mail) => mail.subject === 'Join Test');\n\n    await page.setContent(invited.html);\n    const link = await page.getByTestId('invite').getAttribute('href');\n\n    await page.goto(link);\n\n    // We should be on login page with email prefilled\n    await expect(page).toHaveTitle(/Vaultwarden Web/);\n    await page.getByRole('button', { name: 'Continue' }).click();\n\n    // Unlock page\n    await page.getByLabel('Master password').fill(users.user3.password);\n    await page.getByRole('button', { name: 'Log in with master password' }).click();\n\n    await utils.checkNotification(page, 'Invitation accepted');\n    await utils.ignoreExtension(page);\n\n    // We are now in the default vault page\n    await expect(page).toHaveTitle(/Vaultwarden Web/);\n\n    await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');\n    await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));\n});\n\ntest('Confirm invited user', async ({ page }) => {\n    await logUser(test, page, users.user1, mail1Buffer);\n\n    await orgs.members(test, page, 'Test');\n    await orgs.confirm(test, page, 'Test', users.user2.email);\n\n    await mail2Buffer.expect((m) => m.subject.includes('Invitation to Test confirmed'));\n});\n\ntest('Organization is visible', async ({ page }) => {\n    await logUser(test, page, users.user2, mail2Buffer);\n    await page.getByRole('button', { name: 'vault: Test', exact: true }).click();\n    await expect(page.getByLabel('Filter: Default collection')).toBeVisible();\n});\n"
  },
  {
    "path": "playwright/tests/organization.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nimport * as utils from \"../global-utils\";\nimport * as orgs from './setups/orgs';\nimport { createAccount, logUser } from './setups/user';\n\nlet users = utils.loadEnv();\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    await utils.startVault(browser, testInfo);\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n});\n\ntest('Invite', async ({ page }) => {\n    await createAccount(test, page, users.user3);\n    await createAccount(test, page, users.user1);\n\n    await orgs.create(test, page, 'New organisation');\n    await orgs.members(test, page, 'New organisation');\n\n    await test.step('missing user2', async () => {\n        await orgs.invite(test, page, 'New organisation', users.user2.email);\n        await expect(page.getByRole('row', { name: users.user2.email })).toHaveText(/Invited/);\n    });\n\n    await test.step('existing user3', async () => {\n        await orgs.invite(test, page, 'New organisation', users.user3.email);\n        await expect(page.getByRole('row', { name: users.user3.email })).toHaveText(/Needs confirmation/);\n        await orgs.confirm(test, page, 'New organisation', users.user3.email);\n    });\n\n    await test.step('confirm user2', async () => {\n        await createAccount(test, page, users.user2);\n        await logUser(test, page, users.user1);\n        await orgs.members(test, page, 'New organisation');\n        await orgs.confirm(test, page, 'New organisation', users.user2.email);\n    });\n\n    await test.step('Org visible user2  ', async () => {\n        await logUser(test, page, users.user2);\n        await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();\n        await expect(page.getByLabel('Filter: Default collection')).toBeVisible();\n    });\n\n    await test.step('Org visible user3  ', async () => {\n        await logUser(test, page, users.user3);\n        await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();\n        await expect(page.getByLabel('Filter: Default collection')).toBeVisible();\n    });\n});\n"
  },
  {
    "path": "playwright/tests/setups/2fa.ts",
    "content": "import { expect, type Page, Test } from '@playwright/test';\nimport { type MailBuffer } from 'maildev';\nimport * as OTPAuth from \"otpauth\";\n\nimport * as utils from '../../global-utils';\n\nexport async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {\n    return await test.step('Activate TOTP 2FA', async () => {\n        await page.getByRole('button', { name: user.name }).click();\n        await page.getByRole('menuitem', { name: 'Account settings' }).click();\n        await page.getByRole('link', { name: 'Security' }).click();\n        await page.getByRole('link', { name: 'Two-step login' }).click();\n        await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();\n        await page.getByLabel('Master password (required)').fill(user.password);\n        await page.getByRole('button', { name: 'Continue' }).click();\n\n        const secret = await page.getByLabel('Key').innerText();\n        let totp = new OTPAuth.TOTP({ secret, period: 30 });\n\n        await page.getByLabel(/Verification code/).fill(totp.generate());\n        await page.getByRole('button', { name: 'Turn on' }).click();\n        await page.getByRole('heading', { name: 'Turned on', exact: true });\n        await page.getByLabel('Close').click();\n\n        return totp;\n    })\n}\n\nexport async function disableTOTP(test: Test, page: Page, user: { password: string }) {\n    await test.step('Disable TOTP 2FA', async () => {\n        await page.getByRole('button', { name: 'Test' }).click();\n        await page.getByRole('menuitem', { name: 'Account settings' }).click();\n        await page.getByRole('link', { name: 'Security' }).click();\n        await page.getByRole('link', { name: 'Two-step login' }).click();\n        await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();\n        await page.getByLabel('Master password (required)').click();\n        await page.getByLabel('Master password (required)').fill(user.password);\n        await page.getByRole('button', { name: 'Continue' }).click();\n        await page.getByRole('button', { name: 'Turn off' }).click();\n        await page.getByRole('button', { name: 'Yes' }).click();\n        await utils.checkNotification(page, 'Two-step login provider turned off');\n    });\n}\n\nexport async function activateEmail(test: Test, page: Page, user: { name: string, password: string }, mailBuffer: MailBuffer) {\n    await test.step('Activate Email 2FA', async () => {\n        await page.getByRole('button', { name: user.name }).click();\n        await page.getByRole('menuitem', { name: 'Account settings' }).click();\n        await page.getByRole('link', { name: 'Security' }).click();\n        await page.getByRole('link', { name: 'Two-step login' }).click();\n        await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click();\n        await page.getByLabel('Master password (required)').fill(user.password);\n        await page.getByRole('button', { name: 'Continue' }).click();\n        await page.getByRole('button', { name: 'Send email' }).click();\n    });\n\n    let code = await retrieveEmailCode(test, page, mailBuffer);\n\n    await test.step('input code', async () => {\n        await page.getByLabel('2. Enter the resulting 6').fill(code);\n        await page.getByRole('button', { name: 'Turn on' }).click();\n        await page.getByRole('heading', { name: 'Turned on', exact: true });\n    });\n}\n\nexport async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): string {\n    return await test.step('retrieve code', async () => {\n        const codeMail = await mailBuffer.expect((mail) => mail.subject.includes(\"Login Verification Code\"));\n        const page2 = await page.context().newPage();\n        await page2.setContent(codeMail.html);\n        const code = await page2.getByTestId(\"2fa\").innerText();\n        await page2.close();\n        return code;\n    });\n}\n\nexport async function disableEmail(test: Test, page: Page, user: { password: string }) {\n    await test.step('Disable Email 2FA', async () => {\n        await page.getByRole('button', { name: 'Test' }).click();\n        await page.getByRole('menuitem', { name: 'Account settings' }).click();\n        await page.getByRole('link', { name: 'Security' }).click();\n        await page.getByRole('link', { name: 'Two-step login' }).click();\n        await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();\n        await page.getByLabel('Master password (required)').click();\n        await page.getByLabel('Master password (required)').fill(user.password);\n        await page.getByRole('button', { name: 'Continue' }).click();\n        await page.getByRole('button', { name: 'Turn off' }).click();\n        await page.getByRole('button', { name: 'Yes' }).click();\n\n        await utils.checkNotification(page, 'Two-step login provider turned off');\n    });\n}\n"
  },
  {
    "path": "playwright/tests/setups/db-setup.ts",
    "content": "import { test } from './db-test';\n\nconst utils = require('../../global-utils');\n\ntest('DB start', async ({ serviceName }) => {\n\tutils.startComposeService(serviceName);\n});\n"
  },
  {
    "path": "playwright/tests/setups/db-teardown.ts",
    "content": "import { test } from './db-test';\n\nconst utils = require('../../global-utils');\n\nutils.loadEnv();\n\ntest('DB teardown ?', async ({ serviceName }) => {\n    if( process.env.PW_KEEP_SERVICE_RUNNNING !== \"true\" ) {\n        utils.stopComposeService(serviceName);\n    }\n});\n"
  },
  {
    "path": "playwright/tests/setups/db-test.ts",
    "content": "import { test as base } from '@playwright/test';\n\nexport type TestOptions = {\n  serviceName: string;\n};\n\nexport const test = base.extend<TestOptions>({\n  serviceName: ['', { option: true }],\n});\n"
  },
  {
    "path": "playwright/tests/setups/orgs.ts",
    "content": "import { expect, type Browser,Page } from '@playwright/test';\n\nimport * as utils from '../../global-utils';\n\nexport async function create(test, page: Page, name: string) {\n    await test.step('Create Org', async () => {\n        await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();\n        await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();\n        await page.getByRole('link', { name: 'New organisation' }).click();\n        await page.getByLabel('Organisation name (required)').fill(name);\n        await page.getByRole('button', { name: 'Submit' }).click();\n\n        await utils.checkNotification(page, 'Organisation created');\n    });\n}\n\nexport async function policies(test, page: Page, name: string) {\n    await test.step(`Navigate to ${name} policies`, async () => {\n        await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();\n        await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();\n        await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();\n        await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();\n        await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();\n        await page.getByRole('link', { name: 'Policies' }).click();\n        await expect(page.getByRole('heading', { name: 'Policies' })).toBeVisible();\n    });\n}\n\nexport async function members(test, page: Page, name: string) {\n    await test.step(`Navigate to ${name} members`, async () => {\n        await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();\n        await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();\n        await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();\n        await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();\n        await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();\n        await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();\n        await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();\n    });\n}\n\nexport async function invite(test, page: Page, name: string, email: string) {\n    await test.step(`Invite ${email}`, async () => {\n        await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();\n        await page.getByRole('button', { name: 'Invite member' }).click();\n        await page.getByLabel('Email (required)').fill(email);\n        await page.getByRole('tab', { name: 'Collections' }).click();\n        await page.getByRole('combobox', { name: 'Permission' }).click();\n        await page.getByText('Edit items', { exact: true }).click();\n        await page.getByLabel('Select collections').click();\n        await page.getByText('Default collection').click();\n        await page.getByRole('cell', { name: 'Collection', exact: true }).click();\n        await page.getByRole('button', { name: 'Save' }).click();\n        await utils.checkNotification(page, 'User(s) invited');\n    });\n}\n\nexport async function confirm(test, page: Page, name: string, user_email: string) {\n    await test.step(`Confirm ${user_email}`, async () => {\n        await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();\n        await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();\n        await page.getByRole('menuitem', { name: 'Confirm' }).click();\n        await expect(page.getByRole('heading', { name: 'Confirm user' })).toBeVisible();\n        await page.getByRole('button', { name: 'Confirm' }).click();\n        await utils.checkNotification(page, 'confirmed');\n    });\n}\n\nexport async function revoke(test, page: Page, name: string, user_email: string) {\n    await test.step(`Revoke ${user_email}`, async () => {\n        await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();\n        await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();\n        await page.getByRole('menuitem', { name: 'Revoke access' }).click();\n        await expect(page.getByRole('heading', { name: 'Revoke access' })).toBeVisible();\n        await page.getByRole('button', { name: 'Revoke access' }).click();\n        await utils.checkNotification(page, 'Revoked organisation access');\n    });\n}\n"
  },
  {
    "path": "playwright/tests/setups/sso-setup.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\n\nconst { exec } = require('node:child_process');\nconst utils = require('../../global-utils');\n\nutils.loadEnv();\n\ntest.beforeAll('Setup', async () => {\n    console.log(\"Starting Keycloak\");\n    exec(`docker compose --profile keycloak --env-file test.env up`);\n});\n\ntest('Keycloak is up', async ({ page }) => {\n    await utils.waitFor(process.env.SSO_AUTHORITY, page.context().browser());\n    // Dummy authority is created at the end of the setup\n    await utils.waitFor(process.env.DUMMY_AUTHORITY, page.context().browser());\n    console.log(`Keycloak running on: ${process.env.SSO_AUTHORITY}`);\n});\n"
  },
  {
    "path": "playwright/tests/setups/sso-teardown.ts",
    "content": "import { test, type FullConfig } from '@playwright/test';\n\nconst { execSync } = require('node:child_process');\nconst utils = require('../../global-utils');\n\nutils.loadEnv();\n\ntest('Keycloak teardown', async () => {\n    if( process.env.PW_KEEP_SERVICE_RUNNNING === \"true\" ) {\n        console.log(\"Keep Keycloak running\");\n    } else {\n        console.log(\"Keycloak stopping\");\n        execSync(`docker compose --profile keycloak --env-file test.env stop Keycloak`);\n    }\n});\n"
  },
  {
    "path": "playwright/tests/setups/sso.ts",
    "content": "import { expect, type Page, Test } from '@playwright/test';\nimport { type MailBuffer, MailServer } from 'maildev';\nimport * as OTPAuth from \"otpauth\";\n\nimport * as utils from '../../global-utils';\nimport { retrieveEmailCode } from './2fa';\n\n/**\n * If a MailBuffer is passed it will be used and consume the expected emails\n */\nexport async function logNewUser(\n    test: Test,\n    page: Page,\n    user: { email: string, name: string, password: string },\n    options: { mailBuffer?: MailBuffer } = {}\n) {\n    await test.step(`Create user ${user.name}`, async () => {\n        await page.context().clearCookies();\n\n        await test.step('Landing page', async () => {\n            await utils.cleanLanding(page);\n\n            await page.locator(\"input[type=email].vw-email-sso\").fill(user.email);\n            await page.getByRole('button', { name: /Use single sign-on/ }).click();\n        });\n\n        await test.step('Keycloak login', async () => {\n            await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();\n            await page.getByLabel(/Username/).fill(user.name);\n            await page.getByLabel('Password', { exact: true }).fill(user.password);\n            await page.getByRole('button', { name: 'Sign In' }).click();\n        });\n\n        await test.step('Create Vault account', async () => {\n            await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();\n            await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);\n            await page.getByLabel('Confirm master password (').fill(user.password);\n            await page.getByRole('button', { name: 'Create account' }).click();\n        });\n\n        await utils.checkNotification(page, 'Account successfully created!');\n        await utils.checkNotification(page, 'Invitation accepted');\n\n        await utils.ignoreExtension(page);\n\n        await test.step('Default vault page', async () => {\n            await expect(page).toHaveTitle(/Vaultwarden Web/);\n            await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();\n        });\n\n        if( options.mailBuffer ){\n            let mailBuffer = options.mailBuffer;\n            await test.step('Check emails', async () => {\n                await mailBuffer.expect((m) => m.subject === \"Welcome\");\n                await mailBuffer.expect((m) => m.subject.includes(\"New Device Logged\"));\n            });\n        }\n    });\n}\n\n/**\n * If a MailBuffer is passed it will be used and consume the expected emails\n */\nexport async function logUser(\n    test: Test,\n    page: Page,\n    user: { email: string, password: string },\n    options: {\n        mailBuffer ?: MailBuffer,\n        totp?: OTPAuth.TOTP,\n        mail2fa?: boolean,\n    } = {}\n) {\n    let mailBuffer = options.mailBuffer;\n\n    await test.step(`Log user ${user.email}`, async () => {\n        await page.context().clearCookies();\n\n        await test.step('Landing page', async () => {\n            await utils.cleanLanding(page);\n\n            await page.locator(\"input[type=email].vw-email-sso\").fill(user.email);\n            await page.getByRole('button', { name: /Use single sign-on/ }).click();\n        });\n\n        await test.step('Keycloak login', async () => {\n            await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();\n            await page.getByLabel(/Username/).fill(user.name);\n            await page.getByLabel('Password', { exact: true }).fill(user.password);\n            await page.getByRole('button', { name: 'Sign In' }).click();\n        });\n\n        if( options.totp || options.mail2fa ){\n            let code;\n\n            await test.step('2FA check', async () => {\n                await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();\n\n                if( options.totp ) {\n                    const totp = options.totp;\n                    let timestamp = Date.now(); // Needed to use the next token\n                    timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;\n                    code = totp.generate({timestamp});\n                } else if( options.mail2fa ){\n                    code = await retrieveEmailCode(test, page, mailBuffer);\n                }\n\n                await page.getByLabel(/Verification code/).fill(code);\n                await page.getByRole('button', { name: 'Continue' }).click();\n            });\n        }\n\n        await test.step('Unlock vault', async () => {\n            await expect(page).toHaveTitle('Vaultwarden Web');\n            await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible();\n            await page.getByLabel('Master password').fill(user.password);\n            await page.getByRole('button', { name: 'Unlock' }).click();\n        });\n\n        await utils.ignoreExtension(page);\n\n        await test.step('Default vault page', async () => {\n            await expect(page).toHaveTitle(/Vaultwarden Web/);\n            await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();\n        });\n\n        if( mailBuffer ){\n            await test.step('Check email', async () => {\n                await mailBuffer.expect((m) => m.subject.includes(\"New Device Logged\"));\n            });\n        }\n    });\n}\n"
  },
  {
    "path": "playwright/tests/setups/user.ts",
    "content": "import { expect, type Browser, Page } from '@playwright/test';\n\nimport { type MailBuffer } from 'maildev';\n\nimport * as utils from '../../global-utils';\n\nexport async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {\n    await test.step(`Create user ${user.name}`, async () => {\n        await utils.cleanLanding(page);\n\n        await page.getByRole('link', { name: 'Create account' }).click();\n\n        // Back to Vault create account\n        await expect(page).toHaveTitle(/Create account | Vaultwarden Web/);\n        await page.getByLabel(/Email address/).fill(user.email);\n        await page.getByLabel('Name').fill(user.name);\n        await page.getByRole('button', { name: 'Continue' }).click();\n\n        // Vault finish Creation\n        await page.getByLabel('Master password (required)', { exact: true }).fill(user.password);\n        await page.getByLabel('Confirm master password (').fill(user.password);\n        await page.getByRole('button', { name: 'Create account' }).click();\n\n        await utils.checkNotification(page, 'Your new account has been created')\n        await utils.ignoreExtension(page);\n\n        // We are now in the default vault page\n        await expect(page).toHaveTitle('Vaults | Vaultwarden Web');\n        // await utils.checkNotification(page, 'You have been logged in!');\n\n        if( mailBuffer ){\n            await mailBuffer.expect((m) => m.subject === \"Welcome\");\n            await mailBuffer.expect((m) => m.subject === \"New Device Logged In From Firefox\");\n        }\n    });\n}\n\nexport async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {\n    await test.step(`Log user ${user.email}`, async () => {\n        await utils.cleanLanding(page);\n\n        await page.getByLabel(/Email address/).fill(user.email);\n        await page.getByRole('button', { name: 'Continue' }).click();\n\n        // Unlock page\n        await page.getByLabel('Master password').fill(user.password);\n        await page.getByRole('button', { name: 'Log in with master password' }).click();\n\n        await utils.ignoreExtension(page);\n\n        // We are now in the default vault page\n        await expect(page).toHaveTitle(/Vaultwarden Web/);\n\n        if( mailBuffer ){\n            await mailBuffer.expect((m) => m.subject === \"New Device Logged In From Firefox\");\n        }\n    });\n}\n"
  },
  {
    "path": "playwright/tests/sso_login.smtp.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nimport { logNewUser, logUser } from './setups/sso';\nimport { activateEmail, disableEmail } from './setups/2fa';\nimport * as utils from \"../global-utils\";\n\nlet users = utils.loadEnv();\n\nlet mailserver;\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    mailserver = new MailDev({\n        port: process.env.MAILDEV_SMTP_PORT,\n        web: { port: process.env.MAILDEV_HTTP_PORT },\n    })\n\n    await mailserver.listen();\n\n    await utils.startVault(browser, testInfo, {\n        SSO_ENABLED: true,\n        SSO_ONLY: false,\n        SMTP_HOST: process.env.MAILDEV_HOST,\n        SMTP_FROM: process.env.PW_SMTP_FROM,\n    });\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n    if( mailserver ){\n        await mailserver.close();\n    }\n});\n\ntest('Create and activate 2FA', async ({ page }) => {\n    const mailBuffer = mailserver.buffer(users.user1.email);\n\n    await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});\n\n    await activateEmail(test, page, users.user1, mailBuffer);\n\n    mailBuffer.close();\n});\n\ntest('Log and disable', async ({ page }) => {\n    const mailBuffer = mailserver.buffer(users.user1.email);\n\n    await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true});\n\n    await disableEmail(test, page, users.user1);\n\n    mailBuffer.close();\n});\n"
  },
  {
    "path": "playwright/tests/sso_login.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\n\nimport { logNewUser, logUser } from './setups/sso';\nimport { activateTOTP, disableTOTP } from './setups/2fa';\nimport * as utils from \"../global-utils\";\n\nlet users = utils.loadEnv();\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    await utils.startVault(browser, testInfo, {\n        SSO_ENABLED: true,\n        SSO_ONLY: false\n    });\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n});\n\ntest('Account creation using SSO', async ({ page }) => {\n    // Landing page\n    await logNewUser(test, page, users.user1);\n});\n\ntest('SSO login', async ({ page }) => {\n    await logUser(test, page, users.user1);\n});\n\ntest('Non SSO login', async ({ page }) => {\n    // Landing page\n    await page.goto('/');\n    await page.locator(\"input[type=email].vw-email-sso\").fill(users.user1.email);\n    await page.getByRole('button', { name: 'Other' }).click();\n\n    // Unlock page\n    await page.getByLabel('Master password').fill(users.user1.password);\n    await page.getByRole('button', { name: 'Log in with master password' }).click();\n\n    // We are now in the default vault page\n    await expect(page).toHaveTitle(/Vaultwarden Web/);\n});\n\ntest('SSO login with TOTP 2fa', async ({ page }) => {\n    await logUser(test, page, users.user1);\n\n    let totp = await activateTOTP(test, page, users.user1);\n\n    await logUser(test, page, users.user1, { totp });\n\n    await disableTOTP(test, page, users.user1);\n});\n\ntest('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) => {\n    await utils.restartVault(page, testInfo, {\n        SSO_ENABLED: true,\n        SSO_ONLY: true\n    }, false);\n\n    // Landing page\n    await page.goto('/');\n\n    // Check that SSO login is available\n    await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);\n\n    // No Continue/Other\n    await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0);\n});\n\n\ntest('No SSO login', async ({ page }, testInfo: TestInfo) => {\n    await utils.restartVault(page, testInfo, {\n        SSO_ENABLED: false\n    }, false);\n\n    // Landing page\n    await page.goto('/');\n\n    // No SSO button (rely on a correct selector checked in previous test)\n    await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);\n\n    // Can continue to Master password\n    await page.getByLabel(/Email address/).fill(users.user1.email);\n    await page.getByRole('button', { name: 'Continue' }).click();\n    await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1);\n});\n"
  },
  {
    "path": "playwright/tests/sso_organization.smtp.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nimport * as utils from \"../global-utils\";\nimport * as orgs from './setups/orgs';\nimport { logNewUser, logUser } from './setups/sso';\n\nlet users = utils.loadEnv();\n\nlet mailServer, mail1Buffer, mail2Buffer, mail3Buffer;\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    mailServer = new MailDev({\n        port: process.env.MAILDEV_SMTP_PORT,\n        web: { port: process.env.MAILDEV_HTTP_PORT },\n    })\n\n    await mailServer.listen();\n\n    await utils.startVault(browser, testInfo, {\n        SMTP_HOST: process.env.MAILDEV_HOST,\n        SMTP_FROM: process.env.PW_SMTP_FROM,\n        SSO_ENABLED: true,\n        SSO_ONLY: true,\n    });\n\n    mail1Buffer = mailServer.buffer(users.user1.email);\n    mail2Buffer = mailServer.buffer(users.user2.email);\n    mail3Buffer = mailServer.buffer(users.user3.email);\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n    [mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());\n});\n\ntest('Create user3', async ({ page }) => {\n    await logNewUser(test, page, users.user3, { mailBuffer: mail3Buffer });\n});\n\ntest('Invite users', async ({ page }) => {\n    await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer });\n\n    await orgs.create(test, page, '/Test');\n    await orgs.members(test, page, '/Test');\n    await orgs.invite(test, page, '/Test', users.user2.email);\n    await orgs.invite(test, page, '/Test', users.user3.email);\n});\n\ntest('invited with new account', async ({ page }) => {\n    const link = await test.step('Extract email link', async () => {\n        const invited = await mail2Buffer.expect((m) => m.subject === \"Join /Test\");\n        await page.setContent(invited.html);\n        return await page.getByTestId(\"invite\").getAttribute(\"href\");\n    });\n\n    await test.step('Redirect to Keycloak', async () => {\n        await page.goto(link);\n    });\n\n    await test.step('Keycloak login', async () => {\n        await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();\n        await page.getByLabel(/Username/).fill(users.user2.name);\n        await page.getByLabel('Password', { exact: true }).fill(users.user2.password);\n        await page.getByRole('button', { name: 'Sign In' }).click();\n    });\n\n    await test.step('Create Vault account', async () => {\n        await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();\n        await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password);\n        await page.getByLabel('Confirm master password (').fill(users.user2.password);\n        await page.getByRole('button', { name: 'Create account' }).click();\n\n        await utils.checkNotification(page, 'Account successfully created!');\n        await utils.checkNotification(page, 'Invitation accepted');\n        await utils.ignoreExtension(page);\n    });\n\n    await test.step('Default vault page', async () => {\n        await expect(page).toHaveTitle(/Vaultwarden Web/);\n    });\n\n    await test.step('Check mails', async () => {\n        await mail2Buffer.expect((m) => m.subject.includes(\"New Device Logged\"));\n        await mail1Buffer.expect((m) => m.subject === \"Invitation to /Test accepted\");\n    });\n});\n\ntest('invited with existing account', async ({ page }) => {\n    const link = await test.step('Extract email link', async () => {\n        const invited = await mail3Buffer.expect((m) => m.subject === \"Join /Test\");\n        await page.setContent(invited.html);\n        return await page.getByTestId(\"invite\").getAttribute(\"href\");\n    });\n\n    await test.step('Redirect to Keycloak', async () => {\n        await page.goto(link);\n    });\n\n    await test.step('Keycloak login', async () => {\n        await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();\n        await page.getByLabel(/Username/).fill(users.user3.name);\n        await page.getByLabel('Password', { exact: true }).fill(users.user3.password);\n        await page.getByRole('button', { name: 'Sign In' }).click();\n    });\n\n    await test.step('Unlock vault', async () => {\n        await expect(page).toHaveTitle('Vaultwarden Web');\n        await page.getByLabel('Master password').fill(users.user3.password);\n        await page.getByRole('button', { name: 'Unlock' }).click();\n\n        await utils.checkNotification(page, 'Invitation accepted');\n        await utils.ignoreExtension(page);\n    });\n\n    await test.step('Default vault page', async () => {\n        await expect(page).toHaveTitle(/Vaultwarden Web/);\n    });\n\n    await test.step('Check mails', async () => {\n        await mail3Buffer.expect((m) => m.subject.includes(\"New Device Logged\"));\n        await mail1Buffer.expect((m) => m.subject === \"Invitation to /Test accepted\");\n    });\n});\n"
  },
  {
    "path": "playwright/tests/sso_organization.spec.ts",
    "content": "import { test, expect, type TestInfo } from '@playwright/test';\nimport { MailDev } from 'maildev';\n\nimport * as utils from \"../global-utils\";\nimport * as orgs from './setups/orgs';\nimport { logNewUser, logUser } from './setups/sso';\n\nlet users = utils.loadEnv();\n\ntest.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {\n    await utils.startVault(browser, testInfo, {\n        SSO_ENABLED: true,\n        SSO_ONLY: true,\n    });\n});\n\ntest.afterAll('Teardown', async ({}) => {\n    utils.stopVault();\n});\n\ntest('Create user3', async ({ page }) => {\n    await logNewUser(test, page, users.user3);\n});\n\ntest('Invite users', async ({ page }) => {\n    await logNewUser(test, page, users.user1);\n\n    await orgs.create(test, page, '/Test');\n    await orgs.members(test, page, '/Test');\n    await orgs.invite(test, page, '/Test', users.user2.email);\n    await orgs.invite(test, page, '/Test', users.user3.email);\n    await orgs.confirm(test, page, '/Test', users.user3.email);\n});\n\ntest('Create invited account', async ({ page }) => {\n    await logNewUser(test, page, users.user2);\n});\n\ntest('Confirm invited user', async ({ page }) => {\n    await logUser(test, page, users.user1);\n    await orgs.members(test, page, '/Test');\n    await expect(page.getByRole('row', { name: users.user2.name })).toHaveText(/Needs confirmation/);\n    await orgs.confirm(test, page, '/Test', users.user2.email);\n});\n\ntest('Organization is visible', async ({ page }) => {\n    await logUser(test, page, users.user2);\n    await page.getByLabel('vault: /Test').click();\n    await expect(page.getByLabel('Filter: Default collection')).toBeVisible();\n});\n\ntest('Enforce password policy', async ({ page }) => {\n    await logUser(test, page, users.user1);\n    await orgs.policies(test, page, '/Test');\n\n    await test.step(`Set master password policy`, async () => {\n        await page.getByRole('button', { name: 'Master password requirements' }).click();\n        await page.getByRole('checkbox', { name: 'Turn on' }).check();\n        await page.getByRole('checkbox', { name: 'Require existing members to' }).check();\n        await page.getByRole('spinbutton', { name: 'Minimum length' }).fill('42');\n        await page.getByRole('button', { name: 'Save' }).click();\n        await utils.checkNotification(page, 'Edited policy Master password requirements.');\n    });\n\n    await utils.logout(test, page, users.user1);\n\n    await test.step(`Unlock trigger policy`, async () => {\n        await page.locator(\"input[type=email].vw-email-sso\").fill(users.user1.email);\n        await page.getByRole('button', { name: 'Use single sign-on' }).click();\n\n        await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);\n        await page.getByRole('button', { name: 'Unlock' }).click();\n\n        await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();\n    });\n});\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.94.0\"\ncomponents = [ \"rustfmt\", \"clippy\" ]\nprofile = \"minimal\"\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2021\"\nmax_width = 120\nnewline_style = \"Unix\"\nuse_small_heuristics = \"Off\"\n"
  },
  {
    "path": "src/api/admin.rs",
    "content": "use std::{env, sync::LazyLock};\n\nuse reqwest::Method;\nuse rocket::{\n    form::Form,\n    http::{Cookie, CookieJar, MediaType, SameSite, Status},\n    request::{FromRequest, Outcome, Request},\n    response::{content::RawHtml as Html, Redirect},\n    serde::json::Json,\n    Catcher, Route,\n};\nuse serde::de::DeserializeOwned;\nuse serde_json::Value;\n\nuse crate::{\n    api::{\n        core::{log_event, two_factor},\n        unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,\n    },\n    auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure},\n    config::ConfigBuilder,\n    db::{\n        backup_sqlite, get_sql_server_version,\n        models::{\n            Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,\n            MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,\n        },\n        DbConn, DbConnType, ACTIVE_DB_TYPE,\n    },\n    error::{Error, MapResult},\n    http_client::make_http_request,\n    mail,\n    util::{\n        container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size,\n        is_running_in_container, NumberOrString,\n    },\n    CONFIG, VERSION,\n};\n\npub fn routes() -> Vec<Route> {\n    if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {\n        return routes![admin_disabled];\n    }\n\n    routes![\n        get_users_json,\n        get_user_json,\n        get_user_by_mail_json,\n        post_admin_login,\n        admin_page,\n        admin_page_login,\n        invite_user,\n        logout,\n        delete_user,\n        delete_sso_user,\n        deauth_user,\n        disable_user,\n        enable_user,\n        remove_2fa,\n        update_membership_type,\n        update_revision_users,\n        post_config,\n        delete_config,\n        backup_db,\n        test_smtp,\n        users_overview,\n        organizations_overview,\n        delete_organization,\n        diagnostics,\n        get_diagnostics_config,\n        resend_user_invite,\n        get_diagnostics_http,\n    ]\n}\n\npub fn catchers() -> Vec<Catcher> {\n    if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {\n        catchers![]\n    } else {\n        catchers![admin_login]\n    }\n}\n\nstatic DB_TYPE: LazyLock<&str> = LazyLock::new(|| match ACTIVE_DB_TYPE.get() {\n    #[cfg(mysql)]\n    Some(DbConnType::Mysql) => \"MySQL\",\n    #[cfg(postgresql)]\n    Some(DbConnType::Postgresql) => \"PostgreSQL\",\n    #[cfg(sqlite)]\n    Some(DbConnType::Sqlite) => \"SQLite\",\n    _ => \"Unknown\",\n});\n\n#[cfg(sqlite)]\nstatic CAN_BACKUP: LazyLock<bool> =\n    LazyLock::new(|| ACTIVE_DB_TYPE.get().map(|t| *t == DbConnType::Sqlite).unwrap_or(false));\n#[cfg(not(sqlite))]\nstatic CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);\n\n#[get(\"/\")]\nfn admin_disabled() -> &'static str {\n    \"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it\"\n}\n\nconst COOKIE_NAME: &str = \"VW_ADMIN\";\nconst ADMIN_PATH: &str = \"/admin\";\nconst DT_FMT: &str = \"%Y-%m-%d %H:%M:%S %Z\";\n\nconst BASE_TEMPLATE: &str = \"admin/base\";\n\nconst ACTING_ADMIN_USER: &str = \"vaultwarden-admin-00000-000000000000\";\npub const FAKE_ADMIN_UUID: &str = \"00000000-0000-0000-0000-000000000000\";\n\nfn admin_path() -> String {\n    format!(\"{}{ADMIN_PATH}\", CONFIG.domain_path())\n}\n\n#[derive(Debug)]\nstruct IpHeader(Option<String>);\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for IpHeader {\n    type Error = ();\n\n    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        if req.headers().get_one(&CONFIG.ip_header()).is_some() {\n            Outcome::Success(IpHeader(Some(CONFIG.ip_header())))\n        } else if req.headers().get_one(\"X-Client-IP\").is_some() {\n            Outcome::Success(IpHeader(Some(String::from(\"X-Client-IP\"))))\n        } else if req.headers().get_one(\"X-Real-IP\").is_some() {\n            Outcome::Success(IpHeader(Some(String::from(\"X-Real-IP\"))))\n        } else if req.headers().get_one(\"X-Forwarded-For\").is_some() {\n            Outcome::Success(IpHeader(Some(String::from(\"X-Forwarded-For\"))))\n        } else {\n            Outcome::Success(IpHeader(None))\n        }\n    }\n}\n\nfn admin_url() -> String {\n    format!(\"{}{}\", CONFIG.domain_origin(), admin_path())\n}\n\n#[derive(Responder)]\nenum AdminResponse {\n    #[response(status = 200)]\n    Ok(ApiResult<Html<String>>),\n    #[response(status = 401)]\n    Unauthorized(ApiResult<Html<String>>),\n    #[response(status = 429)]\n    TooManyRequests(ApiResult<Html<String>>),\n}\n\n#[catch(401)]\nfn admin_login(request: &Request<'_>) -> ApiResult<Html<String>> {\n    if request.format() == Some(&MediaType::JSON) {\n        err_code!(\"Authorization failed.\", Status::Unauthorized.code);\n    }\n    let redirect = request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();\n    render_admin_login(None, Some(&redirect))\n}\n\nfn render_admin_login(msg: Option<&str>, redirect: Option<&str>) -> ApiResult<Html<String>> {\n    // If there is an error, show it\n    let msg = msg.map(|msg| format!(\"Error: {msg}\"));\n    let json = json!({\n        \"page_content\": \"admin/login\",\n        \"error\": msg,\n        \"redirect\": redirect,\n        \"urlpath\": CONFIG.domain_path()\n    });\n\n    // Return the page\n    let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;\n    Ok(Html(text))\n}\n\n#[derive(FromForm)]\nstruct LoginForm {\n    token: String,\n    redirect: Option<String>,\n}\n\n#[post(\"/\", format = \"application/x-www-form-urlencoded\", data = \"<data>\")]\nfn post_admin_login(\n    data: Form<LoginForm>,\n    cookies: &CookieJar<'_>,\n    ip: ClientIp,\n    secure: Secure,\n) -> Result<Redirect, AdminResponse> {\n    let data = data.into_inner();\n    let redirect = data.redirect;\n\n    if crate::ratelimit::check_limit_admin(&ip.ip).is_err() {\n        return Err(AdminResponse::TooManyRequests(render_admin_login(\n            Some(\"Too many requests, try again later.\"),\n            redirect.as_deref(),\n        )));\n    }\n\n    // If the token is invalid, redirect to login page\n    if !_validate_token(&data.token) {\n        error!(\"Invalid admin token. IP: {}\", ip.ip);\n        Err(AdminResponse::Unauthorized(render_admin_login(\n            Some(\"Invalid admin token, please try again.\"),\n            redirect.as_deref(),\n        )))\n    } else {\n        // If the token received is valid, generate JWT and save it as a cookie\n        let claims = generate_admin_claims();\n        let jwt = encode_jwt(&claims);\n\n        let cookie = Cookie::build((COOKIE_NAME, jwt))\n            .path(admin_path())\n            .max_age(time::Duration::minutes(CONFIG.admin_session_lifetime()))\n            .same_site(SameSite::Strict)\n            .http_only(true)\n            .secure(secure.https);\n\n        cookies.add(cookie);\n        if let Some(redirect) = redirect {\n            Ok(Redirect::to(format!(\"{}{redirect}\", admin_path())))\n        } else {\n            Err(AdminResponse::Ok(render_admin_page()))\n        }\n    }\n}\n\nfn _validate_token(token: &str) -> bool {\n    match CONFIG.admin_token().as_ref() {\n        None => false,\n        Some(t) if t.starts_with(\"$argon2\") => {\n            use argon2::password_hash::PasswordVerifier;\n            match argon2::password_hash::PasswordHash::new(t) {\n                Ok(h) => {\n                    // NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance.\n                    argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok()\n                }\n                Err(e) => {\n                    error!(\"The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: {e}\");\n                    false\n                }\n            }\n        }\n        Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),\n    }\n}\n\n#[derive(Serialize)]\nstruct AdminTemplateData {\n    page_content: String,\n    page_data: Option<Value>,\n    logged_in: bool,\n    urlpath: String,\n    sso_enabled: bool,\n}\n\nimpl AdminTemplateData {\n    fn new(page_content: &str, page_data: Value) -> Self {\n        Self {\n            page_content: String::from(page_content),\n            page_data: Some(page_data),\n            logged_in: true,\n            urlpath: CONFIG.domain_path(),\n            sso_enabled: CONFIG.sso_enabled(),\n        }\n    }\n\n    fn render(self) -> Result<String, Error> {\n        CONFIG.render_template(BASE_TEMPLATE, &self)\n    }\n}\n\nfn render_admin_page() -> ApiResult<Html<String>> {\n    let settings_json = json!({\n        \"config\": CONFIG.prepare_json(),\n        \"can_backup\": *CAN_BACKUP,\n    });\n    let text = AdminTemplateData::new(\"admin/settings\", settings_json).render()?;\n    Ok(Html(text))\n}\n\n#[get(\"/\")]\nfn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {\n    render_admin_page()\n}\n\n#[get(\"/\", rank = 2)]\nfn admin_page_login() -> ApiResult<Html<String>> {\n    render_admin_login(None, None)\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct InviteData {\n    email: String,\n}\n\nasync fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult<User> {\n    if let Some(user) = User::find_by_uuid(user_id, conn).await {\n        Ok(user)\n    } else {\n        err_code!(\"User doesn't exist\", Status::NotFound.code);\n    }\n}\n\n#[post(\"/invite\", format = \"application/json\", data = \"<data>\")]\nasync fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {\n    let data: InviteData = data.into_inner();\n    if User::find_by_mail(&data.email, &conn).await.is_some() {\n        err_code!(\"User already exists\", Status::Conflict.code)\n    }\n\n    let mut user = User::new(&data.email, None);\n\n    async fn _generate_invite(user: &User, conn: &DbConn) -> EmptyResult {\n        if CONFIG.mail_enabled() {\n            let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();\n            let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();\n            mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await\n        } else {\n            let invitation = Invitation::new(&user.email);\n            invitation.save(conn).await\n        }\n    }\n\n    _generate_invite(&user, &conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;\n    user.save(&conn).await.map_err(|e| e.with_code(Status::InternalServerError.code))?;\n\n    Ok(Json(user.to_json(&conn).await))\n}\n\n#[post(\"/test/smtp\", format = \"application/json\", data = \"<data>\")]\nasync fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {\n    let data: InviteData = data.into_inner();\n\n    if CONFIG.mail_enabled() {\n        mail::send_test(&data.email).await\n    } else {\n        err!(\"Mail is not enabled\")\n    }\n}\n\n#[get(\"/logout\")]\nfn logout(cookies: &CookieJar<'_>) -> Redirect {\n    cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));\n    Redirect::to(admin_path())\n}\n\n#[get(\"/users\")]\nasync fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {\n    let users = User::get_all(&conn).await;\n    let mut users_json = Vec::with_capacity(users.len());\n    for (u, _) in users {\n        let mut usr = u.to_json(&conn).await;\n        usr[\"userEnabled\"] = json!(u.enabled);\n        usr[\"createdAt\"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));\n        usr[\"lastActive\"] = match u.last_active(&conn).await {\n            Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),\n            None => json!(None::<String>),\n        };\n        users_json.push(usr);\n    }\n\n    Json(Value::Array(users_json))\n}\n\n#[get(\"/users/overview\")]\nasync fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {\n    let users = User::get_all(&conn).await;\n    let mut users_json = Vec::with_capacity(users.len());\n    for (u, sso_u) in users {\n        let mut usr = u.to_json(&conn).await;\n        usr[\"cipher_count\"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn).await);\n        usr[\"attachment_count\"] = json!(Attachment::count_by_user(&u.uuid, &conn).await);\n        usr[\"attachment_size\"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn).await));\n        usr[\"user_enabled\"] = json!(u.enabled);\n        usr[\"created_at\"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));\n        usr[\"last_active\"] = match u.last_active(&conn).await {\n            Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),\n            None => json!(\"Never\"),\n        };\n\n        usr[\"sso_identifier\"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new()));\n\n        users_json.push(usr);\n    }\n\n    let text = AdminTemplateData::new(\"admin/users\", json!(users_json)).render()?;\n    Ok(Html(text))\n}\n\n#[get(\"/users/by-mail/<mail>\")]\nasync fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> JsonResult {\n    if let Some(u) = User::find_by_mail(mail, &conn).await {\n        let mut usr = u.to_json(&conn).await;\n        usr[\"userEnabled\"] = json!(u.enabled);\n        usr[\"createdAt\"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));\n        Ok(Json(usr))\n    } else {\n        err_code!(\"User doesn't exist\", Status::NotFound.code);\n    }\n}\n\n#[get(\"/users/<user_id>\")]\nasync fn get_user_json(user_id: UserId, _token: AdminToken, conn: DbConn) -> JsonResult {\n    let u = get_user_or_404(&user_id, &conn).await?;\n    let mut usr = u.to_json(&conn).await;\n    usr[\"userEnabled\"] = json!(u.enabled);\n    usr[\"createdAt\"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));\n    Ok(Json(usr))\n}\n\n#[post(\"/users/<user_id>/delete\", format = \"application/json\")]\nasync fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {\n    let user = get_user_or_404(&user_id, &conn).await?;\n\n    // Get the membership records before deleting the actual user\n    let memberships = Membership::find_any_state_by_user(&user_id, &conn).await;\n    let res = user.delete(&conn).await;\n\n    for membership in memberships {\n        log_event(\n            EventType::OrganizationUserDeleted as i32,\n            &membership.uuid,\n            &membership.org_uuid,\n            &ACTING_ADMIN_USER.into(),\n            14, // Use UnknownBrowser type\n            &token.ip.ip,\n            &conn,\n        )\n        .await;\n    }\n\n    res\n}\n\n#[delete(\"/users/<user_id>/sso\", format = \"application/json\")]\nasync fn delete_sso_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {\n    let memberships = Membership::find_any_state_by_user(&user_id, &conn).await;\n    let res = SsoUser::delete(&user_id, &conn).await;\n\n    for membership in memberships {\n        log_event(\n            EventType::OrganizationUserUnlinkedSso as i32,\n            &membership.uuid,\n            &membership.org_uuid,\n            &ACTING_ADMIN_USER.into(),\n            14, // Use UnknownBrowser type\n            &token.ip.ip,\n            &conn,\n        )\n        .await;\n    }\n\n    res\n}\n\n#[post(\"/users/<user_id>/deauth\", format = \"application/json\")]\nasync fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let mut user = get_user_or_404(&user_id, &conn).await?;\n\n    nt.send_logout(&user, None, &conn).await;\n\n    if CONFIG.push_enabled() {\n        for device in Device::find_push_devices_by_user(&user.uuid, &conn).await {\n            match unregister_push_device(&device.push_uuid).await {\n                Ok(r) => r,\n                Err(e) => error!(\"Unable to unregister devices from Bitwarden server: {e}\"),\n            };\n        }\n    }\n\n    Device::delete_all_by_user(&user.uuid, &conn).await?;\n    user.reset_security_stamp();\n\n    user.save(&conn).await\n}\n\n#[post(\"/users/<user_id>/disable\", format = \"application/json\")]\nasync fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let mut user = get_user_or_404(&user_id, &conn).await?;\n    Device::delete_all_by_user(&user.uuid, &conn).await?;\n    user.reset_security_stamp();\n    user.enabled = false;\n\n    let save_result = user.save(&conn).await;\n\n    nt.send_logout(&user, None, &conn).await;\n\n    save_result\n}\n\n#[post(\"/users/<user_id>/enable\", format = \"application/json\")]\nasync fn enable_user(user_id: UserId, _token: AdminToken, conn: DbConn) -> EmptyResult {\n    let mut user = get_user_or_404(&user_id, &conn).await?;\n    user.enabled = true;\n\n    user.save(&conn).await\n}\n\n#[post(\"/users/<user_id>/remove-2fa\", format = \"application/json\")]\nasync fn remove_2fa(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult {\n    let mut user = get_user_or_404(&user_id, &conn).await?;\n    TwoFactor::delete_all_by_user(&user.uuid, &conn).await?;\n    two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &conn).await?;\n    user.totp_recover = None;\n    user.save(&conn).await\n}\n\n#[post(\"/users/<user_id>/invite/resend\", format = \"application/json\")]\nasync fn resend_user_invite(user_id: UserId, _token: AdminToken, conn: DbConn) -> EmptyResult {\n    if let Some(user) = User::find_by_uuid(&user_id, &conn).await {\n        //TODO: replace this with user.status check when it will be available (PR#3397)\n        if !user.password_hash.is_empty() {\n            err_code!(\"User already accepted invitation\", Status::BadRequest.code);\n        }\n\n        if CONFIG.mail_enabled() {\n            let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();\n            let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();\n            mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await\n        } else {\n            Ok(())\n        }\n    } else {\n        err_code!(\"User doesn't exist\", Status::NotFound.code);\n    }\n}\n\n#[derive(Debug, Deserialize)]\nstruct MembershipTypeData {\n    user_type: NumberOrString,\n    user_uuid: UserId,\n    org_uuid: OrganizationId,\n}\n\n#[post(\"/users/org_type\", format = \"application/json\", data = \"<data>\")]\nasync fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, conn: DbConn) -> EmptyResult {\n    let data: MembershipTypeData = data.into_inner();\n\n    let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn).await\n    else {\n        err!(\"The specified user isn't member of the organization\")\n    };\n\n    let new_type = match MembershipType::from_str(&data.user_type.into_string()) {\n        Some(new_type) => new_type as i32,\n        None => err!(\"Invalid type\"),\n    };\n\n    if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {\n        // Removing owner permission, check that there is at least one other confirmed owner\n        if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &conn).await <= 1 {\n            err!(\"Can't change the type of the last owner\")\n        }\n    }\n\n    member_to_edit.atype = new_type;\n    // This check is also done at api::organizations::{accept_invite, _confirm_invite, _activate_member, edit_member}, update_membership_type\n    OrgPolicy::check_user_allowed(&member_to_edit, \"modify\", &conn).await?;\n\n    log_event(\n        EventType::OrganizationUserUpdated as i32,\n        &member_to_edit.uuid,\n        &data.org_uuid,\n        &ACTING_ADMIN_USER.into(),\n        14, // Use UnknownBrowser type\n        &token.ip.ip,\n        &conn,\n    )\n    .await;\n\n    member_to_edit.save(&conn).await\n}\n\n#[post(\"/users/update_revision\", format = \"application/json\")]\nasync fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {\n    User::update_all_revisions(&conn).await\n}\n\n#[get(\"/organizations/overview\")]\nasync fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {\n    let organizations = Organization::get_all(&conn).await;\n    let mut organizations_json = Vec::with_capacity(organizations.len());\n    for o in organizations {\n        let mut org = o.to_json();\n        org[\"user_count\"] = json!(Membership::count_by_org(&o.uuid, &conn).await);\n        org[\"cipher_count\"] = json!(Cipher::count_by_org(&o.uuid, &conn).await);\n        org[\"collection_count\"] = json!(Collection::count_by_org(&o.uuid, &conn).await);\n        org[\"group_count\"] = json!(Group::count_by_org(&o.uuid, &conn).await);\n        org[\"event_count\"] = json!(Event::count_by_org(&o.uuid, &conn).await);\n        org[\"attachment_count\"] = json!(Attachment::count_by_org(&o.uuid, &conn).await);\n        org[\"attachment_size\"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn).await));\n        organizations_json.push(org);\n    }\n\n    let text = AdminTemplateData::new(\"admin/organizations\", json!(organizations_json)).render()?;\n    Ok(Html(text))\n}\n\n#[post(\"/organizations/<org_id>/delete\", format = \"application/json\")]\nasync fn delete_organization(org_id: OrganizationId, _token: AdminToken, conn: DbConn) -> EmptyResult {\n    let org = Organization::find_by_uuid(&org_id, &conn).await.map_res(\"Organization doesn't exist\")?;\n    org.delete(&conn).await\n}\n\n#[derive(Deserialize)]\nstruct GitRelease {\n    tag_name: String,\n}\n\n#[derive(Deserialize)]\nstruct GitCommit {\n    sha: String,\n}\n\nasync fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {\n    Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::<T>().await?)\n}\n\nasync fn get_text_api(url: &str) -> Result<String, Error> {\n    Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.text().await?)\n}\n\nasync fn has_http_access() -> bool {\n    let Ok(req) = make_http_request(Method::HEAD, \"https://github.com/dani-garcia/vaultwarden\") else {\n        return false;\n    };\n    match req.send().await {\n        Ok(r) => r.status().is_success(),\n        _ => false,\n    }\n}\n\nuse cached::proc_macro::cached;\n/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already\n/// It will cache this function for 600 seconds (10 minutes) which should prevent the exhaustion of the rate limit\n/// Any cache will be lost if Vaultwarden is restarted\nuse std::time::Duration; // Needed for cached\n#[cached(time = 600, sync_writes = \"default\")]\nasync fn get_release_info(has_http_access: bool) -> (String, String, String) {\n    // 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.\n    if has_http_access {\n        (\n            match get_json_api::<GitRelease>(\"https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest\")\n                .await\n            {\n                Ok(r) => r.tag_name,\n                _ => \"-\".to_string(),\n            },\n            match get_json_api::<GitCommit>(\"https://api.github.com/repos/dani-garcia/vaultwarden/commits/main\").await {\n                Ok(mut c) => {\n                    c.sha.truncate(8);\n                    c.sha\n                }\n                _ => \"-\".to_string(),\n            },\n            // Do not fetch the web-vault version when running within a container\n            // The web-vault version is embedded within the container it self, and should not be updated manually\n            match get_json_api::<GitRelease>(\"https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest\")\n                .await\n            {\n                Ok(r) => r.tag_name.trim_start_matches('v').to_string(),\n                _ => \"-\".to_string(),\n            },\n        )\n    } else {\n        (\"-\".to_string(), \"-\".to_string(), \"-\".to_string())\n    }\n}\n\nasync fn get_ntp_time(has_http_access: bool) -> String {\n    if has_http_access {\n        if let Ok(cf_trace) = get_text_api(\"https://cloudflare.com/cdn-cgi/trace\").await {\n            for line in cf_trace.lines() {\n                if let Some((key, value)) = line.split_once('=') {\n                    if key == \"ts\" {\n                        let ts = value.split_once('.').map_or(value, |(s, _)| s);\n                        if let Ok(dt) = chrono::DateTime::parse_from_str(ts, \"%s\") {\n                            return dt.format(\"%Y-%m-%d %H:%M:%S UTC\").to_string();\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n    }\n    String::from(\"Unable to fetch NTP time.\")\n}\n\nfn web_vault_compare(active: &str, latest: &str) -> i8 {\n    use semver::Version;\n    use std::cmp::Ordering;\n\n    let active_semver = Version::parse(active).unwrap_or_else(|e| {\n        warn!(\"Unable to parse active web-vault version '{active}': {e}\");\n        Version::parse(\"2025.1.1\").unwrap()\n    });\n    let latest_semver = Version::parse(latest).unwrap_or_else(|e| {\n        warn!(\"Unable to parse latest web-vault version '{latest}': {e}\");\n        Version::parse(\"2025.1.1\").unwrap()\n    });\n\n    match active_semver.cmp(&latest_semver) {\n        Ordering::Less => -1,\n        Ordering::Equal => 0,\n        Ordering::Greater => 1,\n    }\n}\n\n#[get(\"/diagnostics\")]\nasync fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {\n    use chrono::prelude::*;\n    use std::net::ToSocketAddrs;\n\n    // Execute some environment checks\n    let running_within_container = is_running_in_container();\n    let has_http_access = has_http_access().await;\n    let uses_proxy = env::var_os(\"HTTP_PROXY\").is_some()\n        || env::var_os(\"http_proxy\").is_some()\n        || env::var_os(\"HTTPS_PROXY\").is_some()\n        || env::var_os(\"https_proxy\").is_some();\n\n    // Check if we are able to resolve DNS entries\n    let dns_resolved = match (\"github.com\", 0).to_socket_addrs().map(|mut i| i.next()) {\n        Ok(Some(a)) => a.ip().to_string(),\n        _ => \"Unable to resolve domain name.\".to_string(),\n    };\n\n    let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await;\n    let active_web_release = get_active_web_release();\n    let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release);\n\n    let ip_header_name = &ip_header.0.unwrap_or_default();\n\n    let diagnostics_json = json!({\n        \"dns_resolved\": dns_resolved,\n        \"current_release\": VERSION,\n        \"latest_release\": latest_vw_release,\n        \"latest_commit\": latest_vw_commit,\n        \"web_vault_enabled\": &CONFIG.web_vault_enabled(),\n        \"active_web_release\": active_web_release,\n        \"latest_web_release\": latest_web_release,\n        \"web_vault_compare\": web_vault_compare,\n        \"running_within_container\": running_within_container,\n        \"container_base_image\": if running_within_container { container_base_image() } else { \"Not applicable\" },\n        \"has_http_access\": has_http_access,\n        \"ip_header_exists\": !ip_header_name.is_empty(),\n        \"ip_header_match\": ip_header_name.eq(&CONFIG.ip_header()),\n        \"ip_header_name\": ip_header_name,\n        \"ip_header_config\": &CONFIG.ip_header(),\n        \"uses_proxy\": uses_proxy,\n        \"enable_websocket\": &CONFIG.enable_websocket(),\n        \"db_type\": *DB_TYPE,\n        \"db_version\": get_sql_server_version(&conn).await,\n        \"admin_url\": format!(\"{}/diagnostics\", admin_url()),\n        \"overrides\": &CONFIG.get_overrides().join(\", \"),\n        \"host_arch\": env::consts::ARCH,\n        \"host_os\":  env::consts::OS,\n        \"tz_env\": env::var(\"TZ\").unwrap_or_default(),\n        \"server_time_local\": Local::now().format(\"%Y-%m-%d %H:%M:%S %Z\").to_string(),\n        \"server_time\": Utc::now().format(\"%Y-%m-%d %H:%M:%S UTC\").to_string(), // Run the server date/time check as late as possible to minimize the time difference\n        \"ntp_time\": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference\n    });\n\n    let text = AdminTemplateData::new(\"admin/diagnostics\", diagnostics_json).render()?;\n    Ok(Html(text))\n}\n\n#[get(\"/diagnostics/config\", format = \"application/json\")]\nfn get_diagnostics_config(_token: AdminToken) -> Json<Value> {\n    let support_json = CONFIG.get_support_json();\n    Json(support_json)\n}\n\n#[get(\"/diagnostics/http?<code>\")]\nfn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {\n    err_code!(format!(\"Testing error {code} response\"), code);\n}\n\n#[post(\"/config\", format = \"application/json\", data = \"<data>\")]\nasync fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {\n    let data: ConfigBuilder = data.into_inner();\n    if let Err(e) = CONFIG.update_config(data, true).await {\n        err!(format!(\"Unable to save config: {e:?}\"))\n    }\n    Ok(())\n}\n\n#[post(\"/config/delete\", format = \"application/json\")]\nasync fn delete_config(_token: AdminToken) -> EmptyResult {\n    if let Err(e) = CONFIG.delete_user_config().await {\n        err!(format!(\"Unable to delete config: {e:?}\"))\n    }\n    Ok(())\n}\n\n#[post(\"/config/backup_db\", format = \"application/json\")]\nfn backup_db(_token: AdminToken) -> ApiResult<String> {\n    if *CAN_BACKUP {\n        match backup_sqlite() {\n            Ok(f) => Ok(format!(\"Backup to '{f}' was successful\")),\n            Err(e) => err!(format!(\"Backup was unsuccessful {e}\")),\n        }\n    } else {\n        err!(\"Can't back up current DB (Only SQLite supports this feature)\");\n    }\n}\n\npub struct AdminToken {\n    ip: ClientIp,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for AdminToken {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let ip = match ClientIp::from_request(request).await {\n            Outcome::Success(ip) => ip,\n            _ => err_handler!(\"Error getting Client IP\"),\n        };\n\n        if !CONFIG.disable_admin_token() {\n            let cookies = request.cookies();\n\n            let access_token = match cookies.get(COOKIE_NAME) {\n                Some(cookie) => cookie.value(),\n                None => {\n                    let requested_page =\n                        request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();\n                    // When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page\n                    // Else, return a 401 failure, which will be caught\n                    if requested_page.is_empty() {\n                        return Outcome::Forward(Status::Unauthorized);\n                    } else {\n                        return Outcome::Error((Status::Unauthorized, \"Unauthorized\"));\n                    }\n                }\n            };\n\n            if decode_admin(access_token).is_err() {\n                // Remove admin cookie\n                cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));\n                error!(\"Invalid or expired admin JWT. IP: {}.\", &ip.ip);\n                return Outcome::Error((Status::Unauthorized, \"Session expired\"));\n            }\n        }\n\n        Outcome::Success(Self {\n            ip,\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn validate_web_vault_compare() {\n        // web_vault_compare(active, latest)\n        // Test normal versions\n        assert!(web_vault_compare(\"2025.12.0\", \"2025.12.1\") == -1);\n        assert!(web_vault_compare(\"2025.12.1\", \"2025.12.1\") == 0);\n        assert!(web_vault_compare(\"2025.12.2\", \"2025.12.1\") == 1);\n\n        // Test patched/+build.n versions\n        // Newer latest version\n        assert!(web_vault_compare(\"2025.12.0+build.1\", \"2025.12.1\") == -1);\n        assert!(web_vault_compare(\"2025.12.1\", \"2025.12.1+build.1\") == -1);\n        assert!(web_vault_compare(\"2025.12.0+build.1\", \"2025.12.1+build.1\") == -1);\n        assert!(web_vault_compare(\"2025.12.1+build.1\", \"2025.12.1+build.2\") == -1);\n        // Equal versions\n        assert!(web_vault_compare(\"2025.12.1+build.1\", \"2025.12.1+build.1\") == 0);\n        assert!(web_vault_compare(\"2025.12.2+build.2\", \"2025.12.2+build.2\") == 0);\n        // Newer active version\n        assert!(web_vault_compare(\"2025.12.1+build.1\", \"2025.12.1\") == 1);\n        assert!(web_vault_compare(\"2025.12.2\", \"2025.12.1+build.1\") == 1);\n        assert!(web_vault_compare(\"2025.12.2+build.1\", \"2025.12.1+build.1\") == 1);\n        assert!(web_vault_compare(\"2025.12.1+build.3\", \"2025.12.1+build.2\") == 1);\n    }\n}\n"
  },
  {
    "path": "src/api/core/accounts.rs",
    "content": "use std::collections::HashSet;\n\nuse crate::db::DbPool;\nuse chrono::Utc;\nuse rocket::serde::json::Json;\nuse serde_json::Value;\n\nuse crate::{\n    api::{\n        core::{accept_org_invite, log_user_event, two_factor::email},\n        master_password_policy, register_push_device, unregister_push_device, AnonymousNotify, ApiResult, EmptyResult,\n        JsonResult, Notify, PasswordOrOtpData, UpdateType,\n    },\n    auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},\n    crypto,\n    db::{\n        models::{\n            AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, EmergencyAccess,\n            EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, OrgPolicy,\n            OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType,\n        },\n        DbConn,\n    },\n    mail,\n    util::{format_date, NumberOrString},\n    CONFIG,\n};\n\nuse rocket::{\n    http::Status,\n    request::{FromRequest, Outcome, Request},\n};\n\npub fn routes() -> Vec<rocket::Route> {\n    routes![\n        profile,\n        put_profile,\n        post_profile,\n        put_avatar,\n        get_public_keys,\n        post_keys,\n        post_password,\n        post_set_password,\n        post_kdf,\n        post_rotatekey,\n        post_sstamp,\n        post_email_token,\n        post_email,\n        post_verify_email,\n        post_verify_email_token,\n        post_delete_recover,\n        post_delete_recover_token,\n        post_delete_account,\n        delete_account,\n        revision_date,\n        password_hint,\n        prelogin,\n        verify_password,\n        api_key,\n        rotate_api_key,\n        get_known_device,\n        get_all_devices,\n        get_device,\n        post_device_token,\n        put_device_token,\n        put_clear_device_token,\n        post_clear_device_token,\n        get_tasks,\n        post_auth_request,\n        get_auth_request,\n        put_auth_request,\n        get_auth_request_response,\n        get_auth_requests,\n        get_auth_requests_pending,\n    ]\n}\n\n#[derive(Debug, Deserialize, Eq, PartialEq)]\n#[serde(rename_all = \"camelCase\")]\npub struct KDFData {\n    #[serde(alias = \"kdfType\")]\n    kdf: i32,\n    #[serde(alias = \"iterations\")]\n    kdf_iterations: i32,\n    #[serde(alias = \"memory\")]\n    kdf_memory: Option<i32>,\n    #[serde(alias = \"parallelism\")]\n    kdf_parallelism: Option<i32>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct RegisterData {\n    email: String,\n\n    #[serde(flatten)]\n    kdf: KDFData,\n\n    #[serde(alias = \"userSymmetricKey\")]\n    key: String,\n    #[serde(alias = \"userAsymmetricKeys\")]\n    keys: Option<KeysData>,\n\n    master_password_hash: String,\n    master_password_hint: Option<String>,\n\n    name: Option<String>,\n\n    #[allow(dead_code)]\n    organization_user_id: Option<MembershipId>,\n\n    // Used only from the register/finish endpoint\n    email_verification_token: Option<String>,\n    accept_emergency_access_id: Option<EmergencyAccessId>,\n    accept_emergency_access_invite_token: Option<String>,\n    #[serde(alias = \"token\")]\n    org_invite_token: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SetPasswordData {\n    #[serde(flatten)]\n    kdf: KDFData,\n\n    key: String,\n    keys: Option<KeysData>,\n    master_password_hash: String,\n    master_password_hint: Option<String>,\n    org_identifier: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct KeysData {\n    encrypted_private_key: String,\n    public_key: String,\n}\n\n/// Trims whitespace from password hints, and converts blank password hints to `None`.\nfn clean_password_hint(password_hint: &Option<String>) -> Option<String> {\n    match password_hint {\n        None => None,\n        Some(h) => match h.trim() {\n            \"\" => None,\n            ht => Some(ht.to_string()),\n        },\n    }\n}\n\nfn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {\n    if password_hint.is_some() && !CONFIG.password_hints_allowed() {\n        err!(\"Password hints have been disabled by the administrator. Remove the hint and try again.\");\n    }\n    Ok(())\n}\nasync fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &DbConn) -> bool {\n    if !CONFIG._enable_email_2fa() {\n        return false;\n    }\n    if CONFIG.email_2fa_enforce_on_verified_invite() {\n        return true;\n    }\n    if let Some(member_id) = member_id {\n        return OrgPolicy::is_enabled_for_member(&member_id, OrgPolicyType::TwoFactorAuthentication, conn).await;\n    }\n    false\n}\n\npub async fn _register(data: Json<RegisterData>, email_verification: bool, conn: DbConn) -> JsonResult {\n    let mut data: RegisterData = data.into_inner();\n    let email = data.email.to_lowercase();\n\n    let mut email_verified = false;\n\n    let mut pending_emergency_access = None;\n\n    // First, validate the provided verification tokens\n    if email_verification {\n        match (\n            &data.email_verification_token,\n            &data.accept_emergency_access_id,\n            &data.accept_emergency_access_invite_token,\n            &data.organization_user_id,\n            &data.org_invite_token,\n        ) {\n            // Normal user registration, when email verification is required\n            (Some(email_verification_token), None, None, None, None) => {\n                let claims = crate::auth::decode_register_verify(email_verification_token)?;\n                if claims.sub != data.email {\n                    err!(\"Email verification token does not match email\");\n                }\n\n                // During this call we don't get the name, so extract it from the claims\n                if claims.name.is_some() {\n                    data.name = claims.name;\n                }\n                email_verified = claims.verified;\n            }\n            // Emergency access registration\n            (None, Some(accept_emergency_access_id), Some(accept_emergency_access_invite_token), None, None) => {\n                if !CONFIG.emergency_access_allowed() {\n                    err!(\"Emergency access is not enabled.\")\n                }\n\n                let claims = crate::auth::decode_emergency_access_invite(accept_emergency_access_invite_token)?;\n\n                if claims.email != data.email {\n                    err!(\"Claim email does not match email\")\n                }\n                if &claims.emer_id != accept_emergency_access_id {\n                    err!(\"Claim emer_id does not match accept_emergency_access_id\")\n                }\n\n                pending_emergency_access = Some((accept_emergency_access_id, claims));\n                email_verified = true;\n            }\n            // Org invite\n            (None, None, None, Some(organization_user_id), Some(org_invite_token)) => {\n                let claims = decode_invite(org_invite_token)?;\n\n                if claims.email != data.email {\n                    err!(\"Claim email does not match email\")\n                }\n\n                if &claims.member_id != organization_user_id {\n                    err!(\"Claim org_user_id does not match organization_user_id\")\n                }\n\n                email_verified = true;\n            }\n\n            _ => {\n                err!(\"Registration is missing required parameters\")\n            }\n        }\n    }\n\n    // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)\n    // This also prevents issues with very long usernames causing to large JWT's. See #2419\n    if let Some(ref name) = data.name {\n        if name.len() > 50 {\n            err!(\"The field Name must be a string with a maximum length of 50.\");\n        }\n    }\n\n    // Check against the password hint setting here so if it fails, the user\n    // can retry without losing their invitation below.\n    let password_hint = clean_password_hint(&data.master_password_hint);\n    enforce_password_hint_setting(&password_hint)?;\n\n    let mut user = match User::find_by_mail(&email, &conn).await {\n        Some(user) => {\n            if !user.password_hash.is_empty() {\n                err!(\"Registration not allowed or user already exists\")\n            }\n\n            if let Some(token) = data.org_invite_token {\n                let claims = decode_invite(&token)?;\n                if claims.email == email {\n                    // Verify the email address when signing up via a valid invite token\n                    email_verified = true;\n                    user\n                } else {\n                    err!(\"Registration email does not match invite email\")\n                }\n            } else if Invitation::take(&email, &conn).await {\n                Membership::accept_user_invitations(&user.uuid, &conn).await?;\n                user\n            } else if CONFIG.is_signup_allowed(&email)\n                || (CONFIG.emergency_access_allowed()\n                    && EmergencyAccess::find_invited_by_grantee_email(&email, &conn).await.is_some())\n            {\n                user\n            } else {\n                err!(\"Registration not allowed or user already exists\")\n            }\n        }\n        None => {\n            // Order is important here; the invitation check must come first\n            // because the vaultwarden admin can invite anyone, regardless\n            // of other signup restrictions.\n            if Invitation::take(&email, &conn).await\n                || CONFIG.is_signup_allowed(&email)\n                || pending_emergency_access.is_some()\n            {\n                User::new(&email, None)\n            } else {\n                err!(\"Registration not allowed or user already exists\")\n            }\n        }\n    };\n\n    // Make sure we don't leave a lingering invitation.\n    Invitation::take(&email, &conn).await;\n\n    set_kdf_data(&mut user, &data.kdf)?;\n\n    user.set_password(&data.master_password_hash, Some(data.key), true, None);\n    user.password_hint = password_hint;\n\n    // Add extra fields if present\n    if let Some(name) = data.name {\n        user.name = name;\n    }\n\n    if let Some(keys) = data.keys {\n        user.private_key = Some(keys.encrypted_private_key);\n        user.public_key = Some(keys.public_key);\n    }\n\n    if email_verified {\n        user.verified_at = Some(Utc::now().naive_utc());\n    }\n\n    if CONFIG.mail_enabled() {\n        if CONFIG.signups_verify() && !email_verified {\n            if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {\n                error!(\"Error sending welcome email: {e:#?}\");\n            }\n            user.last_verifying_at = Some(user.created_at);\n        } else if let Err(e) = mail::send_welcome(&user.email).await {\n            error!(\"Error sending welcome email: {e:#?}\");\n        }\n\n        if email_verified && is_email_2fa_required(data.organization_user_id, &conn).await {\n            email::activate_email_2fa(&user, &conn).await.ok();\n        }\n    }\n\n    user.save(&conn).await?;\n\n    // accept any open emergency access invitations\n    if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() {\n        for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &conn).await {\n            emergency_invite.accept_invite(&user.uuid, &user.email, &conn).await.ok();\n        }\n    }\n\n    Ok(Json(json!({\n      \"object\": \"register\",\n      \"captchaBypassToken\": \"\",\n    })))\n}\n\n#[post(\"/accounts/set-password\", data = \"<data>\")]\nasync fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: SetPasswordData = data.into_inner();\n    let mut user = headers.user;\n\n    if user.private_key.is_some() {\n        err!(\"Account already initialized, cannot set password\")\n    }\n\n    // Check against the password hint setting here so if it fails,\n    // the user can retry without losing their invitation below.\n    let password_hint = clean_password_hint(&data.master_password_hint);\n    enforce_password_hint_setting(&password_hint)?;\n\n    set_kdf_data(&mut user, &data.kdf)?;\n\n    user.set_password(\n        &data.master_password_hash,\n        Some(data.key),\n        false,\n        Some(vec![String::from(\"revision_date\")]), // We need to allow revision-date to use the old security_timestamp\n    );\n    user.password_hint = password_hint;\n\n    if let Some(keys) = data.keys {\n        user.private_key = Some(keys.encrypted_private_key);\n        user.public_key = Some(keys.public_key);\n    }\n\n    if let Some(identifier) = data.org_identifier {\n        if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {\n            let org = match Organization::find_by_uuid(&identifier.into(), &conn).await {\n                None => err!(\"Failed to retrieve the associated organization\"),\n                Some(org) => org,\n            };\n\n            let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await {\n                None => err!(\"Failed to retrieve the invitation\"),\n                Some(org) => org,\n            };\n\n            accept_org_invite(&user, membership, None, &conn).await?;\n        }\n    }\n\n    if CONFIG.mail_enabled() {\n        mail::send_welcome(&user.email.to_lowercase()).await?;\n    } else {\n        Membership::accept_user_invitations(&user.uuid, &conn).await?;\n    }\n\n    log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)\n        .await;\n\n    user.save(&conn).await?;\n\n    Ok(Json(json!({\n      \"object\": \"set-password\",\n      \"captchaBypassToken\": \"\",\n    })))\n}\n\n#[get(\"/accounts/profile\")]\nasync fn profile(headers: Headers, conn: DbConn) -> Json<Value> {\n    Json(headers.user.to_json(&conn).await)\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ProfileData {\n    // culture: String, // Ignored, always use en-US\n    name: String,\n}\n\n#[put(\"/accounts/profile\", data = \"<data>\")]\nasync fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {\n    post_profile(data, headers, conn).await\n}\n\n#[post(\"/accounts/profile\", data = \"<data>\")]\nasync fn post_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: ProfileData = data.into_inner();\n\n    // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)\n    // This also prevents issues with very long usernames causing to large JWT's. See #2419\n    if data.name.len() > 50 {\n        err!(\"The field Name must be a string with a maximum length of 50.\");\n    }\n\n    let mut user = headers.user;\n    user.name = data.name;\n\n    user.save(&conn).await?;\n    Ok(Json(user.to_json(&conn).await))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AvatarData {\n    avatar_color: Option<String>,\n}\n\n#[put(\"/accounts/avatar\", data = \"<data>\")]\nasync fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: AvatarData = data.into_inner();\n\n    // It looks like it only supports the 6 hex color format.\n    // If you try to add the short value it will not show that color.\n    // Check and force 7 chars, including the #.\n    if let Some(color) = &data.avatar_color {\n        if color.len() != 7 {\n            err!(\"The field AvatarColor must be a HTML/Hex color code with a length of 7 characters\")\n        }\n    }\n\n    let mut user = headers.user;\n    user.avatar_color = data.avatar_color;\n\n    user.save(&conn).await?;\n    Ok(Json(user.to_json(&conn).await))\n}\n\n#[get(\"/users/<user_id>/public-key\")]\nasync fn get_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> JsonResult {\n    let user = match User::find_by_uuid(&user_id, &conn).await {\n        Some(user) if user.public_key.is_some() => user,\n        Some(_) => err_code!(\"User has no public_key\", Status::NotFound.code),\n        None => err_code!(\"User doesn't exist\", Status::NotFound.code),\n    };\n\n    Ok(Json(json!({\n        \"userId\": user.uuid,\n        \"publicKey\": user.public_key,\n        \"object\":\"userKey\"\n    })))\n}\n\n#[post(\"/accounts/keys\", data = \"<data>\")]\nasync fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: KeysData = data.into_inner();\n\n    let mut user = headers.user;\n\n    user.private_key = Some(data.encrypted_private_key);\n    user.public_key = Some(data.public_key);\n\n    user.save(&conn).await?;\n\n    Ok(Json(json!({\n        \"privateKey\": user.private_key,\n        \"publicKey\": user.public_key,\n        \"object\":\"keys\"\n    })))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ChangePassData {\n    master_password_hash: String,\n    new_master_password_hash: String,\n    master_password_hint: Option<String>,\n    key: String,\n}\n\n#[post(\"/accounts/password\", data = \"<data>\")]\nasync fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let data: ChangePassData = data.into_inner();\n    let mut user = headers.user;\n\n    if !user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\")\n    }\n\n    user.password_hint = clean_password_hint(&data.master_password_hint);\n    enforce_password_hint_setting(&user.password_hint)?;\n\n    log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)\n        .await;\n\n    user.set_password(\n        &data.new_master_password_hash,\n        Some(data.key),\n        true,\n        Some(vec![\n            String::from(\"post_rotatekey\"),\n            String::from(\"get_contacts\"),\n            String::from(\"get_public_keys\"),\n            String::from(\"get_api_webauthn\"),\n        ]),\n    );\n\n    let save_result = user.save(&conn).await;\n\n    // Prevent logging out the client where the user requested this endpoint from.\n    // If you do logout the user it will causes issues at the client side.\n    // Adding the device uuid will prevent this.\n    nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;\n\n    save_result\n}\n\nfn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult {\n    if data.kdf == UserKdfType::Pbkdf2 as i32 && data.kdf_iterations < 100_000 {\n        err!(\"PBKDF2 KDF iterations must be at least 100000.\")\n    }\n\n    if data.kdf == UserKdfType::Argon2id as i32 {\n        if data.kdf_iterations < 1 {\n            err!(\"Argon2 KDF iterations must be at least 1.\")\n        }\n        if let Some(m) = data.kdf_memory {\n            if !(15..=1024).contains(&m) {\n                err!(\"Argon2 memory must be between 15 MB and 1024 MB.\")\n            }\n            user.client_kdf_memory = data.kdf_memory;\n        } else {\n            err!(\"Argon2 memory parameter is required.\")\n        }\n        if let Some(p) = data.kdf_parallelism {\n            if !(1..=16).contains(&p) {\n                err!(\"Argon2 parallelism must be between 1 and 16.\")\n            }\n            user.client_kdf_parallelism = data.kdf_parallelism;\n        } else {\n            err!(\"Argon2 parallelism parameter is required.\")\n        }\n    } else {\n        user.client_kdf_memory = None;\n        user.client_kdf_parallelism = None;\n    }\n    user.client_kdf_iter = data.kdf_iterations;\n    user.client_kdf_type = data.kdf;\n\n    Ok(())\n}\n\n#[allow(dead_code)]\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AuthenticationData {\n    salt: String,\n    kdf: KDFData,\n    master_password_authentication_hash: String,\n}\n\n#[allow(dead_code)]\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct UnlockData {\n    salt: String,\n    kdf: KDFData,\n    master_key_wrapped_user_key: String,\n}\n\n#[allow(dead_code)]\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ChangeKdfData {\n    new_master_password_hash: String,\n    key: String,\n    authentication_data: AuthenticationData,\n    unlock_data: UnlockData,\n    master_password_hash: String,\n}\n\n#[post(\"/accounts/kdf\", data = \"<data>\")]\nasync fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let data: ChangeKdfData = data.into_inner();\n\n    if !headers.user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\")\n    }\n\n    if data.authentication_data.kdf != data.unlock_data.kdf {\n        err!(\"KDF settings must be equal for authentication and unlock\")\n    }\n\n    if headers.user.email != data.authentication_data.salt || headers.user.email != data.unlock_data.salt {\n        err!(\"Invalid master password salt\")\n    }\n\n    let mut user = headers.user;\n\n    set_kdf_data(&mut user, &data.unlock_data.kdf)?;\n\n    user.set_password(\n        &data.authentication_data.master_password_authentication_hash,\n        Some(data.unlock_data.master_key_wrapped_user_key),\n        true,\n        None,\n    );\n    let save_result = user.save(&conn).await;\n\n    nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;\n\n    save_result\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct UpdateFolderData {\n    // There is a bug in 2024.3.x which adds a `null` item.\n    // To bypass this we allow a Option here, but skip it during the updates\n    // See: https://github.com/bitwarden/clients/issues/8453\n    id: Option<FolderId>,\n    name: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct UpdateEmergencyAccessData {\n    id: EmergencyAccessId,\n    key_encrypted: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct UpdateResetPasswordData {\n    organization_id: OrganizationId,\n    reset_password_key: String,\n}\n\nuse super::ciphers::CipherData;\nuse super::sends::{update_send_from_data, SendData};\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct KeyData {\n    account_unlock_data: RotateAccountUnlockData,\n    account_keys: RotateAccountKeys,\n    account_data: RotateAccountData,\n    old_master_key_authentication_hash: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RotateAccountUnlockData {\n    emergency_access_unlock_data: Vec<UpdateEmergencyAccessData>,\n    master_password_unlock_data: MasterPasswordUnlockData,\n    organization_account_recovery_unlock_data: Vec<UpdateResetPasswordData>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct MasterPasswordUnlockData {\n    kdf_type: i32,\n    kdf_iterations: i32,\n    kdf_parallelism: Option<i32>,\n    kdf_memory: Option<i32>,\n    email: String,\n    master_key_authentication_hash: String,\n    master_key_encrypted_user_key: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RotateAccountKeys {\n    user_key_encrypted_account_private_key: String,\n    account_public_key: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RotateAccountData {\n    ciphers: Vec<CipherData>,\n    folders: Vec<UpdateFolderData>,\n    sends: Vec<SendData>,\n}\n\nfn validate_keydata(\n    data: &KeyData,\n    existing_ciphers: &[Cipher],\n    existing_folders: &[Folder],\n    existing_emergency_access: &[EmergencyAccess],\n    existing_memberships: &[Membership],\n    existing_sends: &[Send],\n    user: &User,\n) -> EmptyResult {\n    if user.client_kdf_type != data.account_unlock_data.master_password_unlock_data.kdf_type\n        || user.client_kdf_iter != data.account_unlock_data.master_password_unlock_data.kdf_iterations\n        || user.client_kdf_memory != data.account_unlock_data.master_password_unlock_data.kdf_memory\n        || user.client_kdf_parallelism != data.account_unlock_data.master_password_unlock_data.kdf_parallelism\n        || user.email != data.account_unlock_data.master_password_unlock_data.email\n    {\n        err!(\"Changing the kdf variant or email is not supported during key rotation\");\n    }\n    if user.public_key.as_ref() != Some(&data.account_keys.account_public_key) {\n        err!(\"Changing the asymmetric keypair is not possible during key rotation\")\n    }\n\n    // Check that we're correctly rotating all the user's ciphers\n    let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();\n    let provided_cipher_ids = data\n        .account_data\n        .ciphers\n        .iter()\n        .filter(|c| c.organization_id.is_none())\n        .filter_map(|c| c.id.as_ref())\n        .collect::<HashSet<&CipherId>>();\n    if !provided_cipher_ids.is_superset(&existing_cipher_ids) {\n        err!(\"All existing ciphers must be included in the rotation\")\n    }\n\n    // Check that we're correctly rotating all the user's folders\n    let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();\n    let provided_folder_ids =\n        data.account_data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();\n    if !provided_folder_ids.is_superset(&existing_folder_ids) {\n        err!(\"All existing folders must be included in the rotation\")\n    }\n\n    // Check that we're correctly rotating all the user's emergency access keys\n    let existing_emergency_access_ids =\n        existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();\n    let provided_emergency_access_ids = data\n        .account_unlock_data\n        .emergency_access_unlock_data\n        .iter()\n        .map(|ea| &ea.id)\n        .collect::<HashSet<&EmergencyAccessId>>();\n    if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {\n        err!(\"All existing emergency access keys must be included in the rotation\")\n    }\n\n    // Check that we're correctly rotating all the user's reset password keys\n    let existing_reset_password_ids =\n        existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();\n    let provided_reset_password_ids = data\n        .account_unlock_data\n        .organization_account_recovery_unlock_data\n        .iter()\n        .map(|rp| &rp.organization_id)\n        .collect::<HashSet<&OrganizationId>>();\n    if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {\n        err!(\"All existing reset password keys must be included in the rotation\")\n    }\n\n    // Check that we're correctly rotating all the user's sends\n    let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();\n    let provided_send_ids = data.account_data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();\n    if !provided_send_ids.is_superset(&existing_send_ids) {\n        err!(\"All existing sends must be included in the rotation\")\n    }\n\n    Ok(())\n}\n\n#[post(\"/accounts/key-management/rotate-user-account-keys\", data = \"<data>\")]\nasync fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    // TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything.\n    let data: KeyData = data.into_inner();\n\n    if !headers.user.check_valid_password(&data.old_master_key_authentication_hash) {\n        err!(\"Invalid password\")\n    }\n\n    // Validate the import before continuing\n    // Bitwarden does not process the import if there is one item invalid.\n    // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.\n    // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.\n    Cipher::validate_cipher_data(&data.account_data.ciphers)?;\n\n    let user_id = &headers.user.uuid;\n\n    // TODO: Ideally we'd do everything after this point in a single transaction.\n\n    let mut existing_ciphers = Cipher::find_owned_by_user(user_id, &conn).await;\n    let mut existing_folders = Folder::find_by_user(user_id, &conn).await;\n    let mut existing_emergency_access = EmergencyAccess::find_all_confirmed_by_grantor_uuid(user_id, &conn).await;\n    let mut existing_memberships = Membership::find_by_user(user_id, &conn).await;\n    // We only rotate the reset password key if it is set.\n    existing_memberships.retain(|m| m.reset_password_key.is_some());\n    let mut existing_sends = Send::find_by_user(user_id, &conn).await;\n\n    validate_keydata(\n        &data,\n        &existing_ciphers,\n        &existing_folders,\n        &existing_emergency_access,\n        &existing_memberships,\n        &existing_sends,\n        &headers.user,\n    )?;\n\n    // Update folder data\n    for folder_data in data.account_data.folders {\n        // Skip `null` folder id entries.\n        // See: https://github.com/bitwarden/clients/issues/8453\n        if let Some(folder_id) = folder_data.id {\n            let Some(saved_folder) = existing_folders.iter_mut().find(|f| f.uuid == folder_id) else {\n                err!(\"Folder doesn't exist\")\n            };\n\n            saved_folder.name = folder_data.name;\n            saved_folder.save(&conn).await?\n        }\n    }\n\n    // Update emergency access data\n    for emergency_access_data in data.account_unlock_data.emergency_access_unlock_data {\n        let Some(saved_emergency_access) =\n            existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id)\n        else {\n            err!(\"Emergency access doesn't exist or is not owned by the user\")\n        };\n\n        saved_emergency_access.key_encrypted = Some(emergency_access_data.key_encrypted);\n        saved_emergency_access.save(&conn).await?\n    }\n\n    // Update reset password data\n    for reset_password_data in data.account_unlock_data.organization_account_recovery_unlock_data {\n        let Some(membership) =\n            existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)\n        else {\n            err!(\"Reset password doesn't exist\")\n        };\n\n        membership.reset_password_key = Some(reset_password_data.reset_password_key);\n        membership.save(&conn).await?\n    }\n\n    // Update send data\n    for send_data in data.account_data.sends {\n        let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {\n            err!(\"Send doesn't exist\")\n        };\n\n        update_send_from_data(send, send_data, &headers, &conn, &nt, UpdateType::None).await?;\n    }\n\n    // Update cipher data\n    use super::ciphers::update_cipher_from_data;\n\n    for cipher_data in data.account_data.ciphers {\n        if cipher_data.organization_id.is_none() {\n            let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())\n            else {\n                err!(\"Cipher doesn't exist\")\n            };\n\n            // Prevent triggering cipher updates via WebSockets by settings UpdateType::None\n            // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.\n            // We force the users to logout after the user has been saved to try and prevent these issues.\n            update_cipher_from_data(saved_cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?\n        }\n    }\n\n    // Update user data\n    let mut user = headers.user;\n\n    user.private_key = Some(data.account_keys.user_key_encrypted_account_private_key);\n    user.set_password(\n        &data.account_unlock_data.master_password_unlock_data.master_key_authentication_hash,\n        Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key),\n        true,\n        None,\n    );\n\n    let save_result = user.save(&conn).await;\n\n    // Prevent logging out the client where the user requested this endpoint from.\n    // If you do logout the user it will causes issues at the client side.\n    // Adding the device uuid will prevent this.\n    nt.send_logout(&user, Some(headers.device.uuid.clone()), &conn).await;\n\n    save_result\n}\n\n#[post(\"/accounts/security-stamp\", data = \"<data>\")]\nasync fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let mut user = headers.user;\n\n    data.validate(&user, true, &conn).await?;\n\n    Device::delete_all_by_user(&user.uuid, &conn).await?;\n    user.reset_security_stamp();\n    let save_result = user.save(&conn).await;\n\n    nt.send_logout(&user, None, &conn).await;\n\n    save_result\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EmailTokenData {\n    master_password_hash: String,\n    new_email: String,\n}\n\n#[post(\"/accounts/email-token\", data = \"<data>\")]\nasync fn post_email_token(data: Json<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    if !CONFIG.email_change_allowed() {\n        err!(\"Email change is not allowed.\");\n    }\n\n    let data: EmailTokenData = data.into_inner();\n    let mut user = headers.user;\n\n    if !user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\")\n    }\n\n    if let Some(existing_user) = User::find_by_mail(&data.new_email, &conn).await {\n        if CONFIG.mail_enabled() {\n            // check if existing_user has already registered\n            if existing_user.password_hash.is_empty() {\n                // inform an invited user about how to delete their temporary account if the\n                // request was done intentionally and they want to update their mail address\n                if let Err(e) = mail::send_change_email_invited(&data.new_email, &user.email).await {\n                    error!(\"Error sending change-email-invited email: {e:#?}\");\n                }\n            } else {\n                // inform existing user about the failed attempt to change their mail address\n                if let Err(e) = mail::send_change_email_existing(&data.new_email, &user.email).await {\n                    error!(\"Error sending change-email-existing email: {e:#?}\");\n                }\n            }\n        }\n        err!(\"Email already in use\");\n    }\n\n    if !CONFIG.is_email_domain_allowed(&data.new_email) {\n        err!(\"Email domain not allowed\");\n    }\n\n    let token = crypto::generate_email_token(6);\n\n    if CONFIG.mail_enabled() {\n        if let Err(e) = mail::send_change_email(&data.new_email, &token).await {\n            error!(\"Error sending change-email email: {e:#?}\");\n        }\n    } else {\n        debug!(\"Email change request for user ({}) to email ({}) with token ({token})\", user.uuid, data.new_email);\n    }\n\n    user.email_new = Some(data.new_email);\n    user.email_new_token = Some(token);\n    user.save(&conn).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ChangeEmailData {\n    master_password_hash: String,\n    new_email: String,\n\n    key: String,\n    new_master_password_hash: String,\n    token: NumberOrString,\n}\n\n#[post(\"/accounts/email\", data = \"<data>\")]\nasync fn post_email(data: Json<ChangeEmailData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    if !CONFIG.email_change_allowed() {\n        err!(\"Email change is not allowed.\");\n    }\n\n    let data: ChangeEmailData = data.into_inner();\n    let mut user = headers.user;\n\n    if !user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\")\n    }\n\n    if User::find_by_mail(&data.new_email, &conn).await.is_some() {\n        err!(\"Email already in use\");\n    }\n\n    match user.email_new {\n        Some(ref val) => {\n            if val != &data.new_email {\n                err!(\"Email change mismatch\");\n            }\n        }\n        None => err!(\"No email change pending\"),\n    }\n\n    if CONFIG.mail_enabled() {\n        // Only check the token if we sent out an email...\n        match user.email_new_token {\n            Some(ref val) => {\n                if *val != data.token.into_string() {\n                    err!(\"Token mismatch\");\n                }\n            }\n            None => err!(\"No email change pending\"),\n        }\n        user.verified_at = Some(Utc::now().naive_utc());\n    } else {\n        user.verified_at = None;\n    }\n\n    user.email = data.new_email;\n    user.email_new = None;\n    user.email_new_token = None;\n\n    user.set_password(&data.new_master_password_hash, Some(data.key), true, None);\n\n    let save_result = user.save(&conn).await;\n\n    nt.send_logout(&user, None, &conn).await;\n\n    save_result\n}\n\n#[post(\"/accounts/verify-email\")]\nasync fn post_verify_email(headers: Headers) -> EmptyResult {\n    let user = headers.user;\n\n    if !CONFIG.mail_enabled() {\n        err!(\"Cannot verify email address\");\n    }\n\n    if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {\n        error!(\"Error sending verify_email email: {e:#?}\");\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct VerifyEmailTokenData {\n    user_id: UserId,\n    token: String,\n}\n\n#[post(\"/accounts/verify-email-token\", data = \"<data>\")]\nasync fn post_verify_email_token(data: Json<VerifyEmailTokenData>, conn: DbConn) -> EmptyResult {\n    let data: VerifyEmailTokenData = data.into_inner();\n\n    let Some(mut user) = User::find_by_uuid(&data.user_id, &conn).await else {\n        err!(\"User doesn't exist\")\n    };\n\n    let Ok(claims) = decode_verify_email(&data.token) else {\n        err!(\"Invalid claim\")\n    };\n    if claims.sub != *user.uuid {\n        err!(\"Invalid claim\");\n    }\n    user.verified_at = Some(Utc::now().naive_utc());\n    user.last_verifying_at = None;\n    user.login_verify_count = 0;\n    if let Err(e) = user.save(&conn).await {\n        error!(\"Error saving email verification: {e:#?}\");\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct DeleteRecoverData {\n    email: String,\n}\n\n#[post(\"/accounts/delete-recover\", data = \"<data>\")]\nasync fn post_delete_recover(data: Json<DeleteRecoverData>, conn: DbConn) -> EmptyResult {\n    let data: DeleteRecoverData = data.into_inner();\n\n    if CONFIG.mail_enabled() {\n        if let Some(user) = User::find_by_mail(&data.email, &conn).await {\n            if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {\n                error!(\"Error sending delete account email: {e:#?}\");\n            }\n        }\n        Ok(())\n    } else {\n        // We don't support sending emails, but we shouldn't allow anybody\n        // to delete accounts without at least logging in... And if the user\n        // cannot remember their password then they will need to contact\n        // the administrator to delete it...\n        err!(\"Please contact the administrator to delete your account\");\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct DeleteRecoverTokenData {\n    user_id: UserId,\n    token: String,\n}\n\n#[post(\"/accounts/delete-recover-token\", data = \"<data>\")]\nasync fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult {\n    let data: DeleteRecoverTokenData = data.into_inner();\n\n    let Ok(claims) = decode_delete(&data.token) else {\n        err!(\"Invalid claim\")\n    };\n\n    let Some(user) = User::find_by_uuid(&data.user_id, &conn).await else {\n        err!(\"User doesn't exist\")\n    };\n\n    if claims.sub != *user.uuid {\n        err!(\"Invalid claim\");\n    }\n    user.delete(&conn).await\n}\n\n#[post(\"/accounts/delete\", data = \"<data>\")]\nasync fn post_delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    delete_account(data, headers, conn).await\n}\n\n#[delete(\"/accounts\", data = \"<data>\")]\nasync fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, true, &conn).await?;\n\n    user.delete(&conn).await\n}\n\n#[get(\"/accounts/revision-date\")]\nfn revision_date(headers: Headers) -> JsonResult {\n    let revision_date = headers.user.updated_at.and_utc().timestamp_millis();\n    Ok(Json(json!(revision_date)))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct PasswordHintData {\n    email: String,\n}\n\n#[post(\"/accounts/password-hint\", data = \"<data>\")]\nasync fn password_hint(data: Json<PasswordHintData>, conn: DbConn) -> EmptyResult {\n    if !CONFIG.password_hints_allowed() || (!CONFIG.mail_enabled() && !CONFIG.show_password_hint()) {\n        err!(\"This server is not configured to provide password hints.\");\n    }\n\n    const NO_HINT: &str = \"Sorry, you have no password hint...\";\n\n    let data: PasswordHintData = data.into_inner();\n    let email = &data.email;\n\n    match User::find_by_mail(email, &conn).await {\n        None => {\n            // To prevent user enumeration, act as if the user exists.\n            if CONFIG.mail_enabled() {\n                // There is still a timing side channel here in that the code\n                // paths that send mail take noticeably longer than ones that\n                // don't. Add a randomized sleep to mitigate this somewhat.\n                use rand::{rngs::SmallRng, RngExt};\n                let mut rng: SmallRng = rand::make_rng();\n                let sleep_ms = rng.random_range(900..=1100) as u64;\n                tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;\n                Ok(())\n            } else {\n                err!(NO_HINT);\n            }\n        }\n        Some(user) => {\n            let hint: Option<String> = user.password_hint;\n            if CONFIG.mail_enabled() {\n                mail::send_password_hint(email, hint).await?;\n                Ok(())\n            } else if let Some(hint) = hint {\n                err!(format!(\"Your password hint is: {hint}\"));\n            } else {\n                err!(NO_HINT);\n            }\n        }\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct PreloginData {\n    email: String,\n}\n\n#[post(\"/accounts/prelogin\", data = \"<data>\")]\nasync fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {\n    _prelogin(data, conn).await\n}\n\npub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {\n    let data: PreloginData = data.into_inner();\n\n    let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await {\n        Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism),\n        None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),\n    };\n\n    Json(json!({\n        \"kdf\": kdf_type,\n        \"kdfIterations\": kdf_iter,\n        \"kdfMemory\": kdf_mem,\n        \"kdfParallelism\": kdf_para,\n    }))\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct SecretVerificationRequest {\n    master_password_hash: String,\n}\n\n// Change the KDF Iterations if necessary\npub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &DbConn) -> ApiResult<()> {\n    if user.password_iterations < CONFIG.password_iterations() {\n        user.password_iterations = CONFIG.password_iterations();\n        user.set_password(pwd_hash, None, false, None);\n\n        if let Err(e) = user.save(conn).await {\n            error!(\"Error updating user: {e:#?}\");\n        }\n    }\n    Ok(())\n}\n\n#[post(\"/accounts/verify-password\", data = \"<data>\")]\nasync fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: SecretVerificationRequest = data.into_inner();\n    let mut user = headers.user;\n\n    if !user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\")\n    }\n\n    kdf_upgrade(&mut user, &data.master_password_hash, &conn).await?;\n\n    Ok(Json(master_password_policy(&user, &conn).await))\n}\n\nasync fn _api_key(data: Json<PasswordOrOtpData>, rotate: bool, headers: Headers, conn: DbConn) -> JsonResult {\n    use crate::util::format_date;\n\n    let data: PasswordOrOtpData = data.into_inner();\n    let mut user = headers.user;\n\n    data.validate(&user, true, &conn).await?;\n\n    if rotate || user.api_key.is_none() {\n        user.api_key = Some(crypto::generate_api_key());\n        user.save(&conn).await.expect(\"Error saving API key\");\n    }\n\n    Ok(Json(json!({\n      \"apiKey\": user.api_key,\n      \"revisionDate\": format_date(&user.updated_at),\n      \"object\": \"apiKey\",\n    })))\n}\n\n#[post(\"/accounts/api-key\", data = \"<data>\")]\nasync fn api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    _api_key(data, false, headers, conn).await\n}\n\n#[post(\"/accounts/rotate-api-key\", data = \"<data>\")]\nasync fn rotate_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    _api_key(data, true, headers, conn).await\n}\n\n#[get(\"/devices/knowndevice\")]\nasync fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {\n    let result = if let Some(user) = User::find_by_mail(&device.email, &conn).await {\n        Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &conn).await.is_some()\n    } else {\n        false\n    };\n    Ok(Json(json!(result)))\n}\n\nstruct KnownDevice {\n    email: String,\n    uuid: DeviceId,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for KnownDevice {\n    type Error = &'static str;\n\n    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let email = if let Some(email_b64) = req.headers().get_one(\"X-Request-Email\") {\n            // Bitwarden seems to send padded Base64 strings since 2026.2.1\n            // Since these values are not streamed and Headers are always split by newlines\n            // we can safely ignore padding here and remove any '=' appended.\n            let email_b64 = email_b64.trim_end_matches('=');\n\n            let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) else {\n                return Outcome::Error((Status::BadRequest, \"X-Request-Email value failed to decode as base64url\"));\n            };\n            match String::from_utf8(email_bytes) {\n                Ok(email) => email,\n                Err(_) => {\n                    return Outcome::Error((Status::BadRequest, \"X-Request-Email value failed to decode as UTF-8\"));\n                }\n            }\n        } else {\n            return Outcome::Error((Status::BadRequest, \"X-Request-Email value is required\"));\n        };\n\n        let uuid = if let Some(uuid) = req.headers().get_one(\"X-Device-Identifier\") {\n            uuid.to_string().into()\n        } else {\n            return Outcome::Error((Status::BadRequest, \"X-Device-Identifier value is required\"));\n        };\n\n        Outcome::Success(KnownDevice {\n            email,\n            uuid,\n        })\n    }\n}\n\n#[get(\"/devices\")]\nasync fn get_all_devices(headers: Headers, conn: DbConn) -> JsonResult {\n    let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &conn).await;\n    let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();\n\n    Ok(Json(json!({\n        \"data\": devices,\n        \"continuationToken\": null,\n        \"object\": \"list\"\n    })))\n}\n\n#[get(\"/devices/identifier/<device_id>\")]\nasync fn get_device(device_id: DeviceId, headers: Headers, conn: DbConn) -> JsonResult {\n    let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &conn).await else {\n        err!(\"No device found\");\n    };\n    Ok(Json(device.to_json()))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct PushToken {\n    push_token: String,\n}\n\n#[post(\"/devices/identifier/<device_id>/token\", data = \"<data>\")]\nasync fn post_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {\n    put_device_token(device_id, data, headers, conn).await\n}\n\n#[put(\"/devices/identifier/<device_id>/token\", data = \"<data>\")]\nasync fn put_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {\n    let data = data.into_inner();\n    let token = data.push_token;\n\n    let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &conn).await else {\n        err!(format!(\"Error: device {device_id} should be present before a token can be assigned\"))\n    };\n\n    // Check if the new token is the same as the registered token\n    // Although upstream seems to always register a device on login, we do not.\n    // Unless this causes issues, lets keep it this way, else we might need to also register on every login.\n    if device.push_token.as_ref() == Some(&token) {\n        debug!(\"Device {device_id} for user {} is already registered and token is identical\", headers.user.uuid);\n        return Ok(());\n    }\n\n    device.push_token = Some(token);\n    if let Err(e) = device.save(true, &conn).await {\n        err!(format!(\"An error occurred while trying to save the device push token: {e}\"));\n    }\n\n    register_push_device(&mut device, &conn).await?;\n\n    Ok(())\n}\n\n#[put(\"/devices/identifier/<device_id>/clear-token\")]\nasync fn put_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult {\n    // This only clears push token\n    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Controllers/DevicesController.cs#L215\n    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Services/Implementations/DeviceService.cs#L37\n    // This is somehow not implemented in any app, added it in case it is required\n    // 2025: Also, it looks like it only clears the first found device upstream, which is probably faulty.\n    //       This because currently multiple accounts could be on the same device/app and that would cause issues.\n    //       Vaultwarden removes the push-token for all devices, but this probably means we should also unregister all these devices.\n    if !CONFIG.push_enabled() {\n        return Ok(());\n    }\n\n    if let Some(device) = Device::find_by_uuid(&device_id, &conn).await {\n        Device::clear_push_token_by_uuid(&device_id, &conn).await?;\n        unregister_push_device(&device.push_uuid).await?;\n    }\n\n    Ok(())\n}\n\n// On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere\n#[post(\"/devices/identifier/<device_id>/clear-token\")]\nasync fn post_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult {\n    put_clear_device_token(device_id, conn).await\n}\n\n#[get(\"/tasks\")]\nfn get_tasks(_client_headers: ClientHeaders) -> JsonResult {\n    Ok(Json(json!({\n        \"data\": [],\n        \"object\": \"list\"\n    })))\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AuthRequestRequest {\n    access_code: String,\n    device_identifier: DeviceId,\n    email: String,\n    public_key: String,\n    // Not used for now\n    // #[serde(alias = \"type\")]\n    // _type: i32,\n}\n\n#[post(\"/auth-requests\", data = \"<data>\")]\nasync fn post_auth_request(\n    data: Json<AuthRequestRequest>,\n    client_headers: ClientHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data = data.into_inner();\n\n    let Some(user) = User::find_by_mail(&data.email, &conn).await else {\n        err!(\"AuthRequest doesn't exist\", \"User not found\")\n    };\n\n    // Validate device uuid and type\n    let device = match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &conn).await {\n        Some(device) if device.atype == client_headers.device_type => device,\n        _ => err!(\"AuthRequest doesn't exist\", \"Device verification failed\"),\n    };\n\n    let mut auth_request = AuthRequest::new(\n        user.uuid.clone(),\n        data.device_identifier.clone(),\n        client_headers.device_type,\n        client_headers.ip.ip.to_string(),\n        data.access_code,\n        data.public_key,\n    );\n    auth_request.save(&conn).await?;\n\n    nt.send_auth_request(&user.uuid, &auth_request.uuid, &device, &conn).await;\n\n    log_user_event(\n        EventType::UserRequestedDeviceApproval as i32,\n        &user.uuid,\n        client_headers.device_type,\n        &client_headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(json!({\n        \"id\": auth_request.uuid,\n        \"publicKey\": auth_request.public_key,\n        \"requestDeviceType\": DeviceType::from_i32(auth_request.device_type).to_string(),\n        \"requestIpAddress\": auth_request.request_ip,\n        \"key\": null,\n        \"masterPasswordHash\": null,\n        \"creationDate\": format_date(&auth_request.creation_date),\n        \"responseDate\": null,\n        \"requestApproved\": false,\n        \"origin\": CONFIG.domain_origin(),\n        \"object\": \"auth-request\"\n    })))\n}\n\n#[get(\"/auth-requests/<auth_request_id>\")]\nasync fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, conn: DbConn) -> JsonResult {\n    let Some(auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"AuthRequest doesn't exist\", \"Record not found or user uuid does not match\")\n    };\n\n    let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));\n\n    Ok(Json(json!({\n        \"id\": &auth_request_id,\n        \"publicKey\": auth_request.public_key,\n        \"requestDeviceType\": DeviceType::from_i32(auth_request.device_type).to_string(),\n        \"requestIpAddress\": auth_request.request_ip,\n        \"key\": auth_request.enc_key,\n        \"masterPasswordHash\": auth_request.master_password_hash,\n        \"creationDate\": format_date(&auth_request.creation_date),\n        \"responseDate\": response_date_utc,\n        \"requestApproved\": auth_request.approved,\n        \"origin\": CONFIG.domain_origin(),\n        \"object\":\"auth-request\"\n    })))\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AuthResponseRequest {\n    device_identifier: DeviceId,\n    key: String,\n    master_password_hash: Option<String>,\n    request_approved: bool,\n}\n\n#[put(\"/auth-requests/<auth_request_id>\", data = \"<data>\")]\nasync fn put_auth_request(\n    auth_request_id: AuthRequestId,\n    data: Json<AuthResponseRequest>,\n    headers: Headers,\n    conn: DbConn,\n    ant: AnonymousNotify<'_>,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data = data.into_inner();\n    let Some(mut auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"AuthRequest doesn't exist\", \"Record not found or user uuid does not match\")\n    };\n\n    if headers.device.uuid != data.device_identifier {\n        err!(\"AuthRequest doesn't exist\", \"Device verification failed\")\n    }\n\n    if auth_request.approved.is_some() {\n        err!(\"An authentication request with the same device already exists\")\n    }\n\n    let response_date = Utc::now().naive_utc();\n    let response_date_utc = format_date(&response_date);\n\n    if data.request_approved {\n        auth_request.approved = Some(data.request_approved);\n        auth_request.enc_key = Some(data.key);\n        auth_request.master_password_hash = data.master_password_hash;\n        auth_request.response_device_id = Some(data.device_identifier.clone());\n        auth_request.response_date = Some(response_date);\n        auth_request.save(&conn).await?;\n\n        ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await;\n        nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, &headers.device, &conn).await;\n\n        log_user_event(\n            EventType::OrganizationUserApprovedAuthRequest as i32,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n    } else {\n        // If denied, there's no reason to keep the request\n        auth_request.delete(&conn).await?;\n        log_user_event(\n            EventType::OrganizationUserRejectedAuthRequest as i32,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n    }\n\n    Ok(Json(json!({\n        \"id\": &auth_request_id,\n        \"publicKey\": auth_request.public_key,\n        \"requestDeviceType\": DeviceType::from_i32(auth_request.device_type).to_string(),\n        \"requestIpAddress\": auth_request.request_ip,\n        \"key\": auth_request.enc_key,\n        \"masterPasswordHash\": auth_request.master_password_hash,\n        \"creationDate\": format_date(&auth_request.creation_date),\n        \"responseDate\": response_date_utc,\n        \"requestApproved\": auth_request.approved,\n        \"origin\": CONFIG.domain_origin(),\n        \"object\":\"auth-request\"\n    })))\n}\n\n#[get(\"/auth-requests/<auth_request_id>/response?<code>\")]\nasync fn get_auth_request_response(\n    auth_request_id: AuthRequestId,\n    code: &str,\n    client_headers: ClientHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &conn).await else {\n        err!(\"AuthRequest doesn't exist\", \"User not found\")\n    };\n\n    if auth_request.device_type != client_headers.device_type\n        || auth_request.request_ip != client_headers.ip.ip.to_string()\n        || !auth_request.check_access_code(code)\n    {\n        err!(\"AuthRequest doesn't exist\", \"Invalid device, IP or code\")\n    }\n\n    let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));\n\n    Ok(Json(json!({\n        \"id\": &auth_request_id,\n        \"publicKey\": auth_request.public_key,\n        \"requestDeviceType\": DeviceType::from_i32(auth_request.device_type).to_string(),\n        \"requestIpAddress\": auth_request.request_ip,\n        \"key\": auth_request.enc_key,\n        \"masterPasswordHash\": auth_request.master_password_hash,\n        \"creationDate\": format_date(&auth_request.creation_date),\n        \"responseDate\": response_date_utc,\n        \"requestApproved\": auth_request.approved,\n        \"origin\": CONFIG.domain_origin(),\n        \"object\":\"auth-request\"\n    })))\n}\n\n// Now unused but not yet removed\n// cf https://github.com/bitwarden/clients/blob/9b2fbdba1c028bf3394064609630d2ec224baefa/libs/common/src/services/api.service.ts#L245\n#[get(\"/auth-requests\")]\nasync fn get_auth_requests(headers: Headers, conn: DbConn) -> JsonResult {\n    get_auth_requests_pending(headers, conn).await\n}\n\n#[get(\"/auth-requests/pending\")]\nasync fn get_auth_requests_pending(headers: Headers, conn: DbConn) -> JsonResult {\n    let auth_requests = AuthRequest::find_by_user(&headers.user.uuid, &conn).await;\n\n    Ok(Json(json!({\n        \"data\": auth_requests\n            .iter()\n            .filter(|request| request.approved.is_none())\n            .map(|request| {\n            let response_date_utc = request.response_date.map(|response_date| format_date(&response_date));\n\n            json!({\n                \"id\": request.uuid,\n                \"publicKey\": request.public_key,\n                \"requestDeviceType\": DeviceType::from_i32(request.device_type).to_string(),\n                \"requestIpAddress\": request.request_ip,\n                \"key\": request.enc_key,\n                \"masterPasswordHash\": request.master_password_hash,\n                \"creationDate\": format_date(&request.creation_date),\n                \"responseDate\": response_date_utc,\n                \"requestApproved\": request.approved,\n                \"origin\": CONFIG.domain_origin(),\n                \"object\":\"auth-request\"\n            })\n        }).collect::<Vec<Value>>(),\n        \"continuationToken\": null,\n        \"object\": \"list\"\n    })))\n}\n\npub async fn purge_auth_requests(pool: DbPool) {\n    debug!(\"Purging auth requests\");\n    if let Ok(conn) = pool.get().await {\n        AuthRequest::purge_expired_auth_requests(&conn).await;\n    } else {\n        error!(\"Failed to get DB connection while purging auth requests\")\n    }\n}\n"
  },
  {
    "path": "src/api/core/ciphers.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse chrono::{NaiveDateTime, Utc};\nuse num_traits::ToPrimitive;\nuse rocket::fs::TempFile;\nuse rocket::serde::json::Json;\nuse rocket::{\n    form::{Form, FromForm},\n    Route,\n};\nuse serde_json::Value;\n\nuse crate::auth::ClientVersion;\nuse crate::util::{save_temp_file, NumberOrString};\nuse crate::{\n    api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},\n    auth::Headers,\n    config::PathType,\n    crypto,\n    db::{\n        models::{\n            Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId,\n            CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType,\n            OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,\n        },\n        DbConn, DbPool,\n    },\n    CONFIG,\n};\n\nuse super::folders::FolderData;\n\npub fn routes() -> Vec<Route> {\n    // Note that many routes have an `admin` variant; this seems to be\n    // because the stored procedure that upstream Bitwarden uses to determine\n    // whether the user can edit a cipher doesn't take into account whether\n    // the user is an org owner/admin. The `admin` variant first checks\n    // whether the user is an owner/admin of the relevant org, and if so,\n    // allows the operation unconditionally.\n    //\n    // vaultwarden factors in the org owner/admin status as part of\n    // determining the write accessibility of a cipher, so most\n    // admin/non-admin implementations can be shared.\n    routes![\n        sync,\n        get_ciphers,\n        get_cipher,\n        get_cipher_admin,\n        get_cipher_details,\n        post_ciphers,\n        put_cipher_admin,\n        post_ciphers_admin,\n        post_ciphers_create,\n        post_ciphers_import,\n        get_attachment,\n        post_attachment_v2,\n        post_attachment_v2_data,\n        post_attachment,       // legacy\n        post_attachment_admin, // legacy\n        post_attachment_share,\n        delete_attachment_post,\n        delete_attachment_post_admin,\n        delete_attachment,\n        delete_attachment_admin,\n        post_cipher_admin,\n        post_cipher_share,\n        put_cipher_share,\n        put_cipher_share_selected,\n        post_cipher,\n        post_cipher_partial,\n        put_cipher,\n        put_cipher_partial,\n        delete_cipher_post,\n        delete_cipher_post_admin,\n        delete_cipher_put,\n        delete_cipher_put_admin,\n        delete_cipher,\n        delete_cipher_admin,\n        delete_cipher_selected,\n        delete_cipher_selected_post,\n        delete_cipher_selected_put,\n        delete_cipher_selected_admin,\n        delete_cipher_selected_post_admin,\n        delete_cipher_selected_put_admin,\n        restore_cipher_put,\n        restore_cipher_put_admin,\n        restore_cipher_selected,\n        restore_cipher_selected_admin,\n        delete_all,\n        move_cipher_selected,\n        move_cipher_selected_put,\n        put_collections2_update,\n        post_collections2_update,\n        put_collections_update,\n        post_collections_update,\n        post_collections_admin,\n        put_collections_admin,\n    ]\n}\n\npub async fn purge_trashed_ciphers(pool: DbPool) {\n    debug!(\"Purging trashed ciphers\");\n    if let Ok(conn) = pool.get().await {\n        Cipher::purge_trash(&conn).await;\n    } else {\n        error!(\"Failed to get DB connection while purging trashed ciphers\")\n    }\n}\n\n#[derive(FromForm, Default)]\nstruct SyncData {\n    #[field(name = \"excludeDomains\")]\n    exclude_domains: bool, // Default: 'false'\n}\n\n#[get(\"/sync?<data..>\")]\nasync fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVersion>, conn: DbConn) -> JsonResult {\n    let user_json = headers.user.to_json(&conn).await;\n\n    // Get all ciphers which are visible by the user\n    let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;\n\n    // Filter out SSH keys if the client version is less than 2024.12.0\n    let show_ssh_keys = if let Some(client_version) = client_version {\n        let ver_match = semver::VersionReq::parse(\">=2024.12.0\").unwrap();\n        ver_match.matches(&client_version.0)\n    } else {\n        false\n    };\n    if !show_ssh_keys {\n        ciphers.retain(|c| c.atype != 5);\n    }\n\n    let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;\n\n    // Lets generate the ciphers_json using all the gathered info\n    let mut ciphers_json = Vec::with_capacity(ciphers.len());\n    for c in ciphers {\n        ciphers_json.push(\n            c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &conn).await?,\n        );\n    }\n\n    let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &conn).await;\n    let mut collections_json = Vec::with_capacity(collections.len());\n    for c in collections {\n        collections_json.push(c.to_json_details(&headers.user.uuid, Some(&cipher_sync_data), &conn).await);\n    }\n\n    let folders_json: Vec<Value> =\n        Folder::find_by_user(&headers.user.uuid, &conn).await.iter().map(Folder::to_json).collect();\n\n    let sends_json: Vec<Value> =\n        Send::find_by_user(&headers.user.uuid, &conn).await.iter().map(Send::to_json).collect();\n\n    let policies_json: Vec<Value> =\n        OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn).await.iter().map(OrgPolicy::to_json).collect();\n\n    let domains_json = if data.exclude_domains {\n        Value::Null\n    } else {\n        api::core::_get_eq_domains(&headers, true).into_inner()\n    };\n\n    // This is very similar to the the userDecryptionOptions sent in connect/token,\n    // but as of 2025-12-19 they're both using different casing conventions.\n    let has_master_password = !headers.user.password_hash.is_empty();\n    let master_password_unlock = if has_master_password {\n        json!({\n            \"kdf\": {\n                \"kdfType\": headers.user.client_kdf_type,\n                \"iterations\": headers.user.client_kdf_iter,\n                \"memory\": headers.user.client_kdf_memory,\n                \"parallelism\": headers.user.client_kdf_parallelism\n            },\n            // This field is named inconsistently and will be removed and replaced by the \"wrapped\" variant in the apps.\n            // https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26\n            \"masterKeyEncryptedUserKey\": headers.user.akey,\n            \"masterKeyWrappedUserKey\": headers.user.akey,\n            \"salt\": headers.user.email\n        })\n    } else {\n        Value::Null\n    };\n\n    Ok(Json(json!({\n        \"profile\": user_json,\n        \"folders\": folders_json,\n        \"collections\": collections_json,\n        \"policies\": policies_json,\n        \"ciphers\": ciphers_json,\n        \"domains\": domains_json,\n        \"sends\": sends_json,\n        \"userDecryption\": {\n            \"masterPasswordUnlock\": master_password_unlock,\n        },\n        \"object\": \"sync\"\n    })))\n}\n\n#[get(\"/ciphers\")]\nasync fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {\n    let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;\n    let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;\n\n    let mut ciphers_json = Vec::with_capacity(ciphers.len());\n    for c in ciphers {\n        ciphers_json.push(\n            c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &conn).await?,\n        );\n    }\n\n    Ok(Json(json!({\n      \"data\": ciphers_json,\n      \"object\": \"list\",\n      \"continuationToken\": null\n    })))\n}\n\n#[get(\"/ciphers/<cipher_id>\")]\nasync fn get_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn) -> JsonResult {\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not owned by user\")\n    }\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n#[get(\"/ciphers/<cipher_id>/admin\")]\nasync fn get_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn) -> JsonResult {\n    // TODO: Implement this correctly\n    get_cipher(cipher_id, headers, conn).await\n}\n\n#[get(\"/ciphers/<cipher_id>/details\")]\nasync fn get_cipher_details(cipher_id: CipherId, headers: Headers, conn: DbConn) -> JsonResult {\n    get_cipher(cipher_id, headers, conn).await\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CipherData {\n    // Id is optional as it is included only in bulk share\n    pub id: Option<CipherId>,\n    // Folder id is not included in import\n    pub folder_id: Option<FolderId>,\n    // TODO: Some of these might appear all the time, no need for Option\n    #[serde(alias = \"organizationID\")]\n    pub organization_id: Option<OrganizationId>,\n\n    key: Option<String>,\n\n    /*\n    Login = 1,\n    SecureNote = 2,\n    Card = 3,\n    Identity = 4,\n    SshKey = 5\n    */\n    pub r#type: i32,\n    pub name: String,\n    pub notes: Option<String>,\n    fields: Option<Value>,\n\n    // Only one of these should exist, depending on type\n    login: Option<Value>,\n    secure_note: Option<Value>,\n    card: Option<Value>,\n    identity: Option<Value>,\n    ssh_key: Option<Value>,\n\n    favorite: Option<bool>,\n    reprompt: Option<i32>,\n\n    pub password_history: Option<Value>,\n\n    // These are used during key rotation\n    // 'Attachments' is unused, contains map of {id: filename}\n    #[allow(dead_code)]\n    attachments: Option<Value>,\n    attachments2: Option<HashMap<AttachmentId, Attachments2Data>>,\n\n    // The revision datetime (in ISO 8601 format) of the client's local copy\n    // of the cipher. This is used to prevent a client from updating a cipher\n    // when it doesn't have the latest version, as that can result in data\n    // loss. It's not an error when no value is provided; this can happen\n    // when using older client versions, or if the operation doesn't involve\n    // updating an existing cipher.\n    last_known_revision_date: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct PartialCipherData {\n    folder_id: Option<FolderId>,\n    favorite: bool,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Attachments2Data {\n    file_name: String,\n    key: String,\n}\n\n/// Called when an org admin clones an org cipher.\n#[post(\"/ciphers/admin\", data = \"<data>\")]\nasync fn post_ciphers_admin(data: Json<ShareCipherData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    post_ciphers_create(data, headers, conn, nt).await\n}\n\n/// Called when creating a new org-owned cipher, or cloning a cipher (whether\n/// user- or org-owned). When cloning a cipher to a user-owned cipher,\n/// `organizationId` is null.\n#[post(\"/ciphers/create\", data = \"<data>\")]\nasync fn post_ciphers_create(\n    data: Json<ShareCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let mut data: ShareCipherData = data.into_inner();\n\n    // This check is usually only needed in update_cipher_from_data(), but we\n    // need it here as well to avoid creating an empty cipher in the call to\n    // cipher.save() below.\n    enforce_personal_ownership_policy(Some(&data.cipher), &headers, &conn).await?;\n\n    let mut cipher = Cipher::new(data.cipher.r#type, data.cipher.name.clone());\n    cipher.user_uuid = Some(headers.user.uuid.clone());\n    cipher.save(&conn).await?;\n\n    // When cloning a cipher, the Bitwarden clients seem to set this field\n    // based on the cipher being cloned (when creating a new cipher, it's set\n    // to null as expected). However, `cipher.created_at` is initialized to\n    // the current time, so the stale data check will end up failing down the\n    // line. Since this function only creates new ciphers (whether by cloning\n    // or otherwise), we can just ignore this field entirely.\n    data.cipher.last_known_revision_date = None;\n\n    let res = share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &nt, None).await;\n    if res.is_err() {\n        cipher.delete(&conn).await?;\n    }\n    res\n}\n\n/// Called when creating a new user-owned cipher.\n#[post(\"/ciphers\", data = \"<data>\")]\nasync fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    let mut data: CipherData = data.into_inner();\n\n    // The web/browser clients set this field to null as expected, but the\n    // mobile clients seem to set the invalid value `0001-01-01T00:00:00`,\n    // which results in a warning message being logged. This field isn't\n    // needed when creating a new cipher, so just ignore it unconditionally.\n    data.last_known_revision_date = None;\n\n    let mut cipher = Cipher::new(data.r#type, data.name.clone());\n    update_cipher_from_data(&mut cipher, data, &headers, None, &conn, &nt, UpdateType::SyncCipherCreate).await?;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n/// Enforces the personal ownership policy on user-owned ciphers, if applicable.\n/// A non-owner/admin user belonging to an org with the personal ownership policy\n/// enabled isn't allowed to create new user-owned ciphers or modify existing ones\n/// (that were created before the policy was applicable to the user). The user is\n/// allowed to delete or share such ciphers to an org, however.\n///\n/// Ref: https://bitwarden.com/help/article/policies/#personal-ownership\nasync fn enforce_personal_ownership_policy(data: Option<&CipherData>, headers: &Headers, conn: &DbConn) -> EmptyResult {\n    if data.is_none() || data.unwrap().organization_id.is_none() {\n        let user_id = &headers.user.uuid;\n        let policy_type = OrgPolicyType::PersonalOwnership;\n        if OrgPolicy::is_applicable_to_user(user_id, policy_type, None, conn).await {\n            err!(\"Due to an Enterprise Policy, you are restricted from saving items to your personal vault.\")\n        }\n    }\n    Ok(())\n}\n\npub async fn update_cipher_from_data(\n    cipher: &mut Cipher,\n    data: CipherData,\n    headers: &Headers,\n    shared_to_collections: Option<Vec<CollectionId>>,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n    ut: UpdateType,\n) -> EmptyResult {\n    enforce_personal_ownership_policy(Some(&data), headers, conn).await?;\n\n    // Check that the client isn't updating an existing cipher with stale data.\n    // And only perform this check when not importing ciphers, else the date/time check will fail.\n    if ut != UpdateType::None {\n        if let Some(dt) = data.last_known_revision_date {\n            match NaiveDateTime::parse_from_str(&dt, \"%+\") {\n                // ISO 8601 format\n                Err(err) => warn!(\"Error parsing LastKnownRevisionDate '{dt}': {err}\"),\n                Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {\n                    err!(\"The client copy of this cipher is out of date. Resync the client and try again.\")\n                }\n                Ok(_) => (),\n            }\n        }\n    }\n\n    if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.organization_id {\n        err!(\"Organization mismatch. Please resync the client before updating the cipher\")\n    }\n\n    if let Some(note) = &data.notes {\n        let max_note_size = CONFIG._max_note_size();\n        if note.len() > max_note_size {\n            err!(format!(\"The field Notes exceeds the maximum encrypted value length of {max_note_size} characters.\"))\n        }\n    }\n\n    // Check if this cipher is being transferred from a personal to an organization vault\n    let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some();\n\n    if let Some(org_id) = data.organization_id {\n        match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await {\n            None => err!(\"You don't have permission to add item to organization\"),\n            Some(member) => {\n                if shared_to_collections.is_some()\n                    || member.has_full_access()\n                    || cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await\n                {\n                    cipher.organization_uuid = Some(org_id);\n                    // After some discussion in PR #1329 re-added the user_uuid = None again.\n                    // TODO: Audit/Check the whole save/update cipher chain.\n                    // Upstream uses the user_uuid to allow a cipher added by a user to an org to still allow the user to view/edit the cipher\n                    // even when the user has hide-passwords configured as there policy.\n                    // Removing the line below would fix that, but we have to check which effect this would have on the rest of the code.\n                    cipher.user_uuid = None;\n                } else {\n                    err!(\"You don't have permission to add cipher directly to organization\")\n                }\n            }\n        }\n    } else {\n        cipher.user_uuid = Some(headers.user.uuid.clone());\n    }\n\n    if let Some(ref folder_id) = data.folder_id {\n        if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, conn).await.is_none() {\n            err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\");\n        }\n    }\n\n    // Modify attachments name and keys when rotating\n    if let Some(attachments) = data.attachments2 {\n        for (id, attachment) in attachments {\n            let mut saved_att = match Attachment::find_by_id(&id, conn).await {\n                Some(att) => att,\n                None => {\n                    // Warn and continue here.\n                    // A missing attachment means it was removed via an other client.\n                    // Also the Desktop Client supports removing attachments and save an update afterwards.\n                    // Bitwarden it self ignores these mismatches server side.\n                    warn!(\"Attachment {id} doesn't exist\");\n                    continue;\n                }\n            };\n\n            if saved_att.cipher_uuid != cipher.uuid {\n                // Warn and break here since cloning ciphers provides attachment data but will not be cloned.\n                // If we error out here it will break the whole cloning and causes empty ciphers to appear.\n                warn!(\"Attachment is not owned by the cipher\");\n                break;\n            }\n\n            saved_att.akey = Some(attachment.key);\n            saved_att.file_name = attachment.file_name;\n\n            saved_att.save(conn).await?;\n        }\n    }\n\n    // Cleanup cipher data, like removing the 'Response' key.\n    // This key is somewhere generated during Javascript so no way for us this fix this.\n    // Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.\n    // We do not mind which data is in it, the keep our model more flexible when there are upstream changes.\n    // But, we at least know we do not need to store and return this specific key.\n    fn _clean_cipher_data(mut json_data: Value) -> Value {\n        if json_data.is_array() {\n            json_data.as_array_mut().unwrap().iter_mut().for_each(|ref mut f| {\n                f.as_object_mut().unwrap().remove(\"response\");\n            });\n        };\n        json_data\n    }\n\n    let type_data_opt = match data.r#type {\n        1 => data.login,\n        2 => data.secure_note,\n        3 => data.card,\n        4 => data.identity,\n        5 => data.ssh_key,\n        _ => err!(\"Invalid type\"),\n    };\n\n    let type_data = match type_data_opt {\n        Some(mut data) => {\n            // Remove the 'Response' key from the base object.\n            data.as_object_mut().unwrap().remove(\"response\");\n            // Remove the 'Response' key from every Uri.\n            if data[\"uris\"].is_array() {\n                data[\"uris\"] = _clean_cipher_data(data[\"uris\"].clone());\n            }\n            data\n        }\n        None => err!(\"Data missing\"),\n    };\n\n    cipher.key = data.key;\n    cipher.name = data.name;\n    cipher.notes = data.notes;\n    cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string());\n    cipher.data = type_data.to_string();\n    cipher.password_history = data.password_history.map(|f| f.to_string());\n    cipher.reprompt = data.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32);\n\n    cipher.save(conn).await?;\n    cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;\n    cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;\n\n    if ut != UpdateType::None {\n        // Only log events for organizational ciphers\n        if let Some(org_id) = &cipher.organization_uuid {\n            let event_type = match (&ut, transfer_cipher) {\n                (UpdateType::SyncCipherCreate, true) => EventType::CipherCreated,\n                (UpdateType::SyncCipherUpdate, true) => EventType::CipherShared,\n                (_, _) => EventType::CipherUpdated,\n            };\n\n            log_event(\n                event_type as i32,\n                &cipher.uuid,\n                org_id,\n                &headers.user.uuid,\n                headers.device.atype,\n                &headers.ip.ip,\n                conn,\n            )\n            .await;\n        }\n        nt.send_cipher_update(\n            ut,\n            cipher,\n            &cipher.update_users_revision(conn).await,\n            &headers.device,\n            shared_to_collections,\n            conn,\n        )\n        .await;\n    }\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ImportData {\n    ciphers: Vec<CipherData>,\n    folders: Vec<FolderData>,\n    folder_relationships: Vec<RelationsData>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RelationsData {\n    // Cipher id\n    key: usize,\n    // Folder id\n    value: usize,\n}\n\n#[post(\"/ciphers/import\", data = \"<data>\")]\nasync fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    enforce_personal_ownership_policy(None, &headers, &conn).await?;\n\n    let data: ImportData = data.into_inner();\n\n    // Validate the import before continuing\n    // Bitwarden does not process the import if there is one item invalid.\n    // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.\n    // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.\n    Cipher::validate_cipher_data(&data.ciphers)?;\n\n    // Read and create the folders\n    let existing_folders: HashSet<Option<FolderId>> =\n        Folder::find_by_user(&headers.user.uuid, &conn).await.into_iter().map(|f| Some(f.uuid)).collect();\n    let mut folders: Vec<FolderId> = Vec::with_capacity(data.folders.len());\n    for folder in data.folders.into_iter() {\n        let folder_id = if existing_folders.contains(&folder.id) {\n            folder.id.unwrap()\n        } else {\n            let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);\n            new_folder.save(&conn).await?;\n            new_folder.uuid\n        };\n\n        folders.push(folder_id);\n    }\n\n    // Read the relations between folders and ciphers\n    // Ciphers can only be in one folder at the same time\n    let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());\n    for relation in data.folder_relationships {\n        relations_map.insert(relation.key, relation.value);\n    }\n\n    // Read and create the ciphers\n    for (index, mut cipher_data) in data.ciphers.into_iter().enumerate() {\n        let folder_id = relations_map.get(&index).map(|i| folders[*i].clone());\n        cipher_data.folder_id = folder_id;\n\n        let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());\n        update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &conn, &nt, UpdateType::None).await?;\n    }\n\n    let mut user = headers.user;\n    user.update_revision(&conn).await?;\n    nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;\n\n    Ok(())\n}\n\n/// Called when an org admin modifies an existing org cipher.\n#[put(\"/ciphers/<cipher_id>/admin\", data = \"<data>\")]\nasync fn put_cipher_admin(\n    cipher_id: CipherId,\n    data: Json<CipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    put_cipher(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/admin\", data = \"<data>\")]\nasync fn post_cipher_admin(\n    cipher_id: CipherId,\n    data: Json<CipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    post_cipher(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>\", data = \"<data>\")]\nasync fn post_cipher(\n    cipher_id: CipherId,\n    data: Json<CipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    put_cipher(cipher_id, data, headers, conn, nt).await\n}\n\n#[put(\"/ciphers/<cipher_id>\", data = \"<data>\")]\nasync fn put_cipher(\n    cipher_id: CipherId,\n    data: Json<CipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data: CipherData = data.into_inner();\n\n    let Some(mut cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    // TODO: Check if only the folder ID or favorite status is being changed.\n    // These are per-user properties that technically aren't part of the\n    // cipher itself, so the user shouldn't need write access to change these.\n    // Interestingly, upstream Bitwarden doesn't properly handle this either.\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not write accessible\")\n    }\n\n    update_cipher_from_data(&mut cipher, data, &headers, None, &conn, &nt, UpdateType::SyncCipherUpdate).await?;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n#[post(\"/ciphers/<cipher_id>/partial\", data = \"<data>\")]\nasync fn post_cipher_partial(\n    cipher_id: CipherId,\n    data: Json<PartialCipherData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    put_cipher_partial(cipher_id, data, headers, conn).await\n}\n\n// Only update the folder and favorite for the user, since this cipher is read-only\n#[put(\"/ciphers/<cipher_id>/partial\", data = \"<data>\")]\nasync fn put_cipher_partial(\n    cipher_id: CipherId,\n    data: Json<PartialCipherData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    let data: PartialCipherData = data.into_inner();\n\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher does not exist\")\n    };\n\n    if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher does not exist\", \"Cipher is not accessible for the current user\")\n    }\n\n    if let Some(ref folder_id) = data.folder_id {\n        if Folder::find_by_uuid_and_user(folder_id, &headers.user.uuid, &conn).await.is_none() {\n            err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\");\n        }\n    }\n\n    // Move cipher\n    cipher.move_to_folder(data.folder_id.clone(), &headers.user.uuid, &conn).await?;\n    // Update favorite\n    cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &conn).await?;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct CollectionsAdminData {\n    #[serde(alias = \"CollectionIds\")]\n    collection_ids: Vec<CollectionId>,\n}\n\n#[put(\"/ciphers/<cipher_id>/collections_v2\", data = \"<data>\")]\nasync fn put_collections2_update(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    post_collections2_update(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/collections_v2\", data = \"<data>\")]\nasync fn post_collections2_update(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let cipher_details = post_collections_update(cipher_id, data, headers, conn, nt).await?;\n    Ok(Json(json!({ // AttachmentUploadDataResponseModel\n        \"object\": \"optionalCipherDetails\",\n        \"unavailable\": false,\n        \"cipher\": *cipher_details\n    })))\n}\n\n#[put(\"/ciphers/<cipher_id>/collections\", data = \"<data>\")]\nasync fn put_collections_update(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    post_collections_update(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/collections\", data = \"<data>\")]\nasync fn post_collections_update(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data: CollectionsAdminData = data.into_inner();\n\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &conn).await {\n        err!(\"Collection cannot be changed\")\n    }\n\n    let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);\n    let current_collections =\n        HashSet::<CollectionId>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &conn).await);\n\n    for collection in posted_collections.symmetric_difference(&current_collections) {\n        match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {\n            None => err!(\"Invalid collection ID provided\"),\n            Some(collection) => {\n                if collection.is_writable_by_user(&headers.user.uuid, &conn).await {\n                    if posted_collections.contains(&collection.uuid) {\n                        // Add to collection\n                        CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;\n                    } else {\n                        // Remove from collection\n                        CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;\n                    }\n                } else {\n                    err!(\"No rights to modify the collection\")\n                }\n            }\n        }\n    }\n\n    nt.send_cipher_update(\n        UpdateType::SyncCipherUpdate,\n        &cipher,\n        &cipher.update_users_revision(&conn).await,\n        &headers.device,\n        Some(Vec::from_iter(posted_collections)),\n        &conn,\n    )\n    .await;\n\n    log_event(\n        EventType::CipherUpdatedCollections as i32,\n        &cipher.uuid,\n        &cipher.organization_uuid.clone().unwrap(),\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n#[put(\"/ciphers/<cipher_id>/collections-admin\", data = \"<data>\")]\nasync fn put_collections_admin(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    post_collections_admin(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/collections-admin\", data = \"<data>\")]\nasync fn post_collections_admin(\n    cipher_id: CipherId,\n    data: Json<CollectionsAdminData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let data: CollectionsAdminData = data.into_inner();\n\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_in_editable_collection_by_user(&headers.user.uuid, &conn).await {\n        err!(\"Collection cannot be changed\")\n    }\n\n    let posted_collections = HashSet::<CollectionId>::from_iter(data.collection_ids);\n    let current_collections =\n        HashSet::<CollectionId>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &conn).await);\n\n    for collection in posted_collections.symmetric_difference(&current_collections) {\n        match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &conn).await {\n            None => err!(\"Invalid collection ID provided\"),\n            Some(collection) => {\n                if collection.is_writable_by_user(&headers.user.uuid, &conn).await {\n                    if posted_collections.contains(&collection.uuid) {\n                        // Add to collection\n                        CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;\n                    } else {\n                        // Remove from collection\n                        CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;\n                    }\n                } else {\n                    err!(\"No rights to modify the collection\")\n                }\n            }\n        }\n    }\n\n    nt.send_cipher_update(\n        UpdateType::SyncCipherUpdate,\n        &cipher,\n        &cipher.update_users_revision(&conn).await,\n        &headers.device,\n        Some(Vec::from_iter(posted_collections)),\n        &conn,\n    )\n    .await;\n\n    log_event(\n        EventType::CipherUpdatedCollections as i32,\n        &cipher.uuid,\n        &cipher.organization_uuid.unwrap(),\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ShareCipherData {\n    #[serde(alias = \"Cipher\")]\n    cipher: CipherData,\n    #[serde(alias = \"CollectionIds\")]\n    collection_ids: Vec<CollectionId>,\n}\n\n#[post(\"/ciphers/<cipher_id>/share\", data = \"<data>\")]\nasync fn post_cipher_share(\n    cipher_id: CipherId,\n    data: Json<ShareCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data: ShareCipherData = data.into_inner();\n\n    share_cipher_by_uuid(&cipher_id, data, &headers, &conn, &nt, None).await\n}\n\n#[put(\"/ciphers/<cipher_id>/share\", data = \"<data>\")]\nasync fn put_cipher_share(\n    cipher_id: CipherId,\n    data: Json<ShareCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data: ShareCipherData = data.into_inner();\n\n    share_cipher_by_uuid(&cipher_id, data, &headers, &conn, &nt, None).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ShareSelectedCipherData {\n    ciphers: Vec<CipherData>,\n    collection_ids: Vec<CollectionId>,\n}\n\n#[put(\"/ciphers/share\", data = \"<data>\")]\nasync fn put_cipher_share_selected(\n    data: Json<ShareSelectedCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let mut data: ShareSelectedCipherData = data.into_inner();\n\n    if data.ciphers.is_empty() {\n        err!(\"You must select at least one cipher.\")\n    }\n\n    if data.collection_ids.is_empty() {\n        err!(\"You must select at least one collection.\")\n    }\n\n    for cipher in data.ciphers.iter() {\n        if cipher.id.is_none() {\n            err!(\"Request missing ids field\")\n        }\n    }\n\n    while let Some(cipher) = data.ciphers.pop() {\n        let mut shared_cipher_data = ShareCipherData {\n            cipher,\n            collection_ids: data.collection_ids.clone(),\n        };\n\n        match shared_cipher_data.cipher.id.take() {\n            Some(id) => {\n                share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn, &nt, Some(UpdateType::None)).await?\n            }\n            None => err!(\"Request missing ids field\"),\n        };\n    }\n\n    // Multi share actions do not send out a push for each cipher, we need to send a general sync here\n    nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;\n\n    Ok(())\n}\n\nasync fn share_cipher_by_uuid(\n    cipher_id: &CipherId,\n    data: ShareCipherData,\n    headers: &Headers,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n    override_ut: Option<UpdateType>,\n) -> JsonResult {\n    let mut cipher = match Cipher::find_by_uuid(cipher_id, conn).await {\n        Some(cipher) => {\n            if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {\n                cipher\n            } else {\n                err!(\"Cipher is not write accessible\")\n            }\n        }\n        None => err!(\"Cipher doesn't exist\"),\n    };\n\n    let mut shared_to_collections = vec![];\n\n    if let Some(organization_id) = &data.cipher.organization_id {\n        for col_id in &data.collection_ids {\n            match Collection::find_by_uuid_and_org(col_id, organization_id, conn).await {\n                None => err!(\"Invalid collection ID provided\"),\n                Some(collection) => {\n                    if collection.is_writable_by_user(&headers.user.uuid, conn).await {\n                        CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?;\n                        shared_to_collections.push(collection.uuid);\n                    } else {\n                        err!(\"No rights to modify the collection\")\n                    }\n                }\n            }\n        }\n    };\n\n    // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.\n    // If there is an override, like when handling multiple items, we want to prevent a push notification for every single item\n    let ut = if let Some(ut) = override_ut {\n        ut\n    } else if data.cipher.last_known_revision_date.is_some() {\n        UpdateType::SyncCipherUpdate\n    } else {\n        UpdateType::SyncCipherCreate\n    };\n\n    update_cipher_from_data(&mut cipher, data.cipher, headers, Some(shared_to_collections), conn, nt, ut).await?;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))\n}\n\n/// v2 API for downloading an attachment. This just redirects the client to\n/// the actual location of an attachment.\n///\n/// Upstream added this v2 API to support direct download of attachments from\n/// their object storage service. For self-hosted instances, it basically just\n/// redirects to the same location as before the v2 API.\n#[get(\"/ciphers/<cipher_id>/attachment/<attachment_id>\")]\nasync fn get_attachment(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not accessible\")\n    }\n\n    match Attachment::find_by_id(&attachment_id, &conn).await {\n        Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host).await?)),\n        Some(_) => err!(\"Attachment doesn't belong to cipher\"),\n        None => err!(\"Attachment doesn't exist\"),\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AttachmentRequestData {\n    key: String,\n    file_name: String,\n    file_size: NumberOrString,\n    admin_request: Option<bool>, // true when attaching from an org vault view\n}\n\nenum FileUploadType {\n    Direct = 0,\n    // Azure = 1, // only used upstream\n}\n\n/// v2 API for creating an attachment associated with a cipher.\n/// This redirects the client to the API it should use to upload the attachment.\n/// For upstream's cloud-hosted service, it's an Azure object storage API.\n/// For self-hosted instances, it's another API on the local instance.\n#[post(\"/ciphers/<cipher_id>/attachment/v2\", data = \"<data>\")]\nasync fn post_attachment_v2(\n    cipher_id: CipherId,\n    data: Json<AttachmentRequestData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not write accessible\")\n    }\n\n    let data: AttachmentRequestData = data.into_inner();\n    let file_size = data.file_size.into_i64()?;\n\n    if file_size < 0 {\n        err!(\"Attachment size can't be negative\")\n    }\n    let attachment_id = crypto::generate_attachment_id();\n    let attachment =\n        Attachment::new(attachment_id.clone(), cipher.uuid.clone(), data.file_name, file_size, Some(data.key));\n    attachment.save(&conn).await.expect(\"Error saving attachment\");\n\n    let url = format!(\"/ciphers/{}/attachment/{attachment_id}\", cipher.uuid);\n    let response_key = match data.admin_request {\n        Some(b) if b => \"cipherMiniResponse\",\n        _ => \"cipherResponse\",\n    };\n\n    Ok(Json(json!({ // AttachmentUploadDataResponseModel\n        \"object\": \"attachment-fileUpload\",\n        \"attachmentId\": attachment_id,\n        \"url\": url,\n        \"fileUploadType\": FileUploadType::Direct as i32,\n        response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?,\n    })))\n}\n\n#[derive(FromForm)]\nstruct UploadData<'f> {\n    key: Option<String>,\n    data: TempFile<'f>,\n}\n\n/// Saves the data content of an attachment to a file. This is common code\n/// shared between the v2 and legacy attachment APIs.\n///\n/// When used with the legacy API, this function is responsible for creating\n/// the attachment database record, so `attachment` is None.\n///\n/// When used with the v2 API, post_attachment_v2() has already created the\n/// database record, which is passed in as `attachment`.\nasync fn save_attachment(\n    mut attachment: Option<Attachment>,\n    cipher_id: CipherId,\n    data: Form<UploadData<'_>>,\n    headers: &Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> Result<(Cipher, DbConn), crate::error::Error> {\n    let data = data.into_inner();\n\n    let Some(size) = data.data.len().to_i64() else {\n        err!(\"Attachment data size overflow\");\n    };\n    if size < 0 {\n        err!(\"Attachment size can't be negative\")\n    }\n\n    let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {\n        err!(\"Cipher is not write accessible\")\n    }\n\n    // In the v2 API, the attachment record has already been created,\n    // so the size limit needs to be adjusted to account for that.\n    let size_adjust = match &attachment {\n        None => 0,              // Legacy API\n        Some(a) => a.file_size, // v2 API\n    };\n\n    let size_limit = if let Some(ref user_id) = cipher.user_uuid {\n        match CONFIG.user_attachment_limit() {\n            Some(0) => err!(\"Attachments are disabled\"),\n            Some(limit_kb) => {\n                let already_used = Attachment::size_by_user(user_id, &conn).await;\n                let left = limit_kb\n                    .checked_mul(1024)\n                    .and_then(|l| l.checked_sub(already_used))\n                    .and_then(|l| l.checked_add(size_adjust));\n\n                let Some(left) = left else {\n                    err!(\"Attachment size overflow\");\n                };\n\n                if left <= 0 {\n                    err!(\"Attachment storage limit reached! Delete some attachments to free up space\")\n                }\n\n                Some(left)\n            }\n            None => None,\n        }\n    } else if let Some(ref org_id) = cipher.organization_uuid {\n        match CONFIG.org_attachment_limit() {\n            Some(0) => err!(\"Attachments are disabled\"),\n            Some(limit_kb) => {\n                let already_used = Attachment::size_by_org(org_id, &conn).await;\n                let left = limit_kb\n                    .checked_mul(1024)\n                    .and_then(|l| l.checked_sub(already_used))\n                    .and_then(|l| l.checked_add(size_adjust));\n\n                let Some(left) = left else {\n                    err!(\"Attachment size overflow\");\n                };\n\n                if left <= 0 {\n                    err!(\"Attachment storage limit reached! Delete some attachments to free up space\")\n                }\n\n                Some(left)\n            }\n            None => None,\n        }\n    } else {\n        err!(\"Cipher is neither owned by a user nor an organization\");\n    };\n\n    if let Some(size_limit) = size_limit {\n        if size > size_limit {\n            err!(\"Attachment storage limit exceeded with this file\");\n        }\n    }\n\n    let file_id = match &attachment {\n        Some(attachment) => attachment.id.clone(), // v2 API\n        None => crypto::generate_attachment_id(),  // Legacy API\n    };\n\n    if let Some(attachment) = &mut attachment {\n        // v2 API\n\n        // Check the actual size against the size initially provided by\n        // the client. Upstream allows +/- 1 MiB deviation from this\n        // size, but it's not clear when or why this is needed.\n        const LEEWAY: i64 = 1024 * 1024; // 1 MiB\n        let Some(max_size) = attachment.file_size.checked_add(LEEWAY) else {\n            err!(\"Invalid attachment size max\")\n        };\n        let Some(min_size) = attachment.file_size.checked_sub(LEEWAY) else {\n            err!(\"Invalid attachment size min\")\n        };\n\n        if min_size <= size && size <= max_size {\n            if size != attachment.file_size {\n                // Update the attachment with the actual file size.\n                attachment.file_size = size;\n                attachment.save(&conn).await.expect(\"Error updating attachment\");\n            }\n        } else {\n            attachment.delete(&conn).await.ok();\n\n            err!(format!(\"Attachment size mismatch (expected within [{min_size}, {max_size}], got {size})\"));\n        }\n    } else {\n        // Legacy API\n\n        // SAFETY: This value is only stored in the database and is not used to access the file system.\n        // As a result, the conditions specified by Rocket [0] are met and this is safe to use.\n        // [0]: https://docs.rs/rocket/latest/rocket/fs/struct.FileName.html#-danger-\n        let encrypted_filename = data.data.raw_name().map(|s| s.dangerous_unsafe_unsanitized_raw().to_string());\n\n        if encrypted_filename.is_none() {\n            err!(\"No filename provided\")\n        }\n        if data.key.is_none() {\n            err!(\"No attachment key provided\")\n        }\n        let attachment =\n            Attachment::new(file_id.clone(), cipher_id.clone(), encrypted_filename.unwrap(), size, data.key);\n        attachment.save(&conn).await.expect(\"Error saving attachment\");\n    }\n\n    save_temp_file(&PathType::Attachments, &format!(\"{cipher_id}/{file_id}\"), data.data, true).await?;\n\n    nt.send_cipher_update(\n        UpdateType::SyncCipherUpdate,\n        &cipher,\n        &cipher.update_users_revision(&conn).await,\n        &headers.device,\n        None,\n        &conn,\n    )\n    .await;\n\n    if let Some(org_id) = &cipher.organization_uuid {\n        log_event(\n            EventType::CipherAttachmentCreated as i32,\n            &cipher.uuid,\n            org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n    }\n\n    Ok((cipher, conn))\n}\n\n/// v2 API for uploading the actual data content of an attachment.\n/// This route needs a rank specified so that Rocket prioritizes the\n/// /ciphers/<cipher_id>/attachment/v2 route, which would otherwise conflict\n/// with this one.\n#[post(\"/ciphers/<cipher_id>/attachment/<attachment_id>\", format = \"multipart/form-data\", data = \"<data>\", rank = 1)]\nasync fn post_attachment_v2_data(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    data: Form<UploadData<'_>>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let attachment = match Attachment::find_by_id(&attachment_id, &conn).await {\n        Some(attachment) if cipher_id == attachment.cipher_uuid => Some(attachment),\n        Some(_) => err!(\"Attachment doesn't belong to cipher\"),\n        None => err!(\"Attachment doesn't exist\"),\n    };\n\n    save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;\n\n    Ok(())\n}\n\n/// Legacy API for creating an attachment associated with a cipher.\n#[post(\"/ciphers/<cipher_id>/attachment\", format = \"multipart/form-data\", data = \"<data>\")]\nasync fn post_attachment(\n    cipher_id: CipherId,\n    data: Form<UploadData<'_>>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    // Setting this as None signifies to save_attachment() that it should create\n    // the attachment database record as well as saving the data to disk.\n    let attachment = None;\n\n    let (cipher, conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?;\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &conn).await?))\n}\n\n#[post(\"/ciphers/<cipher_id>/attachment-admin\", format = \"multipart/form-data\", data = \"<data>\")]\nasync fn post_attachment_admin(\n    cipher_id: CipherId,\n    data: Form<UploadData<'_>>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    post_attachment(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/attachment/<attachment_id>/share\", format = \"multipart/form-data\", data = \"<data>\")]\nasync fn post_attachment_share(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    data: Form<UploadData<'_>>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await?;\n    post_attachment(cipher_id, data, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/attachment/<attachment_id>/delete-admin\")]\nasync fn delete_attachment_post_admin(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    delete_attachment(cipher_id, attachment_id, headers, conn, nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/attachment/<attachment_id>/delete\")]\nasync fn delete_attachment_post(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    delete_attachment(cipher_id, attachment_id, headers, conn, nt).await\n}\n\n#[delete(\"/ciphers/<cipher_id>/attachment/<attachment_id>\")]\nasync fn delete_attachment(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await\n}\n\n#[delete(\"/ciphers/<cipher_id>/attachment/<attachment_id>/admin\")]\nasync fn delete_attachment_admin(\n    cipher_id: CipherId,\n    attachment_id: AttachmentId,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    _delete_cipher_attachment_by_id(&cipher_id, &attachment_id, &headers, &conn, &nt).await\n}\n\n#[post(\"/ciphers/<cipher_id>/delete\")]\nasync fn delete_cipher_post(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await\n    // permanent delete\n}\n\n#[post(\"/ciphers/<cipher_id>/delete-admin\")]\nasync fn delete_cipher_post_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await\n    // permanent delete\n}\n\n#[put(\"/ciphers/<cipher_id>/delete\")]\nasync fn delete_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await\n    // soft delete\n}\n\n#[put(\"/ciphers/<cipher_id>/delete-admin\")]\nasync fn delete_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::SoftSingle, &nt).await\n    // soft delete\n}\n\n#[delete(\"/ciphers/<cipher_id>\")]\nasync fn delete_cipher(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await\n    // permanent delete\n}\n\n#[delete(\"/ciphers/<cipher_id>/admin\")]\nasync fn delete_cipher_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &CipherDeleteOptions::HardSingle, &nt).await\n    // permanent delete\n}\n\n#[delete(\"/ciphers\", data = \"<data>\")]\nasync fn delete_cipher_selected(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await\n    // permanent delete\n}\n\n#[post(\"/ciphers/delete\", data = \"<data>\")]\nasync fn delete_cipher_selected_post(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await\n    // permanent delete\n}\n\n#[put(\"/ciphers/delete\", data = \"<data>\")]\nasync fn delete_cipher_selected_put(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await\n    // soft delete\n}\n\n#[delete(\"/ciphers/admin\", data = \"<data>\")]\nasync fn delete_cipher_selected_admin(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await\n    // permanent delete\n}\n\n#[post(\"/ciphers/delete-admin\", data = \"<data>\")]\nasync fn delete_cipher_selected_post_admin(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::HardMulti, nt).await\n    // permanent delete\n}\n\n#[put(\"/ciphers/delete-admin\", data = \"<data>\")]\nasync fn delete_cipher_selected_put_admin(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_multiple_ciphers(data, headers, conn, CipherDeleteOptions::SoftMulti, nt).await\n    // soft delete\n}\n\n#[put(\"/ciphers/<cipher_id>/restore\")]\nasync fn restore_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    _restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await\n}\n\n#[put(\"/ciphers/<cipher_id>/restore-admin\")]\nasync fn restore_cipher_put_admin(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    _restore_cipher_by_uuid(&cipher_id, &headers, false, &conn, &nt).await\n}\n\n#[put(\"/ciphers/restore-admin\", data = \"<data>\")]\nasync fn restore_cipher_selected_admin(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    _restore_multiple_ciphers(data, &headers, &conn, &nt).await\n}\n\n#[put(\"/ciphers/restore\", data = \"<data>\")]\nasync fn restore_cipher_selected(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    _restore_multiple_ciphers(data, &headers, &conn, &nt).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct MoveCipherData {\n    folder_id: Option<FolderId>,\n    ids: Vec<CipherId>,\n}\n\n#[post(\"/ciphers/move\", data = \"<data>\")]\nasync fn move_cipher_selected(\n    data: Json<MoveCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let data = data.into_inner();\n    let user_id = &headers.user.uuid;\n\n    if let Some(ref folder_id) = data.folder_id {\n        if Folder::find_by_uuid_and_user(folder_id, user_id, &conn).await.is_none() {\n            err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\");\n        }\n    }\n\n    let cipher_count = data.ids.len();\n    let mut single_cipher: Option<Cipher> = None;\n\n    // TODO: Convert this to use a single query (or at least less) to update all items\n    // Find all ciphers a user has access to, all others will be ignored\n    let accessible_ciphers = Cipher::find_by_user_and_ciphers(user_id, &data.ids, &conn).await;\n    let accessible_ciphers_count = accessible_ciphers.len();\n    for cipher in accessible_ciphers {\n        cipher.move_to_folder(data.folder_id.clone(), user_id, &conn).await?;\n        if cipher_count == 1 {\n            single_cipher = Some(cipher);\n        }\n    }\n\n    if let Some(cipher) = single_cipher {\n        nt.send_cipher_update(\n            UpdateType::SyncCipherUpdate,\n            &cipher,\n            std::slice::from_ref(user_id),\n            &headers.device,\n            None,\n            &conn,\n        )\n        .await;\n    } else {\n        // Multi move actions do not send out a push for each cipher, we need to send a general sync here\n        nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;\n    }\n\n    if cipher_count != accessible_ciphers_count {\n        err!(format!(\n            \"Not all ciphers are moved! {accessible_ciphers_count} of the selected {cipher_count} were moved.\"\n        ))\n    }\n\n    Ok(())\n}\n\n#[put(\"/ciphers/move\", data = \"<data>\")]\nasync fn move_cipher_selected_put(\n    data: Json<MoveCipherData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    move_cipher_selected(data, headers, conn, nt).await\n}\n\n#[derive(FromForm)]\nstruct OrganizationIdData {\n    #[field(name = \"organizationId\")]\n    org_id: OrganizationId,\n}\n\n#[post(\"/ciphers/purge?<organization..>\", data = \"<data>\")]\nasync fn delete_all(\n    organization: Option<OrganizationIdData>,\n    data: Json<PasswordOrOtpData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let mut user = headers.user;\n\n    data.validate(&user, true, &conn).await?;\n\n    match organization {\n        Some(org_data) => {\n            // Organization ID in query params, purging organization vault\n            match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await {\n                None => err!(\"You don't have permission to purge the organization vault\"),\n                Some(member) => {\n                    if member.atype == MembershipType::Owner {\n                        Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?;\n                        nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;\n\n                        log_event(\n                            EventType::OrganizationPurgedVault as i32,\n                            &org_data.org_id,\n                            &org_data.org_id,\n                            &user.uuid,\n                            headers.device.atype,\n                            &headers.ip.ip,\n                            &conn,\n                        )\n                        .await;\n\n                        Ok(())\n                    } else {\n                        err!(\"You don't have permission to purge the organization vault\");\n                    }\n                }\n            }\n        }\n        None => {\n            // No organization ID in query params, purging user vault\n            // Delete ciphers and their attachments\n            for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await {\n                cipher.delete(&conn).await?;\n            }\n\n            // Delete folders\n            for f in Folder::find_by_user(&user.uuid, &conn).await {\n                f.delete(&conn).await?;\n            }\n\n            user.update_revision(&conn).await?;\n            nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;\n\n            Ok(())\n        }\n    }\n}\n\n#[derive(PartialEq)]\npub enum CipherDeleteOptions {\n    SoftSingle,\n    SoftMulti,\n    HardSingle,\n    HardMulti,\n}\n\nasync fn _delete_cipher_by_uuid(\n    cipher_id: &CipherId,\n    headers: &Headers,\n    conn: &DbConn,\n    delete_options: &CipherDeleteOptions,\n    nt: &Notify<'_>,\n) -> EmptyResult {\n    let Some(mut cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {\n        err!(\"Cipher can't be deleted by user\")\n    }\n\n    if *delete_options == CipherDeleteOptions::SoftSingle || *delete_options == CipherDeleteOptions::SoftMulti {\n        cipher.deleted_at = Some(Utc::now().naive_utc());\n        cipher.save(conn).await?;\n        if *delete_options == CipherDeleteOptions::SoftSingle {\n            nt.send_cipher_update(\n                UpdateType::SyncCipherUpdate,\n                &cipher,\n                &cipher.update_users_revision(conn).await,\n                &headers.device,\n                None,\n                conn,\n            )\n            .await;\n        }\n    } else {\n        cipher.delete(conn).await?;\n        if *delete_options == CipherDeleteOptions::HardSingle {\n            nt.send_cipher_update(\n                UpdateType::SyncLoginDelete,\n                &cipher,\n                &cipher.update_users_revision(conn).await,\n                &headers.device,\n                None,\n                conn,\n            )\n            .await;\n        }\n    }\n\n    if let Some(org_id) = cipher.organization_uuid {\n        let event_type = if *delete_options == CipherDeleteOptions::SoftSingle\n            || *delete_options == CipherDeleteOptions::SoftMulti\n        {\n            EventType::CipherSoftDeleted as i32\n        } else {\n            EventType::CipherDeleted as i32\n        };\n\n        log_event(event_type, &cipher.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, conn)\n            .await;\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct CipherIdsData {\n    ids: Vec<CipherId>,\n}\n\nasync fn _delete_multiple_ciphers(\n    data: Json<CipherIdsData>,\n    headers: Headers,\n    conn: DbConn,\n    delete_options: CipherDeleteOptions,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let data = data.into_inner();\n\n    for cipher_id in data.ids {\n        if let error @ Err(_) = _delete_cipher_by_uuid(&cipher_id, &headers, &conn, &delete_options, &nt).await {\n            return error;\n        };\n    }\n\n    // Multi delete actions do not send out a push for each cipher, we need to send a general sync here\n    nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, &conn).await;\n\n    Ok(())\n}\n\nasync fn _restore_cipher_by_uuid(\n    cipher_id: &CipherId,\n    headers: &Headers,\n    multi_restore: bool,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n) -> JsonResult {\n    let Some(mut cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {\n        err!(\"Cipher can't be restored by user\")\n    }\n\n    cipher.deleted_at = None;\n    cipher.save(conn).await?;\n\n    if !multi_restore {\n        nt.send_cipher_update(\n            UpdateType::SyncCipherUpdate,\n            &cipher,\n            &cipher.update_users_revision(conn).await,\n            &headers.device,\n            None,\n            conn,\n        )\n        .await;\n    }\n\n    if let Some(org_id) = &cipher.organization_uuid {\n        log_event(\n            EventType::CipherRestored as i32,\n            &cipher.uuid.clone(),\n            org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            conn,\n        )\n        .await;\n    }\n\n    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))\n}\n\nasync fn _restore_multiple_ciphers(\n    data: Json<CipherIdsData>,\n    headers: &Headers,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n) -> JsonResult {\n    let data = data.into_inner();\n\n    let mut ciphers: Vec<Value> = Vec::new();\n    for cipher_id in data.ids {\n        match _restore_cipher_by_uuid(&cipher_id, headers, true, conn, nt).await {\n            Ok(json) => ciphers.push(json.into_inner()),\n            err => return err,\n        }\n    }\n\n    // Multi move actions do not send out a push for each cipher, we need to send a general sync here\n    nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;\n\n    Ok(Json(json!({\n      \"data\": ciphers,\n      \"object\": \"list\",\n      \"continuationToken\": null\n    })))\n}\n\nasync fn _delete_cipher_attachment_by_id(\n    cipher_id: &CipherId,\n    attachment_id: &AttachmentId,\n    headers: &Headers,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n) -> JsonResult {\n    let Some(attachment) = Attachment::find_by_id(attachment_id, conn).await else {\n        err!(\"Attachment doesn't exist\")\n    };\n\n    if &attachment.cipher_uuid != cipher_id {\n        err!(\"Attachment from other cipher\")\n    }\n\n    let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {\n        err!(\"Cipher doesn't exist\")\n    };\n\n    if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await {\n        err!(\"Cipher cannot be deleted by user\")\n    }\n\n    // Delete attachment\n    attachment.delete(conn).await?;\n    nt.send_cipher_update(\n        UpdateType::SyncCipherUpdate,\n        &cipher,\n        &cipher.update_users_revision(conn).await,\n        &headers.device,\n        None,\n        conn,\n    )\n    .await;\n\n    if let Some(ref org_id) = cipher.organization_uuid {\n        log_event(\n            EventType::CipherAttachmentDeleted as i32,\n            &cipher.uuid,\n            org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            conn,\n        )\n        .await;\n    }\n    let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?;\n    Ok(Json(json!({\"cipher\":cipher_json})))\n}\n\n/// This will hold all the necessary data to improve a full sync of all the ciphers\n/// It can be used during the `Cipher::to_json()` call.\n/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.\n/// This will not improve the speed of a single cipher.to_json() call that much, so better not to use it for those calls.\npub struct CipherSyncData {\n    pub cipher_attachments: HashMap<CipherId, Vec<Attachment>>,\n    pub cipher_folders: HashMap<CipherId, FolderId>,\n    pub cipher_favorites: HashSet<CipherId>,\n    pub cipher_collections: HashMap<CipherId, Vec<CollectionId>>,\n    pub members: HashMap<OrganizationId, Membership>,\n    pub user_collections: HashMap<CollectionId, CollectionUser>,\n    pub user_collections_groups: HashMap<CollectionId, CollectionGroup>,\n    pub user_group_full_access_for_organizations: HashSet<OrganizationId>,\n}\n\n#[derive(Eq, PartialEq)]\npub enum CipherSyncType {\n    User,\n    Organization,\n}\n\nimpl CipherSyncData {\n    pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {\n        let cipher_folders: HashMap<CipherId, FolderId>;\n        let cipher_favorites: HashSet<CipherId>;\n        match sync_type {\n            // User Sync supports Folders and Favorites\n            CipherSyncType::User => {\n                // Generate a HashMap with the Cipher UUID as key and the Folder UUID as value\n                cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect();\n\n                // Generate a HashSet of all the Cipher UUID's which are marked as favorite\n                cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect();\n            }\n            // Organization Sync does not support Folders and Favorites.\n            // If these are set, it will cause issues in the web-vault.\n            CipherSyncType::Organization => {\n                cipher_folders = HashMap::with_capacity(0);\n                cipher_favorites = HashSet::with_capacity(0);\n            }\n        }\n\n        // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records\n        let orgs = Membership::get_orgs_by_user(user_id, conn).await;\n        let attachments = Attachment::find_all_by_user_and_orgs(user_id, &orgs, conn).await;\n        let mut cipher_attachments: HashMap<CipherId, Vec<Attachment>> = HashMap::with_capacity(attachments.len());\n        for attachment in attachments {\n            cipher_attachments.entry(attachment.cipher_uuid.clone()).or_default().push(attachment);\n        }\n\n        // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's\n        let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_id.clone(), conn).await;\n        let mut cipher_collections: HashMap<CipherId, Vec<CollectionId>> =\n            HashMap::with_capacity(user_cipher_collections.len());\n        for (cipher, collection) in user_cipher_collections {\n            cipher_collections.entry(cipher).or_default().push(collection);\n        }\n\n        // Generate a HashMap with the Organization UUID as key and the Membership record\n        let members: HashMap<OrganizationId, Membership> =\n            Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect();\n\n        // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record\n        let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)\n            .await\n            .into_iter()\n            .map(|uc| (uc.collection_uuid.clone(), uc))\n            .collect();\n\n        // Generate a HashMap with the collections_uuid as key and the CollectionGroup record\n        let user_collections_groups: HashMap<CollectionId, CollectionGroup> = if CONFIG.org_groups_enabled() {\n            CollectionGroup::find_by_user(user_id, conn).await.into_iter().fold(\n                HashMap::new(),\n                |mut combined_permissions, cg| {\n                    combined_permissions\n                        .entry(cg.collections_uuid.clone())\n                        .and_modify(|existing| {\n                            // Combine permissions: take the most permissive settings.\n                            existing.read_only &= cg.read_only; // false if ANY group allows write\n                            existing.hide_passwords &= cg.hide_passwords; // false if ANY group allows password view\n                            existing.manage |= cg.manage; // true if ANY group allows manage\n                        })\n                        .or_insert(cg);\n                    combined_permissions\n                },\n            )\n        } else {\n            HashMap::new()\n        };\n\n        // Get all organizations that the given user has full access to via group assignment\n        let user_group_full_access_for_organizations: HashSet<OrganizationId> = if CONFIG.org_groups_enabled() {\n            Group::get_orgs_by_user_with_full_access(user_id, conn).await.into_iter().collect()\n        } else {\n            HashSet::new()\n        };\n\n        Self {\n            cipher_attachments,\n            cipher_folders,\n            cipher_favorites,\n            cipher_collections,\n            members,\n            user_collections,\n            user_collections_groups,\n            user_group_full_access_for_organizations,\n        }\n    }\n}\n"
  },
  {
    "path": "src/api/core/emergency_access.rs",
    "content": "use chrono::{TimeDelta, Utc};\nuse rocket::{serde::json::Json, Route};\nuse serde_json::Value;\n\nuse crate::{\n    api::{\n        core::{CipherSyncData, CipherSyncType},\n        EmptyResult, JsonResult,\n    },\n    auth::{decode_emergency_access_invite, Headers},\n    db::{\n        models::{\n            Cipher, EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType, Invitation,\n            Membership, MembershipType, OrgPolicy, TwoFactor, User, UserId,\n        },\n        DbConn, DbPool,\n    },\n    mail,\n    util::NumberOrString,\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![\n        get_contacts,\n        get_grantees,\n        get_emergency_access,\n        put_emergency_access,\n        post_emergency_access,\n        delete_emergency_access,\n        post_delete_emergency_access,\n        send_invite,\n        resend_invite,\n        accept_invite,\n        confirm_emergency_access,\n        initiate_emergency_access,\n        approve_emergency_access,\n        reject_emergency_access,\n        takeover_emergency_access,\n        password_emergency_access,\n        view_emergency_access,\n        policies_emergency_access,\n    ]\n}\n\n// region get\n\n#[get(\"/emergency-access/trusted\")]\nasync fn get_contacts(headers: Headers, conn: DbConn) -> Json<Value> {\n    let emergency_access_list = if CONFIG.emergency_access_allowed() {\n        EmergencyAccess::find_all_by_grantor_uuid(&headers.user.uuid, &conn).await\n    } else {\n        Vec::new()\n    };\n    let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());\n    for ea in emergency_access_list {\n        if let Some(grantee) = ea.to_json_grantee_details(&conn).await {\n            emergency_access_list_json.push(grantee)\n        }\n    }\n\n    Json(json!({\n      \"data\": emergency_access_list_json,\n      \"object\": \"list\",\n      \"continuationToken\": null\n    }))\n}\n\n#[get(\"/emergency-access/granted\")]\nasync fn get_grantees(headers: Headers, conn: DbConn) -> Json<Value> {\n    let emergency_access_list = if CONFIG.emergency_access_allowed() {\n        EmergencyAccess::find_all_by_grantee_uuid(&headers.user.uuid, &conn).await\n    } else {\n        Vec::new()\n    };\n    let mut emergency_access_list_json = Vec::with_capacity(emergency_access_list.len());\n    for ea in emergency_access_list {\n        emergency_access_list_json.push(ea.to_json_grantor_details(&conn).await);\n    }\n\n    Json(json!({\n      \"data\": emergency_access_list_json,\n      \"object\": \"list\",\n      \"continuationToken\": null\n    }))\n}\n\n#[get(\"/emergency-access/<emer_id>\")]\nasync fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await {\n        Some(emergency_access) => Ok(Json(\n            emergency_access.to_json_grantee_details(&conn).await.expect(\"Grantee user should exist but does not!\"),\n        )),\n        None => err!(\"Emergency access not valid.\"),\n    }\n}\n\n// endregion\n\n// region put/post\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EmergencyAccessUpdateData {\n    r#type: NumberOrString,\n    wait_time_days: i32,\n    key_encrypted: Option<String>,\n}\n\n#[put(\"/emergency-access/<emer_id>\", data = \"<data>\")]\nasync fn put_emergency_access(\n    emer_id: EmergencyAccessId,\n    data: Json<EmergencyAccessUpdateData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    post_emergency_access(emer_id, data, headers, conn).await\n}\n\n#[post(\"/emergency-access/<emer_id>\", data = \"<data>\")]\nasync fn post_emergency_access(\n    emer_id: EmergencyAccessId,\n    data: Json<EmergencyAccessUpdateData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let data: EmergencyAccessUpdateData = data.into_inner();\n\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) {\n        Some(new_type) => new_type as i32,\n        None => err!(\"Invalid emergency access type.\"),\n    };\n\n    emergency_access.atype = new_type;\n    emergency_access.wait_time_days = data.wait_time_days;\n    if data.key_encrypted.is_some() {\n        emergency_access.key_encrypted = data.key_encrypted;\n    }\n\n    emergency_access.save(&conn).await?;\n    Ok(Json(emergency_access.to_json()))\n}\n\n// endregion\n\n// region delete\n\n#[delete(\"/emergency-access/<emer_id>\")]\nasync fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {\n    check_emergency_access_enabled()?;\n\n    let emergency_access = match (\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await,\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await,\n    ) {\n        (Some(grantor_emer), None) => {\n            info!(\"Grantor deleted emergency access {emer_id}\");\n            grantor_emer\n        }\n        (None, Some(grantee_emer)) => {\n            info!(\"Grantee deleted emergency access {emer_id}\");\n            grantee_emer\n        }\n        _ => err!(\"Emergency access not valid.\"),\n    };\n\n    emergency_access.delete(&conn).await?;\n    Ok(())\n}\n\n#[post(\"/emergency-access/<emer_id>/delete\")]\nasync fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {\n    delete_emergency_access(emer_id, headers, conn).await\n}\n\n// endregion\n\n// region invite\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EmergencyAccessInviteData {\n    email: String,\n    r#type: NumberOrString,\n    wait_time_days: i32,\n}\n\n#[post(\"/emergency-access/invite\", data = \"<data>\")]\nasync fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    check_emergency_access_enabled()?;\n\n    let data: EmergencyAccessInviteData = data.into_inner();\n    let email = data.email.to_lowercase();\n    let wait_time_days = data.wait_time_days;\n\n    let emergency_access_status = EmergencyAccessStatus::Invited as i32;\n\n    let new_type = match EmergencyAccessType::from_str(&data.r#type.into_string()) {\n        Some(new_type) => new_type as i32,\n        None => err!(\"Invalid emergency access type.\"),\n    };\n\n    let grantor_user = headers.user;\n\n    // avoid setting yourself as emergency contact\n    if email == grantor_user.email {\n        err!(\"You can not set yourself as an emergency contact.\")\n    }\n\n    let (grantee_user, new_user) = match User::find_by_mail(&email, &conn).await {\n        None => {\n            if !CONFIG.invitations_allowed() {\n                err!(format!(\"Grantee user does not exist: {email}\"))\n            }\n\n            if !CONFIG.is_email_domain_allowed(&email) {\n                err!(\"Email domain not eligible for invitations\")\n            }\n\n            if !CONFIG.mail_enabled() {\n                let invitation = Invitation::new(&email);\n                invitation.save(&conn).await?;\n            }\n\n            let mut user = User::new(&email, None);\n            user.save(&conn).await?;\n            (user, true)\n        }\n        Some(user) if user.password_hash.is_empty() => (user, true),\n        Some(user) => (user, false),\n    };\n\n    if EmergencyAccess::find_by_grantor_uuid_and_grantee_uuid_or_email(\n        &grantor_user.uuid,\n        &grantee_user.uuid,\n        &grantee_user.email,\n        &conn,\n    )\n    .await\n    .is_some()\n    {\n        err!(format!(\"Grantee user already invited: {}\", &grantee_user.email))\n    }\n\n    let mut new_emergency_access =\n        EmergencyAccess::new(grantor_user.uuid, grantee_user.email, emergency_access_status, new_type, wait_time_days);\n    new_emergency_access.save(&conn).await?;\n\n    if CONFIG.mail_enabled() {\n        mail::send_emergency_access_invite(\n            &new_emergency_access.email.expect(\"Grantee email does not exists\"),\n            grantee_user.uuid,\n            new_emergency_access.uuid,\n            &grantor_user.name,\n            &grantor_user.email,\n        )\n        .await?;\n    } else if !new_user {\n        // if mail is not enabled immediately accept the invitation for existing users\n        new_emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;\n    }\n\n    Ok(())\n}\n\n#[post(\"/emergency-access/<emer_id>/reinvite\")]\nasync fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {\n    check_emergency_access_enabled()?;\n\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if emergency_access.status != EmergencyAccessStatus::Invited as i32 {\n        err!(\"The grantee user is already accepted or confirmed to the organization\");\n    }\n\n    let Some(email) = emergency_access.email.clone() else {\n        err!(\"Email not valid.\")\n    };\n\n    let Some(grantee_user) = User::find_by_mail(&email, &conn).await else {\n        err!(\"Grantee user not found.\")\n    };\n\n    let grantor_user = headers.user;\n\n    if CONFIG.mail_enabled() {\n        mail::send_emergency_access_invite(\n            &email,\n            grantor_user.uuid,\n            emergency_access.uuid,\n            &grantor_user.name,\n            &grantor_user.email,\n        )\n        .await?;\n    } else if !grantee_user.password_hash.is_empty() {\n        // accept the invitation for existing user\n        emergency_access.accept_invite(&grantee_user.uuid, &email, &conn).await?;\n    } else if CONFIG.invitations_allowed() && Invitation::find_by_mail(&email, &conn).await.is_none() {\n        let invitation = Invitation::new(&email);\n        invitation.save(&conn).await?;\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AcceptData {\n    token: String,\n}\n\n#[post(\"/emergency-access/<emer_id>/accept\", data = \"<data>\")]\nasync fn accept_invite(\n    emer_id: EmergencyAccessId,\n    data: Json<AcceptData>,\n    headers: Headers,\n    conn: DbConn,\n) -> EmptyResult {\n    check_emergency_access_enabled()?;\n\n    let data: AcceptData = data.into_inner();\n    let token = &data.token;\n    let claims = decode_emergency_access_invite(token)?;\n\n    // This can happen if the user who received the invite used a different email to signup.\n    // Since we do not know if this is intended, we error out here and do nothing with the invite.\n    if claims.email != headers.user.email {\n        err!(\"Claim email does not match current users email\")\n    }\n\n    let grantee_user = match User::find_by_mail(&claims.email, &conn).await {\n        Some(user) => {\n            Invitation::take(&claims.email, &conn).await;\n            user\n        }\n        None => err!(\"Invited user not found\"),\n    };\n\n    // We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.\n    // The uuid of the grantee gets stored once accepted.\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    // get grantor user to send Accepted email\n    let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    if emer_id == claims.emer_id\n        && grantor_user.name == claims.grantor_name\n        && grantor_user.email == claims.grantor_email\n    {\n        emergency_access.accept_invite(&grantee_user.uuid, &grantee_user.email, &conn).await?;\n\n        if CONFIG.mail_enabled() {\n            mail::send_emergency_access_invite_accepted(&grantor_user.email, &grantee_user.email).await?;\n        }\n\n        Ok(())\n    } else {\n        err!(\"Emergency access invitation error.\")\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ConfirmData {\n    key: String,\n}\n\n#[post(\"/emergency-access/<emer_id>/confirm\", data = \"<data>\")]\nasync fn confirm_emergency_access(\n    emer_id: EmergencyAccessId,\n    data: Json<ConfirmData>,\n    headers: Headers,\n    conn: DbConn,\n) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let confirming_user = headers.user;\n    let data: ConfirmData = data.into_inner();\n    let key = data.key;\n\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if emergency_access.status != EmergencyAccessStatus::Accepted as i32\n        || emergency_access.grantor_uuid != confirming_user.uuid\n    {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(grantor_user) = User::find_by_uuid(&confirming_user.uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {\n        let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {\n            err!(\"Grantee user not found.\")\n        };\n\n        emergency_access.status = EmergencyAccessStatus::Confirmed as i32;\n        emergency_access.key_encrypted = Some(key);\n        emergency_access.email = None;\n\n        emergency_access.save(&conn).await?;\n\n        if CONFIG.mail_enabled() {\n            mail::send_emergency_access_invite_confirmed(&grantee_user.email, &grantor_user.name).await?;\n        }\n        Ok(Json(emergency_access.to_json()))\n    } else {\n        err!(\"Grantee user not found.\")\n    }\n}\n\n// endregion\n\n// region access emergency access\n\n#[post(\"/emergency-access/<emer_id>/initiate\")]\nasync fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let initiating_user = headers.user;\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if emergency_access.status != EmergencyAccessStatus::Confirmed as i32 {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    let now = Utc::now().naive_utc();\n    emergency_access.status = EmergencyAccessStatus::RecoveryInitiated as i32;\n    emergency_access.updated_at = now;\n    emergency_access.recovery_initiated_at = Some(now);\n    emergency_access.last_notification_at = Some(now);\n    emergency_access.save(&conn).await?;\n\n    if CONFIG.mail_enabled() {\n        mail::send_emergency_access_recovery_initiated(\n            &grantor_user.email,\n            &initiating_user.name,\n            emergency_access.get_type_as_str(),\n            &emergency_access.wait_time_days,\n        )\n        .await?;\n    }\n    Ok(Json(emergency_access.to_json()))\n}\n\n#[post(\"/emergency-access/<emer_id>/approve\")]\nasync fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32 {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(grantor_user) = User::find_by_uuid(&headers.user.uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {\n        let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {\n            err!(\"Grantee user not found.\")\n        };\n\n        emergency_access.status = EmergencyAccessStatus::RecoveryApproved as i32;\n        emergency_access.save(&conn).await?;\n\n        if CONFIG.mail_enabled() {\n            mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name).await?;\n        }\n        Ok(Json(emergency_access.to_json()))\n    } else {\n        err!(\"Grantee user not found.\")\n    }\n}\n\n#[post(\"/emergency-access/<emer_id>/reject\")]\nasync fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let Some(mut emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if emergency_access.status != EmergencyAccessStatus::RecoveryInitiated as i32\n        && emergency_access.status != EmergencyAccessStatus::RecoveryApproved as i32\n    {\n        err!(\"Emergency access not valid.\")\n    }\n\n    if let Some(grantee_uuid) = emergency_access.grantee_uuid.as_ref() {\n        let Some(grantee_user) = User::find_by_uuid(grantee_uuid, &conn).await else {\n            err!(\"Grantee user not found.\")\n        };\n\n        emergency_access.status = EmergencyAccessStatus::Confirmed as i32;\n        emergency_access.save(&conn).await?;\n\n        if CONFIG.mail_enabled() {\n            mail::send_emergency_access_recovery_rejected(&grantee_user.email, &headers.user.name).await?;\n        }\n        Ok(Json(emergency_access.to_json()))\n    } else {\n        err!(\"Grantee user not found.\")\n    }\n}\n\n// endregion\n\n// region action\n\n#[post(\"/emergency-access/<emer_id>/view\")]\nasync fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let Some(emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if !is_valid_request(&emergency_access, &headers.user.uuid, EmergencyAccessType::View) {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let ciphers = Cipher::find_owned_by_user(&emergency_access.grantor_uuid, &conn).await;\n    let cipher_sync_data = CipherSyncData::new(&emergency_access.grantor_uuid, CipherSyncType::User, &conn).await;\n\n    let mut ciphers_json = Vec::with_capacity(ciphers.len());\n    for c in ciphers {\n        ciphers_json.push(\n            c.to_json(\n                &headers.host,\n                &emergency_access.grantor_uuid,\n                Some(&cipher_sync_data),\n                CipherSyncType::User,\n                &conn,\n            )\n            .await?,\n        );\n    }\n\n    Ok(Json(json!({\n      \"ciphers\": ciphers_json,\n      \"keyEncrypted\": &emergency_access.key_encrypted,\n      \"object\": \"emergencyAccessView\",\n    })))\n}\n\n#[post(\"/emergency-access/<emer_id>/takeover\")]\nasync fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    check_emergency_access_enabled()?;\n\n    let requesting_user = headers.user;\n    let Some(emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    let result = json!({\n        \"kdf\": grantor_user.client_kdf_type,\n        \"kdfIterations\": grantor_user.client_kdf_iter,\n        \"kdfMemory\": grantor_user.client_kdf_memory,\n        \"kdfParallelism\": grantor_user.client_kdf_parallelism,\n        \"keyEncrypted\": &emergency_access.key_encrypted,\n        \"object\": \"emergencyAccessTakeover\",\n    });\n\n    Ok(Json(result))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EmergencyAccessPasswordData {\n    new_master_password_hash: String,\n    key: String,\n}\n\n#[post(\"/emergency-access/<emer_id>/password\", data = \"<data>\")]\nasync fn password_emergency_access(\n    emer_id: EmergencyAccessId,\n    data: Json<EmergencyAccessPasswordData>,\n    headers: Headers,\n    conn: DbConn,\n) -> EmptyResult {\n    check_emergency_access_enabled()?;\n\n    let data: EmergencyAccessPasswordData = data.into_inner();\n    let new_master_password_hash = &data.new_master_password_hash;\n    //let key = &data.Key;\n\n    let requesting_user = headers.user;\n    let Some(emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(mut grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    // change grantor_user password\n    grantor_user.set_password(new_master_password_hash, Some(data.key), true, None);\n    grantor_user.save(&conn).await?;\n\n    // Disable TwoFactor providers since they will otherwise block logins\n    TwoFactor::delete_all_by_user(&grantor_user.uuid, &conn).await?;\n\n    // Remove grantor from all organisations unless Owner\n    for member in Membership::find_any_state_by_user(&grantor_user.uuid, &conn).await {\n        if member.atype != MembershipType::Owner as i32 {\n            member.delete(&conn).await?;\n        }\n    }\n    Ok(())\n}\n\n// endregion\n\n#[get(\"/emergency-access/<emer_id>/policies\")]\nasync fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> JsonResult {\n    let requesting_user = headers.user;\n    let Some(emergency_access) =\n        EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &conn).await\n    else {\n        err!(\"Emergency access not valid.\")\n    };\n\n    if !is_valid_request(&emergency_access, &requesting_user.uuid, EmergencyAccessType::Takeover) {\n        err!(\"Emergency access not valid.\")\n    }\n\n    let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &conn).await else {\n        err!(\"Grantor user not found.\")\n    };\n\n    let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn);\n    let policies_json: Vec<Value> = policies.await.iter().map(OrgPolicy::to_json).collect();\n\n    Ok(Json(json!({\n        \"data\": policies_json,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\nfn is_valid_request(\n    emergency_access: &EmergencyAccess,\n    requesting_user_id: &UserId,\n    requested_access_type: EmergencyAccessType,\n) -> bool {\n    emergency_access.grantee_uuid.is_some()\n        && emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_id\n        && emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32\n        && emergency_access.atype == requested_access_type as i32\n}\n\nfn check_emergency_access_enabled() -> EmptyResult {\n    if !CONFIG.emergency_access_allowed() {\n        err!(\"Emergency access is not enabled.\")\n    }\n    Ok(())\n}\n\npub async fn emergency_request_timeout_job(pool: DbPool) {\n    debug!(\"Start emergency_request_timeout_job\");\n    if !CONFIG.emergency_access_allowed() {\n        return;\n    }\n\n    if let Ok(conn) = pool.get().await {\n        let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;\n\n        if emergency_access_list.is_empty() {\n            debug!(\"No emergency request timeout to approve\");\n        }\n\n        let now = Utc::now().naive_utc();\n        for mut emer in emergency_access_list {\n            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)\n            let recovery_allowed_at =\n                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();\n            if recovery_allowed_at.le(&now) {\n                // Only update the access status\n                // Updating the whole record could cause issues when the emergency_notification_reminder_job is also active\n                emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)\n                    .await\n                    .expect(\"Unable to update emergency access status\");\n\n                if CONFIG.mail_enabled() {\n                    // get grantor user to send Accepted email\n                    let grantor_user =\n                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect(\"Grantor user not found\");\n\n                    // get grantee user to send Accepted email\n                    let grantee_user =\n                        User::find_by_uuid(&emer.grantee_uuid.clone().expect(\"Grantee user invalid\"), &conn)\n                            .await\n                            .expect(\"Grantee user not found\");\n\n                    mail::send_emergency_access_recovery_timed_out(\n                        &grantor_user.email,\n                        &grantee_user.name,\n                        emer.get_type_as_str(),\n                    )\n                    .await\n                    .expect(\"Error on sending email\");\n\n                    mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)\n                        .await\n                        .expect(\"Error on sending email\");\n                }\n            }\n        }\n    } else {\n        error!(\"Failed to get DB connection while searching emergency request timed out\")\n    }\n}\n\npub async fn emergency_notification_reminder_job(pool: DbPool) {\n    debug!(\"Start emergency_notification_reminder_job\");\n    if !CONFIG.emergency_access_allowed() {\n        return;\n    }\n\n    if let Ok(conn) = pool.get().await {\n        let emergency_access_list = EmergencyAccess::find_all_recoveries_initiated(&conn).await;\n\n        if emergency_access_list.is_empty() {\n            debug!(\"No emergency request reminder notification to send\");\n        }\n\n        let now = Utc::now().naive_utc();\n        for mut emer in emergency_access_list {\n            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)\n            // Calculate the day before the recovery will become active\n            let final_recovery_reminder_at =\n                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();\n            // Calculate if a day has passed since the previous notification, else no notification has been sent before\n            let next_recovery_reminder_at = if let Some(last_notification_at) = emer.last_notification_at {\n                last_notification_at + TimeDelta::try_days(1).unwrap()\n            } else {\n                now\n            };\n            if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {\n                // Only update the last notification date\n                // Updating the whole record could cause issues when the emergency_request_timeout_job is also active\n                emer.update_last_notification_date_and_save(&now, &conn)\n                    .await\n                    .expect(\"Unable to update emergency access notification date\");\n\n                if CONFIG.mail_enabled() {\n                    // get grantor user to send Accepted email\n                    let grantor_user =\n                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect(\"Grantor user not found\");\n\n                    // get grantee user to send Accepted email\n                    let grantee_user =\n                        User::find_by_uuid(&emer.grantee_uuid.clone().expect(\"Grantee user invalid\"), &conn)\n                            .await\n                            .expect(\"Grantee user not found\");\n\n                    mail::send_emergency_access_recovery_reminder(\n                        &grantor_user.email,\n                        &grantee_user.name,\n                        emer.get_type_as_str(),\n                        \"1\", // This notification is only triggered one day before the activation\n                    )\n                    .await\n                    .expect(\"Error on sending email\");\n                }\n            }\n        }\n    } else {\n        error!(\"Failed to get DB connection while searching emergency notification reminder\")\n    }\n}\n"
  },
  {
    "path": "src/api/core/events.rs",
    "content": "use std::net::IpAddr;\n\nuse chrono::NaiveDateTime;\nuse rocket::{form::FromForm, serde::json::Json, Route};\nuse serde_json::Value;\n\nuse crate::{\n    api::{EmptyResult, JsonResult},\n    auth::{AdminHeaders, Headers},\n    db::{\n        models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},\n        DbConn, DbPool,\n    },\n    util::parse_date,\n    CONFIG,\n};\n\n/// ###############################################################################################################\n/// /api routes\npub fn routes() -> Vec<Route> {\n    routes![get_org_events, get_cipher_events, get_user_events,]\n}\n\n#[derive(FromForm)]\nstruct EventRange {\n    start: String,\n    end: String,\n    #[field(name = \"continuationToken\")]\n    continuation_token: Option<String>,\n}\n\n// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/EventsController.cs#L87\n#[get(\"/organizations/<org_id>/events?<data..>\")]\nasync fn get_org_events(org_id: OrganizationId, data: EventRange, headers: AdminHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    // Return an empty vec when we org events are disabled.\n    // This prevents client errors\n    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {\n        Vec::with_capacity(0)\n    } else {\n        let start_date = parse_date(&data.start);\n        let end_date = if let Some(before_date) = &data.continuation_token {\n            parse_date(before_date)\n        } else {\n            parse_date(&data.end)\n        };\n\n        Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &conn)\n            .await\n            .iter()\n            .map(|e| e.to_json())\n            .collect()\n    };\n\n    Ok(Json(json!({\n        \"data\": events_json,\n        \"object\": \"list\",\n        \"continuationToken\": get_continuation_token(&events_json),\n    })))\n}\n\n#[get(\"/ciphers/<cipher_id>/events?<data..>\")]\nasync fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, conn: DbConn) -> JsonResult {\n    // Return an empty vec when we org events are disabled.\n    // This prevents client errors\n    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {\n        Vec::with_capacity(0)\n    } else {\n        let mut events_json = Vec::with_capacity(0);\n        if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &conn).await {\n            let start_date = parse_date(&data.start);\n            let end_date = if let Some(before_date) = &data.continuation_token {\n                parse_date(before_date)\n            } else {\n                parse_date(&data.end)\n            };\n\n            events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &conn)\n                .await\n                .iter()\n                .map(|e| e.to_json())\n                .collect()\n        }\n        events_json\n    };\n\n    Ok(Json(json!({\n        \"data\": events_json,\n        \"object\": \"list\",\n        \"continuationToken\": get_continuation_token(&events_json),\n    })))\n}\n\n#[get(\"/organizations/<org_id>/users/<member_id>/events?<data..>\")]\nasync fn get_user_events(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: EventRange,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    // Return an empty vec when we org events are disabled.\n    // This prevents client errors\n    let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {\n        Vec::with_capacity(0)\n    } else {\n        let start_date = parse_date(&data.start);\n        let end_date = if let Some(before_date) = &data.continuation_token {\n            parse_date(before_date)\n        } else {\n            parse_date(&data.end)\n        };\n\n        Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &conn)\n            .await\n            .iter()\n            .map(|e| e.to_json())\n            .collect()\n    };\n\n    Ok(Json(json!({\n        \"data\": events_json,\n        \"object\": \"list\",\n        \"continuationToken\": get_continuation_token(&events_json),\n    })))\n}\n\nfn get_continuation_token(events_json: &[Value]) -> Option<&str> {\n    // When the length of the vec equals the max page_size there probably is more data\n    // When it is less, then all events are loaded.\n    if events_json.len() as i64 == Event::PAGE_SIZE {\n        if let Some(last_event) = events_json.last() {\n            last_event[\"date\"].as_str()\n        } else {\n            None\n        }\n    } else {\n        None\n    }\n}\n\n/// ###############################################################################################################\n/// /events routes\npub fn main_routes() -> Vec<Route> {\n    routes![post_events_collect,]\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EventCollection {\n    // Mandatory\n    r#type: i32,\n    date: String,\n\n    // Optional\n    cipher_id: Option<CipherId>,\n    organization_id: Option<OrganizationId>,\n}\n\n// Upstream:\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Events/Controllers/CollectController.cs\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs\n#[post(\"/collect\", format = \"application/json\", data = \"<data>\")]\nasync fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers, conn: DbConn) -> EmptyResult {\n    if !CONFIG.org_events_enabled() {\n        return Ok(());\n    }\n\n    for event in data.iter() {\n        let event_date = parse_date(&event.date);\n        match event.r#type {\n            1000..=1099 => {\n                _log_user_event(\n                    event.r#type,\n                    &headers.user.uuid,\n                    headers.device.atype,\n                    Some(event_date),\n                    &headers.ip.ip,\n                    &conn,\n                )\n                .await;\n            }\n            1600..=1699 => {\n                if let Some(org_id) = &event.organization_id {\n                    _log_event(\n                        event.r#type,\n                        org_id,\n                        org_id,\n                        &headers.user.uuid,\n                        headers.device.atype,\n                        Some(event_date),\n                        &headers.ip.ip,\n                        &conn,\n                    )\n                    .await;\n                }\n            }\n            _ => {\n                if let Some(cipher_uuid) = &event.cipher_id {\n                    if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &conn).await {\n                        if let Some(org_id) = cipher.organization_uuid {\n                            _log_event(\n                                event.r#type,\n                                cipher_uuid,\n                                &org_id,\n                                &headers.user.uuid,\n                                headers.device.atype,\n                                Some(event_date),\n                                &headers.ip.ip,\n                                &conn,\n                            )\n                            .await;\n                        }\n                    }\n                }\n            }\n        }\n    }\n    Ok(())\n}\n\npub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &DbConn) {\n    if !CONFIG.org_events_enabled() {\n        return;\n    }\n    _log_user_event(event_type, user_id, device_type, None, ip, conn).await;\n}\n\nasync fn _log_user_event(\n    event_type: i32,\n    user_id: &UserId,\n    device_type: i32,\n    event_date: Option<NaiveDateTime>,\n    ip: &IpAddr,\n    conn: &DbConn,\n) {\n    let memberships = Membership::find_by_user(user_id, conn).await;\n    let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org\n\n    // Upstream saves the event also without any org_id.\n    let mut event = Event::new(event_type, event_date);\n    event.user_uuid = Some(user_id.clone());\n    event.act_user_uuid = Some(user_id.clone());\n    event.device_type = Some(device_type);\n    event.ip_address = Some(ip.to_string());\n    events.push(event);\n\n    // For each org a user is a member of store these events per org\n    for membership in memberships {\n        let mut event = Event::new(event_type, event_date);\n        event.user_uuid = Some(user_id.clone());\n        event.org_uuid = Some(membership.org_uuid);\n        event.org_user_uuid = Some(membership.uuid);\n        event.act_user_uuid = Some(user_id.clone());\n        event.device_type = Some(device_type);\n        event.ip_address = Some(ip.to_string());\n        events.push(event);\n    }\n\n    Event::save_user_event(events, conn).await.unwrap_or(());\n}\n\npub async fn log_event(\n    event_type: i32,\n    source_uuid: &str,\n    org_id: &OrganizationId,\n    act_user_id: &UserId,\n    device_type: i32,\n    ip: &IpAddr,\n    conn: &DbConn,\n) {\n    if !CONFIG.org_events_enabled() {\n        return;\n    }\n    _log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;\n}\n\n#[allow(clippy::too_many_arguments)]\nasync fn _log_event(\n    event_type: i32,\n    source_uuid: &str,\n    org_id: &OrganizationId,\n    act_user_id: &UserId,\n    device_type: i32,\n    event_date: Option<NaiveDateTime>,\n    ip: &IpAddr,\n    conn: &DbConn,\n) {\n    // Create a new empty event\n    let mut event = Event::new(event_type, event_date);\n    match event_type {\n        // 1000..=1099 Are user events, they need to be logged via log_user_event()\n        // Cipher Events\n        1100..=1199 => {\n            event.cipher_uuid = Some(source_uuid.to_string().into());\n        }\n        // Collection Events\n        1300..=1399 => {\n            event.collection_uuid = Some(source_uuid.to_string().into());\n        }\n        // Group Events\n        1400..=1499 => {\n            event.group_uuid = Some(source_uuid.to_string().into());\n        }\n        // Org User Events\n        1500..=1599 => {\n            event.org_user_uuid = Some(source_uuid.to_string().into());\n        }\n        // 1600..=1699 Are organizational events, and they do not need the source_uuid\n        // Policy Events\n        1700..=1799 => {\n            event.policy_uuid = Some(source_uuid.to_string().into());\n        }\n        // Ignore others\n        _ => {}\n    }\n\n    event.org_uuid = Some(org_id.clone());\n    event.act_user_uuid = Some(act_user_id.clone());\n    event.device_type = Some(device_type);\n    event.ip_address = Some(ip.to_string());\n    event.save(conn).await.unwrap_or(());\n}\n\npub async fn event_cleanup_job(pool: DbPool) {\n    debug!(\"Start events cleanup job\");\n    if CONFIG.events_days_retain().is_none() {\n        debug!(\"events_days_retain is not configured, abort\");\n        return;\n    }\n\n    if let Ok(conn) = pool.get().await {\n        Event::clean_events(&conn).await.ok();\n    } else {\n        error!(\"Failed to get DB connection while trying to cleanup the events table\")\n    }\n}\n"
  },
  {
    "path": "src/api/core/folders.rs",
    "content": "use rocket::serde::json::Json;\nuse serde_json::Value;\n\nuse crate::{\n    api::{EmptyResult, JsonResult, Notify, UpdateType},\n    auth::Headers,\n    db::{\n        models::{Folder, FolderId},\n        DbConn,\n    },\n};\n\npub fn routes() -> Vec<rocket::Route> {\n    routes![get_folders, get_folder, post_folders, post_folder, put_folder, delete_folder_post, delete_folder,]\n}\n\n#[get(\"/folders\")]\nasync fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {\n    let folders = Folder::find_by_user(&headers.user.uuid, &conn).await;\n    let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();\n\n    Json(json!({\n      \"data\": folders_json,\n      \"object\": \"list\",\n      \"continuationToken\": null,\n    }))\n}\n\n#[get(\"/folders/<folder_id>\")]\nasync fn get_folder(folder_id: FolderId, headers: Headers, conn: DbConn) -> JsonResult {\n    match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await {\n        Some(folder) => Ok(Json(folder.to_json())),\n        _ => err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\"),\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct FolderData {\n    pub name: String,\n    pub id: Option<FolderId>,\n}\n\n#[post(\"/folders\", data = \"<data>\")]\nasync fn post_folders(data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    let data: FolderData = data.into_inner();\n\n    let mut folder = Folder::new(headers.user.uuid, data.name);\n\n    folder.save(&conn).await?;\n    nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device, &conn).await;\n\n    Ok(Json(folder.to_json()))\n}\n\n#[post(\"/folders/<folder_id>\", data = \"<data>\")]\nasync fn post_folder(\n    folder_id: FolderId,\n    data: Json<FolderData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    put_folder(folder_id, data, headers, conn, nt).await\n}\n\n#[put(\"/folders/<folder_id>\", data = \"<data>\")]\nasync fn put_folder(\n    folder_id: FolderId,\n    data: Json<FolderData>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let data: FolderData = data.into_inner();\n\n    let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await else {\n        err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\")\n    };\n\n    folder.name = data.name;\n\n    folder.save(&conn).await?;\n    nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device, &conn).await;\n\n    Ok(Json(folder.to_json()))\n}\n\n#[post(\"/folders/<folder_id>/delete\")]\nasync fn delete_folder_post(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    delete_folder(folder_id, headers, conn, nt).await\n}\n\n#[delete(\"/folders/<folder_id>\")]\nasync fn delete_folder(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &conn).await else {\n        err!(\"Invalid folder\", \"Folder does not exist or belongs to another user\")\n    };\n\n    // Delete the actual folder entry\n    folder.delete(&conn).await?;\n\n    nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device, &conn).await;\n    Ok(())\n}\n"
  },
  {
    "path": "src/api/core/mod.rs",
    "content": "pub mod accounts;\nmod ciphers;\nmod emergency_access;\nmod events;\nmod folders;\nmod organizations;\nmod public;\nmod sends;\npub mod two_factor;\n\npub use accounts::purge_auth_requests;\npub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};\npub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};\npub use events::{event_cleanup_job, log_event, log_user_event};\nuse reqwest::Method;\npub use sends::purge_sends;\n\npub fn routes() -> Vec<Route> {\n    let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];\n    let mut hibp_routes = routes![hibp_breach];\n    let mut meta_routes = routes![alive, now, version, config, get_api_webauthn];\n\n    let mut routes = Vec::new();\n    routes.append(&mut accounts::routes());\n    routes.append(&mut ciphers::routes());\n    routes.append(&mut emergency_access::routes());\n    routes.append(&mut events::routes());\n    routes.append(&mut folders::routes());\n    routes.append(&mut organizations::routes());\n    routes.append(&mut two_factor::routes());\n    routes.append(&mut sends::routes());\n    routes.append(&mut public::routes());\n    routes.append(&mut eq_domains_routes);\n    routes.append(&mut hibp_routes);\n    routes.append(&mut meta_routes);\n\n    routes\n}\n\npub fn events_routes() -> Vec<Route> {\n    let mut routes = Vec::new();\n    routes.append(&mut events::main_routes());\n\n    routes\n}\n\n//\n// Move this somewhere else\n//\nuse rocket::{serde::json::Json, serde::json::Value, Catcher, Route};\n\nuse crate::{\n    api::{EmptyResult, JsonResult, Notify, UpdateType},\n    auth::Headers,\n    db::{\n        models::{Membership, MembershipStatus, OrgPolicy, Organization, User},\n        DbConn,\n    },\n    error::Error,\n    http_client::make_http_request,\n    mail,\n    util::parse_experimental_client_feature_flags,\n};\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct GlobalDomain {\n    r#type: i32,\n    domains: Vec<String>,\n    excluded: bool,\n}\n\nconst GLOBAL_DOMAINS: &str = include_str!(\"../../static/global_domains.json\");\n\n#[get(\"/settings/domains\")]\nfn get_eq_domains(headers: Headers) -> Json<Value> {\n    _get_eq_domains(&headers, false)\n}\n\nfn _get_eq_domains(headers: &Headers, no_excluded: bool) -> Json<Value> {\n    let user = &headers.user;\n    use serde_json::from_str;\n\n    let equivalent_domains: Vec<Vec<String>> = from_str(&user.equivalent_domains).unwrap();\n    let excluded_globals: Vec<i32> = from_str(&user.excluded_globals).unwrap();\n\n    let mut globals: Vec<GlobalDomain> = from_str(GLOBAL_DOMAINS).unwrap();\n\n    for global in &mut globals {\n        global.excluded = excluded_globals.contains(&global.r#type);\n    }\n\n    if no_excluded {\n        globals.retain(|g| !g.excluded);\n    }\n\n    Json(json!({\n        \"equivalentDomains\": equivalent_domains,\n        \"globalEquivalentDomains\": globals,\n        \"object\": \"domains\",\n    }))\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EquivDomainData {\n    excluded_global_equivalent_domains: Option<Vec<i32>>,\n    equivalent_domains: Option<Vec<Vec<String>>>,\n}\n\n#[post(\"/settings/domains\", data = \"<data>\")]\nasync fn post_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    let data: EquivDomainData = data.into_inner();\n\n    let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default();\n    let equivalent_domains = data.equivalent_domains.unwrap_or_default();\n\n    let mut user = headers.user;\n    use serde_json::to_string;\n\n    user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| \"[]\".to_string());\n    user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| \"[]\".to_string());\n\n    user.save(&conn).await?;\n\n    nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &conn).await;\n\n    Ok(Json(json!({})))\n}\n\n#[put(\"/settings/domains\", data = \"<data>\")]\nasync fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    post_eq_domains(data, headers, conn, nt).await\n}\n\n#[get(\"/hibp/breach?<username>\")]\nasync fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {\n    let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();\n    if let Some(api_key) = crate::CONFIG.hibp_api_key() {\n        let url = format!(\n            \"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false\"\n        );\n\n        let res = make_http_request(Method::GET, &url)?.header(\"hibp-api-key\", api_key).send().await?;\n\n        // If we get a 404, return a 404, it means no breached accounts\n        if res.status() == 404 {\n            return Err(Error::empty().with_code(404));\n        }\n\n        let value: Value = res.error_for_status()?.json().await?;\n        Ok(Json(value))\n    } else {\n        Ok(Json(json!([{\n            \"name\": \"HaveIBeenPwned\",\n            \"title\": \"Manual HIBP Check\",\n            \"domain\": \"haveibeenpwned.com\",\n            \"breachDate\": \"2019-08-18T00:00:00Z\",\n            \"addedDate\": \"2019-08-18T00:00:00Z\",\n            \"description\": format!(\"Go to: <a href=\\\"https://haveibeenpwned.com/account/{username}\\\" target=\\\"_blank\\\" rel=\\\"noreferrer\\\">https://haveibeenpwned.com/account/{username}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\\\"https://haveibeenpwned.com/API/Key\\\" target=\\\"_blank\\\" rel=\\\"noreferrer\\\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>\"),\n            \"logoPath\": \"vw_static/hibp.png\",\n            \"pwnCount\": 0,\n            \"dataClasses\": [\n                \"Error - No API key set!\"\n            ]\n        }])))\n    }\n}\n\n// We use DbConn here to let the alive healthcheck also verify the database connection.\n#[get(\"/alive\")]\nfn alive(_conn: DbConn) -> Json<String> {\n    now()\n}\n\n#[get(\"/now\")]\npub fn now() -> Json<String> {\n    Json(crate::util::format_date(&chrono::Utc::now().naive_utc()))\n}\n\n#[get(\"/version\")]\nfn version() -> Json<&'static str> {\n    Json(crate::VERSION.unwrap_or_default())\n}\n\n#[get(\"/webauthn\")]\nfn get_api_webauthn(_headers: Headers) -> Json<Value> {\n    // Prevent a 404 error, which also causes key-rotation issues\n    // It looks like this is used when login with passkeys is enabled, which Vaultwarden does not (yet) support\n    // An empty list/data also works fine\n    Json(json!({\n        \"object\": \"list\",\n        \"data\": [],\n        \"continuationToken\": null\n    }))\n}\n\n#[get(\"/config\")]\nfn config() -> Json<Value> {\n    let domain = crate::CONFIG.domain();\n    // Official available feature flags can be found here:\n    // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103\n    // Client (web-v2025.6.1): https://github.com/bitwarden/clients/blob/747c2fd6a1c348a57a76e4a7de8128466ffd3c01/libs/common/src/enums/feature-flag.enum.ts#L12\n    // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22\n    // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7\n    let mut feature_states =\n        parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());\n    feature_states.insert(\"duo-redirect\".to_string(), true);\n    feature_states.insert(\"email-verification\".to_string(), true);\n    feature_states.insert(\"unauth-ui-refresh\".to_string(), true);\n    feature_states.insert(\"enable-pm-flight-recorder\".to_string(), true);\n    feature_states.insert(\"mobile-error-reporting\".to_string(), true);\n\n    Json(json!({\n        // Note: The clients use this version to handle backwards compatibility concerns\n        // This means they expect a version that closely matches the Bitwarden server version\n        // We should make sure that we keep this updated when we support the new server features\n        // Version history:\n        // - Individual cipher key encryption: 2024.2.0\n        // - Mobile app support for MasterPasswordUnlockData: 2025.8.0\n        \"version\": \"2025.12.0\",\n        \"gitHash\": option_env!(\"GIT_REV\"),\n        \"server\": {\n          \"name\": \"Vaultwarden\",\n          \"url\": \"https://github.com/dani-garcia/vaultwarden\"\n        },\n        \"settings\": {\n            \"disableUserRegistration\": crate::CONFIG.is_signup_disabled()\n        },\n        \"environment\": {\n          \"vault\": domain,\n          \"api\": format!(\"{domain}/api\"),\n          \"identity\": format!(\"{domain}/identity\"),\n          \"notifications\": format!(\"{domain}/notifications\"),\n          \"sso\": \"\",\n          \"cloudRegion\": null,\n        },\n        // Bitwarden uses this for the self-hosted servers to indicate the default push technology\n        \"push\": {\n          \"pushTechnology\": 0,\n          \"vapidPublicKey\": null\n        },\n        \"featureStates\": feature_states,\n        \"object\": \"config\",\n    }))\n}\n\npub fn catchers() -> Vec<Catcher> {\n    catchers![api_not_found]\n}\n\n#[catch(404)]\nfn api_not_found() -> Json<Value> {\n    Json(json!({\n        \"error\": {\n            \"code\": 404,\n            \"reason\": \"Not Found\",\n            \"description\": \"The requested resource could not be found.\"\n        }\n    }))\n}\n\nasync fn accept_org_invite(\n    user: &User,\n    mut member: Membership,\n    reset_password_key: Option<String>,\n    conn: &DbConn,\n) -> EmptyResult {\n    if member.status != MembershipStatus::Invited as i32 {\n        err!(\"User already accepted the invitation\");\n    }\n\n    member.status = MembershipStatus::Accepted as i32;\n    member.reset_password_key = reset_password_key;\n\n    // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type\n    OrgPolicy::check_user_allowed(&member, \"join\", conn).await?;\n\n    member.save(conn).await?;\n\n    if crate::CONFIG.mail_enabled() {\n        let org = match Organization::find_by_uuid(&member.org_uuid, conn).await {\n            Some(org) => org,\n            None => err!(\"Organization not found.\"),\n        };\n        // User was invited to an organization, so they must be confirmed manually after acceptance\n        mail::send_invite_accepted(&user.email, &member.invited_by_email.unwrap_or(org.billing_email), &org.name)\n            .await?;\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/api/core/organizations.rs",
    "content": "use num_traits::FromPrimitive;\nuse rocket::serde::json::Json;\nuse rocket::Route;\nuse serde_json::Value;\nuse std::collections::{HashMap, HashSet};\n\nuse crate::api::admin::FAKE_ADMIN_UUID;\nuse crate::{\n    api::{\n        core::{accept_org_invite, log_event, two_factor, CipherSyncData, CipherSyncType},\n        EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,\n    },\n    auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders},\n    db::{\n        models::{\n            Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType,\n            Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType,\n            OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId,\n        },\n        DbConn,\n    },\n    mail,\n    util::{convert_json_key_lcase_first, get_uuid, NumberOrString},\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![\n        get_organization,\n        create_organization,\n        delete_organization,\n        post_delete_organization,\n        leave_organization,\n        get_user_collections,\n        get_org_collections,\n        get_org_collections_details,\n        get_org_collection_detail,\n        get_collection_users,\n        put_organization,\n        post_organization,\n        post_organization_collections,\n        post_bulk_access_collections,\n        post_organization_collection_update,\n        put_organization_collection_update,\n        delete_organization_collection,\n        post_organization_collection_delete,\n        bulk_delete_organization_collections,\n        post_bulk_collections,\n        get_org_details,\n        get_org_domain_sso_verified,\n        get_members,\n        send_invite,\n        reinvite_member,\n        bulk_reinvite_members,\n        confirm_invite,\n        bulk_confirm_invite,\n        accept_invite,\n        get_org_user_mini_details,\n        get_user,\n        edit_member,\n        put_member,\n        delete_member,\n        bulk_delete_member,\n        post_org_import,\n        list_policies,\n        list_policies_token,\n        get_master_password_policy,\n        get_policy,\n        put_policy,\n        put_policy_vnext,\n        get_plans,\n        post_org_keys,\n        get_organization_keys,\n        get_organization_public_key,\n        bulk_public_keys,\n        revoke_member,\n        bulk_revoke_members,\n        restore_member,\n        bulk_restore_members,\n        get_groups,\n        get_groups_details,\n        post_groups,\n        get_group,\n        put_group,\n        post_group,\n        get_group_details,\n        delete_group,\n        post_delete_group,\n        bulk_delete_groups,\n        get_group_members,\n        put_group_members,\n        post_delete_group_member,\n        put_reset_password_enrollment,\n        get_reset_password_details,\n        put_reset_password,\n        get_org_export,\n        api_key,\n        rotate_api_key,\n        get_billing_metadata,\n        get_billing_warnings,\n        get_auto_enroll_status,\n    ]\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgData {\n    billing_email: String,\n    collection_name: String,\n    key: String,\n    name: String,\n    keys: Option<OrgKeyData>,\n    #[allow(dead_code)]\n    plan_type: NumberOrString, // Ignored, always use the same plan\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrganizationUpdateData {\n    billing_email: String,\n    name: String,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct FullCollectionData {\n    name: String,\n    groups: Vec<CollectionGroupData>,\n    users: Vec<CollectionMembershipData>,\n    id: Option<CollectionId>,\n    external_id: Option<String>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct CollectionGroupData {\n    hide_passwords: bool,\n    id: GroupId,\n    read_only: bool,\n    manage: bool,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct CollectionMembershipData {\n    hide_passwords: bool,\n    id: MembershipId,\n    read_only: bool,\n    manage: bool,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgKeyData {\n    encrypted_private_key: String,\n    public_key: String,\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkGroupIds {\n    ids: Vec<GroupId>,\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkMembershipIds {\n    ids: Vec<MembershipId>,\n}\n\n#[post(\"/organizations\", data = \"<data>\")]\nasync fn create_organization(headers: Headers, data: Json<OrgData>, conn: DbConn) -> JsonResult {\n    if !CONFIG.is_org_creation_allowed(&headers.user.email) {\n        err!(\"User not allowed to create organizations\")\n    }\n    if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, None, &conn).await {\n        err!(\n            \"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization.\"\n        )\n    }\n\n    let data: OrgData = data.into_inner();\n    let (private_key, public_key) = if let Some(keys) = data.keys {\n        (Some(keys.encrypted_private_key), Some(keys.public_key))\n    } else {\n        (None, None)\n    };\n\n    let org = Organization::new(data.name, &data.billing_email, private_key, public_key);\n    let mut member = Membership::new(headers.user.uuid, org.uuid.clone(), None);\n    let collection = Collection::new(org.uuid.clone(), data.collection_name, None);\n\n    member.akey = data.key;\n    member.access_all = true;\n    member.atype = MembershipType::Owner as i32;\n    member.status = MembershipStatus::Confirmed as i32;\n\n    org.save(&conn).await?;\n    member.save(&conn).await?;\n    collection.save(&conn).await?;\n\n    Ok(Json(org.to_json()))\n}\n\n#[delete(\"/organizations/<org_id>\", data = \"<data>\")]\nasync fn delete_organization(\n    org_id: OrganizationId,\n    data: Json<PasswordOrOtpData>,\n    headers: OwnerHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: PasswordOrOtpData = data.into_inner();\n\n    data.validate(&headers.user, true, &conn).await?;\n\n    match Organization::find_by_uuid(&org_id, &conn).await {\n        None => err!(\"Organization not found\"),\n        Some(org) => org.delete(&conn).await,\n    }\n}\n\n#[post(\"/organizations/<org_id>/delete\", data = \"<data>\")]\nasync fn post_delete_organization(\n    org_id: OrganizationId,\n    data: Json<PasswordOrOtpData>,\n    headers: OwnerHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    delete_organization(org_id, data, headers, conn).await\n}\n\n#[post(\"/organizations/<org_id>/leave\")]\nasync fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbConn) -> EmptyResult {\n    match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await {\n        None => err!(\"User not part of organization\"),\n        Some(member) => {\n            if member.atype == MembershipType::Owner\n                && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1\n            {\n                err!(\"The last owner can't leave\")\n            }\n\n            log_event(\n                EventType::OrganizationUserLeft as i32,\n                &member.uuid,\n                &org_id,\n                &headers.user.uuid,\n                headers.device.atype,\n                &headers.ip.ip,\n                &conn,\n            )\n            .await;\n\n            member.delete(&conn).await\n        }\n    }\n}\n\n#[get(\"/organizations/<org_id>\")]\nasync fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    match Organization::find_by_uuid(&org_id, &conn).await {\n        Some(organization) => Ok(Json(organization.to_json())),\n        None => err!(\"Can't find organization details\"),\n    }\n}\n\n#[put(\"/organizations/<org_id>\", data = \"<data>\")]\nasync fn put_organization(\n    org_id: OrganizationId,\n    headers: OwnerHeaders,\n    data: Json<OrganizationUpdateData>,\n    conn: DbConn,\n) -> JsonResult {\n    post_organization(org_id, headers, data, conn).await\n}\n\n#[post(\"/organizations/<org_id>\", data = \"<data>\")]\nasync fn post_organization(\n    org_id: OrganizationId,\n    headers: OwnerHeaders,\n    data: Json<OrganizationUpdateData>,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    let data: OrganizationUpdateData = data.into_inner();\n\n    let Some(mut org) = Organization::find_by_uuid(&org_id, &conn).await else {\n        err!(\"Organization not found\")\n    };\n\n    org.name = data.name;\n    org.billing_email = data.billing_email.to_lowercase();\n\n    org.save(&conn).await?;\n\n    log_event(\n        EventType::OrganizationUpdated as i32,\n        org_id.as_ref(),\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(org.to_json()))\n}\n\n// GET /api/collections?writeOnly=false\n#[get(\"/collections\")]\nasync fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {\n    Json(json!({\n        \"data\":\n            Collection::find_by_user_uuid(headers.user.uuid, &conn).await\n            .iter()\n            .map(Collection::to_json)\n            .collect::<Value>(),\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    }))\n}\n\n// Called during the SSO enrollment\n// The `identifier` should be the value returned by `get_org_domain_sso_verified`\n// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it\n#[get(\"/organizations/<identifier>/auto-enroll-status\")]\nasync fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn) -> JsonResult {\n    let org = if identifier == crate::sso::FAKE_IDENTIFIER {\n        match Membership::find_main_user_org(&headers.user.uuid, &conn).await {\n            Some(member) => Organization::find_by_uuid(&member.org_uuid, &conn).await,\n            None => None,\n        }\n    } else {\n        Organization::find_by_uuid(&identifier.into(), &conn).await\n    };\n\n    let (id, identifier, rp_auto_enroll) = match org {\n        None => (get_uuid(), identifier.to_string(), false),\n        Some(org) => (\n            org.uuid.to_string(),\n            org.uuid.to_string(),\n            OrgPolicy::org_is_reset_password_auto_enroll(&org.uuid, &conn).await,\n        ),\n    };\n\n    Ok(Json(json!({\n        \"id\": id,\n        \"identifier\": identifier,\n        \"resetPasswordEnabled\": rp_auto_enroll,\n    })))\n}\n\n#[get(\"/organizations/<org_id>/collections\")]\nasync fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    if !headers.membership.has_full_access() {\n        err_code!(\"Resource not found.\", \"User does not have full access\", rocket::http::Status::NotFound.code);\n    }\n\n    Ok(Json(json!({\n        \"data\": _get_org_collections(&org_id, &conn).await,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\n#[get(\"/organizations/<org_id>/collections/details\")]\nasync fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else {\n        err!(\"User is not part of organization\")\n    };\n\n    // get all collection memberships for the current organization\n    let col_users = CollectionUser::find_by_organization_swap_user_uuid_with_member_uuid(&org_id, &conn).await;\n    // Generate a HashMap to get the correct MembershipType per user to determine the manage permission\n    // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser\n    let membership_type: HashMap<MembershipId, i32> =\n        Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| (m.uuid, m.atype)).collect();\n\n    // check if current user has full access to the organization (either directly or via any group)\n    let has_full_access_to_org = member.has_full_access()\n        || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &conn).await);\n\n    // Get all admins, owners and managers who can manage/access all\n    // Those are currently not listed in the col_users but need to be listed too.\n    let manage_all_members: Vec<Value> = Membership::find_confirmed_and_manage_all_by_org(&org_id, &conn)\n        .await\n        .into_iter()\n        .map(|member| {\n            json!({\n                \"id\": member.uuid,\n                \"readOnly\": false,\n                \"hidePasswords\": false,\n                \"manage\": true,\n            })\n        })\n        .collect();\n\n    let mut data = Vec::new();\n    for col in Collection::find_by_organization(&org_id, &conn).await {\n        // check whether the current user has access to the given collection\n        let assigned = has_full_access_to_org\n            || CollectionUser::has_access_to_collection_by_user(&col.uuid, &member.user_uuid, &conn).await\n            || (CONFIG.org_groups_enabled()\n                && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &conn).await);\n\n        // If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection\n        if !assigned {\n            continue;\n        }\n\n        // get the users assigned directly to the given collection\n        let mut users: Vec<Value> = col_users\n            .iter()\n            .filter(|collection_member| collection_member.collection_uuid == col.uuid)\n            .map(|collection_member| {\n                collection_member.to_json_details_for_member(\n                    *membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)),\n                )\n            })\n            .collect();\n        users.extend_from_slice(&manage_all_members);\n\n        // get the group details for the given collection\n        let groups: Vec<Value> = if CONFIG.org_groups_enabled() {\n            CollectionGroup::find_by_collection(&col.uuid, &conn)\n                .await\n                .iter()\n                .map(|collection_group| collection_group.to_json_details_for_group())\n                .collect()\n        } else {\n            Vec::with_capacity(0)\n        };\n\n        let mut json_object = col.to_json_details(&headers.user.uuid, None, &conn).await;\n        json_object[\"assigned\"] = json!(assigned);\n        json_object[\"users\"] = json!(users);\n        json_object[\"groups\"] = json!(groups);\n        json_object[\"object\"] = json!(\"collectionAccessDetails\");\n        json_object[\"unmanaged\"] = json!(false);\n        data.push(json_object)\n    }\n\n    Ok(Json(json!({\n        \"data\": data,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\nasync fn _get_org_collections(org_id: &OrganizationId, conn: &DbConn) -> Value {\n    Collection::find_by_organization(org_id, conn).await.iter().map(Collection::to_json).collect::<Value>()\n}\n\n#[post(\"/organizations/<org_id>/collections\", data = \"<data>\")]\nasync fn post_organization_collections(\n    org_id: OrganizationId,\n    headers: ManagerHeadersLoose,\n    data: Json<FullCollectionData>,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: FullCollectionData = data.into_inner();\n\n    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {\n        err!(\"Can't find organization details\")\n    };\n\n    let collection = Collection::new(org.uuid, data.name, data.external_id);\n    collection.save(&conn).await?;\n\n    log_event(\n        EventType::CollectionCreated as i32,\n        &collection.uuid,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    for group in data.groups {\n        CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)\n            .save(&conn)\n            .await?;\n    }\n\n    for user in data.users {\n        let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {\n            err!(\"User is not part of organization\")\n        };\n\n        if member.access_all {\n            continue;\n        }\n\n        CollectionUser::save(\n            &member.user_uuid,\n            &collection.uuid,\n            user.read_only,\n            user.hide_passwords,\n            user.manage,\n            &conn,\n        )\n        .await?;\n    }\n\n    if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {\n        CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &conn).await?;\n    }\n\n    Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &conn).await))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkCollectionAccessData {\n    collection_ids: Vec<CollectionId>,\n    groups: Vec<CollectionGroupData>,\n    users: Vec<CollectionMembershipData>,\n}\n\n#[post(\"/organizations/<org_id>/collections/bulk-access\", data = \"<data>\", rank = 1)]\nasync fn post_bulk_access_collections(\n    org_id: OrganizationId,\n    headers: ManagerHeadersLoose,\n    data: Json<BulkCollectionAccessData>,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: BulkCollectionAccessData = data.into_inner();\n\n    if Organization::find_by_uuid(&org_id, &conn).await.is_none() {\n        err!(\"Can't find organization details\")\n    };\n\n    for col_id in data.collection_ids {\n        let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {\n            err!(\"Collection not found\")\n        };\n\n        if !collection.is_manageable_by_user(&headers.membership.user_uuid, &conn).await {\n            err!(\"Collection not found\", \"The current user isn't a manager for this collection\")\n        }\n\n        // update collection modification date\n        collection.save(&conn).await?;\n\n        log_event(\n            EventType::CollectionUpdated as i32,\n            &collection.uuid,\n            &org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n\n        CollectionGroup::delete_all_by_collection(&col_id, &conn).await?;\n        for group in &data.groups {\n            CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage)\n                .save(&conn)\n                .await?;\n        }\n\n        CollectionUser::delete_all_by_collection(&col_id, &conn).await?;\n        for user in &data.users {\n            let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {\n                err!(\"User is not part of organization\")\n            };\n\n            if member.access_all {\n                continue;\n            }\n\n            CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &conn)\n                .await?;\n        }\n    }\n\n    Ok(())\n}\n\n#[put(\"/organizations/<org_id>/collections/<col_id>\", data = \"<data>\")]\nasync fn put_organization_collection_update(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    data: Json<FullCollectionData>,\n    conn: DbConn,\n) -> JsonResult {\n    post_organization_collection_update(org_id, col_id, headers, data, conn).await\n}\n\n#[post(\"/organizations/<org_id>/collections/<col_id>\", data = \"<data>\", rank = 2)]\nasync fn post_organization_collection_update(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    data: Json<FullCollectionData>,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: FullCollectionData = data.into_inner();\n\n    if Organization::find_by_uuid(&org_id, &conn).await.is_none() {\n        err!(\"Can't find organization details\")\n    };\n\n    let Some(mut collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {\n        err!(\"Collection not found\")\n    };\n\n    collection.name = data.name;\n    collection.external_id = match data.external_id {\n        Some(external_id) if !external_id.trim().is_empty() => Some(external_id),\n        _ => None,\n    };\n\n    collection.save(&conn).await?;\n\n    log_event(\n        EventType::CollectionUpdated as i32,\n        &collection.uuid,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    CollectionGroup::delete_all_by_collection(&col_id, &conn).await?;\n\n    for group in data.groups {\n        CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)\n            .save(&conn)\n            .await?;\n    }\n\n    CollectionUser::delete_all_by_collection(&col_id, &conn).await?;\n\n    for user in data.users {\n        let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &conn).await else {\n            err!(\"User is not part of organization\")\n        };\n\n        if member.access_all {\n            continue;\n        }\n\n        CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &conn)\n            .await?;\n    }\n\n    Ok(Json(collection.to_json_details(&headers.user.uuid, None, &conn).await))\n}\n\nasync fn _delete_organization_collection(\n    org_id: &OrganizationId,\n    col_id: &CollectionId,\n    headers: &ManagerHeaders,\n    conn: &DbConn,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {\n        err!(\"Collection not found\", \"Collection does not exist or does not belong to this organization\")\n    };\n    log_event(\n        EventType::CollectionDeleted as i32,\n        &collection.uuid,\n        org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        conn,\n    )\n    .await;\n    collection.delete(conn).await\n}\n\n#[delete(\"/organizations/<org_id>/collections/<col_id>\")]\nasync fn delete_organization_collection(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    _delete_organization_collection(&org_id, &col_id, &headers, &conn).await\n}\n\n#[post(\"/organizations/<org_id>/collections/<col_id>/delete\")]\nasync fn post_organization_collection_delete(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    _delete_organization_collection(&org_id, &col_id, &headers, &conn).await\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkCollectionIds {\n    ids: Vec<CollectionId>,\n}\n\n#[delete(\"/organizations/<org_id>/collections\", data = \"<data>\")]\nasync fn bulk_delete_organization_collections(\n    org_id: OrganizationId,\n    headers: ManagerHeadersLoose,\n    data: Json<BulkCollectionIds>,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: BulkCollectionIds = data.into_inner();\n\n    let collections = data.ids;\n\n    let headers = ManagerHeaders::from_loose(headers, &collections, &conn).await?;\n\n    for col_id in collections {\n        _delete_organization_collection(&org_id, &col_id, &headers, &conn).await?\n    }\n    Ok(())\n}\n\n#[get(\"/organizations/<org_id>/collections/<col_id>/details\")]\nasync fn get_org_collection_detail(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &conn).await {\n        None => err!(\"Collection not found\"),\n        Some(collection) => {\n            if collection.org_uuid != org_id {\n                err!(\"Collection is not owned by organization\")\n            }\n\n            let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else {\n                err!(\"User is not part of organization\")\n            };\n\n            let groups: Vec<Value> = if CONFIG.org_groups_enabled() {\n                CollectionGroup::find_by_collection(&collection.uuid, &conn)\n                    .await\n                    .iter()\n                    .map(|collection_group| collection_group.to_json_details_for_group())\n                    .collect()\n            } else {\n                // The Bitwarden clients seem to call this API regardless of whether groups are enabled,\n                // so just act as if there are no groups.\n                Vec::with_capacity(0)\n            };\n\n            // Generate a HashMap to get the correct MembershipType per user to determine the manage permission\n            // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser\n            let membership_type: HashMap<MembershipId, i32> = Membership::find_confirmed_by_org(&org_id, &conn)\n                .await\n                .into_iter()\n                .map(|m| (m.uuid, m.atype))\n                .collect();\n\n            let users: Vec<Value> =\n                CollectionUser::find_by_org_and_coll_swap_user_uuid_with_member_uuid(&org_id, &collection.uuid, &conn)\n                    .await\n                    .iter()\n                    .map(|collection_member| {\n                        collection_member.to_json_details_for_member(\n                            *membership_type\n                                .get(&collection_member.membership_uuid)\n                                .unwrap_or(&(MembershipType::User as i32)),\n                        )\n                    })\n                    .collect();\n\n            let assigned = Collection::can_access_collection(&member, &collection.uuid, &conn).await;\n\n            let mut json_object = collection.to_json_details(&headers.user.uuid, None, &conn).await;\n            json_object[\"assigned\"] = json!(assigned);\n            json_object[\"users\"] = json!(users);\n            json_object[\"groups\"] = json!(groups);\n            json_object[\"object\"] = json!(\"collectionAccessDetails\");\n\n            Ok(Json(json_object))\n        }\n    }\n}\n\n#[get(\"/organizations/<org_id>/collections/<col_id>/users\")]\nasync fn get_collection_users(\n    org_id: OrganizationId,\n    col_id: CollectionId,\n    headers: ManagerHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    // Get org and collection, check that collection is from org\n    let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else {\n        err!(\"Collection not found in Organization\")\n    };\n\n    let mut member_list = Vec::new();\n    for col_user in CollectionUser::find_by_collection(&collection.uuid, &conn).await {\n        member_list.push(\n            Membership::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)\n                .await\n                .unwrap()\n                .to_json_user_access_restrictions(&col_user),\n        );\n    }\n\n    Ok(Json(json!(member_list)))\n}\n\n#[derive(FromForm)]\nstruct OrgIdData {\n    #[field(name = \"organizationId\")]\n    organization_id: OrganizationId,\n}\n\n#[get(\"/ciphers/organization-details?<data..>\")]\nasync fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    if data.organization_id != headers.membership.org_uuid {\n        err_code!(\"Resource not found.\", \"Organization id's do not match\", rocket::http::Status::NotFound.code);\n    }\n\n    if !headers.membership.has_full_access() {\n        err_code!(\"Resource not found.\", \"User does not have full access\", rocket::http::Status::NotFound.code);\n    }\n\n    Ok(Json(json!({\n        \"data\": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await?,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\nasync fn _get_org_details(\n    org_id: &OrganizationId,\n    host: &str,\n    user_id: &UserId,\n    conn: &DbConn,\n) -> Result<Value, crate::Error> {\n    let ciphers = Cipher::find_by_org(org_id, conn).await;\n    let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await;\n\n    let mut ciphers_json = Vec::with_capacity(ciphers.len());\n    for c in ciphers {\n        ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await?);\n    }\n    Ok(json!(ciphers_json))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgDomainDetails {\n    email: String,\n}\n\n// Returning a Domain/Organization here allow to prefill it and prevent prompting the user\n// So we either return an Org name associated to the user or a dummy value.\n// In use since `v2025.6.0`, appears to use only the first `organizationIdentifier`\n#[post(\"/organizations/domain/sso/verified\", data = \"<data>\")]\nasync fn get_org_domain_sso_verified(data: Json<OrgDomainDetails>, conn: DbConn) -> JsonResult {\n    let data: OrgDomainDetails = data.into_inner();\n\n    let identifiers = match Organization::find_org_user_email(&data.email, &conn)\n        .await\n        .into_iter()\n        .map(|o| (o.name, o.uuid.to_string()))\n        .collect::<Vec<(String, String)>>()\n    {\n        v if !v.is_empty() => v,\n        _ => vec![(crate::sso::FAKE_IDENTIFIER.to_string(), crate::sso::FAKE_IDENTIFIER.to_string())],\n    };\n\n    Ok(Json(json!({\n        \"object\": \"list\",\n        \"data\": identifiers.into_iter().map(|(name, identifier)| json!({\n            \"organizationName\": name,           // appear unused\n            \"organizationIdentifier\": identifier,\n            \"domainName\": CONFIG.domain(),      // appear unused\n        })).collect::<Vec<Value>>()\n    })))\n}\n\n#[derive(FromForm)]\nstruct GetOrgUserData {\n    #[field(name = \"includeCollections\")]\n    include_collections: Option<bool>,\n    #[field(name = \"includeGroups\")]\n    include_groups: Option<bool>,\n}\n\n#[get(\"/organizations/<org_id>/users?<data..>\")]\nasync fn get_members(\n    data: GetOrgUserData,\n    org_id: OrganizationId,\n    headers: ManagerHeadersLoose,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let mut users_json = Vec::new();\n    for u in Membership::find_by_org(&org_id, &conn).await {\n        users_json.push(\n            u.to_json_user_details(\n                data.include_collections.unwrap_or(false),\n                data.include_groups.unwrap_or(false),\n                &conn,\n            )\n            .await,\n        );\n    }\n\n    Ok(Json(json!({\n        \"data\": users_json,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\n#[post(\"/organizations/<org_id>/keys\", data = \"<data>\")]\nasync fn post_org_keys(\n    org_id: OrganizationId,\n    data: Json<OrgKeyData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: OrgKeyData = data.into_inner();\n\n    let mut org = match Organization::find_by_uuid(&org_id, &conn).await {\n        Some(organization) => {\n            if organization.private_key.is_some() && organization.public_key.is_some() {\n                err!(\"Organization Keys already exist\")\n            }\n            organization\n        }\n        None => err!(\"Can't find organization details\"),\n    };\n\n    org.private_key = Some(data.encrypted_private_key);\n    org.public_key = Some(data.public_key);\n\n    org.save(&conn).await?;\n\n    Ok(Json(json!({\n        \"object\": \"organizationKeys\",\n        \"publicKey\": org.public_key,\n        \"privateKey\": org.private_key,\n    })))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct InviteData {\n    emails: Vec<String>,\n    groups: Vec<GroupId>,\n    r#type: NumberOrString,\n    collections: Option<Vec<CollectionData>>,\n    #[serde(default)]\n    permissions: HashMap<String, Value>,\n}\n\n#[post(\"/organizations/<org_id>/users/invite\", data = \"<data>\")]\nasync fn send_invite(\n    org_id: OrganizationId,\n    data: Json<InviteData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: InviteData = data.into_inner();\n\n    // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission\n    // The from_str() will convert the custom role type into a manager role type\n    let raw_type = &data.r#type.into_string();\n    // Membership::from_str will convert custom (4) to manager (3)\n    let new_type = match MembershipType::from_str(raw_type) {\n        Some(new_type) => new_type as i32,\n        None => err!(\"Invalid type\"),\n    };\n\n    if new_type != MembershipType::User && headers.membership_type != MembershipType::Owner {\n        err!(\"Only Owners can invite Managers, Admins or Owners\")\n    }\n\n    // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag\n    // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes\n    // If the box is not checked, the user will still be a manager, but not with the access_all permission\n    let access_all = new_type >= MembershipType::Admin\n        || (raw_type.eq(\"4\")\n            && data.permissions.get(\"editAnyCollection\") == Some(&json!(true))\n            && data.permissions.get(\"deleteAnyCollection\") == Some(&json!(true))\n            && data.permissions.get(\"createNewCollections\") == Some(&json!(true)));\n\n    let mut user_created: bool = false;\n    for email in data.emails.iter() {\n        let mut member_status = MembershipStatus::Invited as i32;\n        let user = match User::find_by_mail(email, &conn).await {\n            None => {\n                if !CONFIG.invitations_allowed() {\n                    err!(format!(\"User does not exist: {email}\"))\n                }\n\n                if !CONFIG.is_email_domain_allowed(email) {\n                    err!(\"Email domain not eligible for invitations\")\n                }\n\n                if !CONFIG.mail_enabled() {\n                    Invitation::new(email).save(&conn).await?;\n                }\n\n                let mut new_user = User::new(email, None);\n                new_user.save(&conn).await?;\n                user_created = true;\n                new_user\n            }\n            Some(user) => {\n                if Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await.is_some() {\n                    err!(format!(\"User already in organization: {email}\"))\n                } else {\n                    // automatically accept existing users if mail is disabled\n                    if !CONFIG.mail_enabled() && !user.password_hash.is_empty() {\n                        member_status = MembershipStatus::Accepted as i32;\n                    }\n                    user\n                }\n            }\n        };\n\n        let mut new_member = Membership::new(user.uuid.clone(), org_id.clone(), Some(headers.user.email.clone()));\n        new_member.access_all = access_all;\n        new_member.atype = new_type;\n        new_member.status = member_status;\n        new_member.save(&conn).await?;\n\n        if CONFIG.mail_enabled() {\n            let org_name = match Organization::find_by_uuid(&org_id, &conn).await {\n                Some(org) => org.name,\n                None => err!(\"Error looking up organization\"),\n            };\n\n            if let Err(e) = mail::send_invite(\n                &user,\n                org_id.clone(),\n                new_member.uuid.clone(),\n                &org_name,\n                Some(headers.user.email.clone()),\n            )\n            .await\n            {\n                // Upon error delete the user, invite and org member records when needed\n                if user_created {\n                    user.delete(&conn).await?;\n                } else {\n                    new_member.delete(&conn).await?;\n                }\n\n                err!(format!(\"Error sending invite: {e:?} \"));\n            }\n        }\n\n        log_event(\n            EventType::OrganizationUserInvited as i32,\n            &new_member.uuid,\n            &org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n\n        // If no accessAll, add the collections received\n        if !access_all {\n            for col in data.collections.iter().flatten() {\n                match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await {\n                    None => err!(\"Collection not found in Organization\"),\n                    Some(collection) => {\n                        CollectionUser::save(\n                            &user.uuid,\n                            &collection.uuid,\n                            col.read_only,\n                            col.hide_passwords,\n                            col.manage,\n                            &conn,\n                        )\n                        .await?;\n                    }\n                }\n            }\n        }\n\n        for group_id in data.groups.iter() {\n            let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone());\n            group_entry.save(&conn).await?;\n        }\n    }\n\n    Ok(())\n}\n\n#[post(\"/organizations/<org_id>/users/reinvite\", data = \"<data>\")]\nasync fn bulk_reinvite_members(\n    org_id: OrganizationId,\n    data: Json<BulkMembershipIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: BulkMembershipIds = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    for member_id in data.ids {\n        let err_msg = match _reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await {\n            Ok(_) => String::new(),\n            Err(e) => format!(\"{e:?}\"),\n        };\n\n        bulk_response.push(json!(\n            {\n                \"object\": \"OrganizationBulkConfirmResponseModel\",\n                \"id\": member_id,\n                \"error\": err_msg\n            }\n        ))\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\n#[post(\"/organizations/<org_id>/users/<member_id>/reinvite\")]\nasync fn reinvite_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    _reinvite_member(&org_id, &member_id, &headers.user.email, &conn).await\n}\n\nasync fn _reinvite_member(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    invited_by_email: &str,\n    conn: &DbConn,\n) -> EmptyResult {\n    let Some(member) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {\n        err!(\"The user hasn't been invited to the organization.\")\n    };\n\n    if member.status != MembershipStatus::Invited as i32 {\n        err!(\"The user is already accepted or confirmed to the organization\")\n    }\n\n    let Some(user) = User::find_by_uuid(&member.user_uuid, conn).await else {\n        err!(\"User not found.\")\n    };\n\n    if !CONFIG.invitations_allowed() && user.password_hash.is_empty() {\n        err!(\"Invitations are not allowed.\")\n    }\n\n    let org_name = match Organization::find_by_uuid(org_id, conn).await {\n        Some(org) => org.name,\n        None => err!(\"Error looking up organization.\"),\n    };\n\n    if CONFIG.mail_enabled() {\n        mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?;\n    } else if user.password_hash.is_empty() {\n        let invitation = Invitation::new(&user.email);\n        invitation.save(conn).await?;\n    } else {\n        Invitation::take(&user.email, conn).await;\n        let mut member = member;\n        member.status = MembershipStatus::Accepted as i32;\n        member.save(conn).await?;\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct AcceptData {\n    token: String,\n    reset_password_key: Option<String>,\n}\n\n#[post(\"/organizations/<org_id>/users/<member_id>/accept\", data = \"<data>\")]\nasync fn accept_invite(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: Json<AcceptData>,\n    headers: Headers,\n    conn: DbConn,\n) -> EmptyResult {\n    // The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead\n    let data: AcceptData = data.into_inner();\n    let claims = decode_invite(&data.token)?;\n\n    // Don't allow other users from accepting an invitation.\n    if !claims.email.eq(&headers.user.email) {\n        err!(\"Invitation was issued to a different account\", \"Claim does not match user_id\")\n    }\n\n    // If a claim org_id does not match the one in from the URI, something is wrong.\n    if !claims.org_id.eq(&org_id) {\n        err!(\"Error accepting the invitation\", \"Claim does not match the org_id\")\n    }\n\n    // If a claim does not have a member_id or it does not match the one in from the URI, something is wrong.\n    if !claims.member_id.eq(&member_id) {\n        err!(\"Error accepting the invitation\", \"Claim does not match the member_id\")\n    }\n\n    let member_id = &claims.member_id;\n    Invitation::take(&claims.email, &conn).await;\n\n    // skip invitation logic when we were invited via the /admin panel\n    if **member_id != FAKE_ADMIN_UUID {\n        let Some(mut member) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else {\n            err!(\"Error accepting the invitation\")\n        };\n\n        let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&member.org_uuid, &conn).await {\n            true if data.reset_password_key.is_none() => err!(\"Reset password key is required, but not provided.\"),\n            true => data.reset_password_key,\n            false => None,\n        };\n\n        // In case the user was invited before the mail was saved in db.\n        member.invited_by_email = member.invited_by_email.or(claims.invited_by_email);\n\n        accept_org_invite(&headers.user, member, reset_password_key, &conn).await?;\n    } else if CONFIG.mail_enabled() {\n        // User was invited from /admin, so they are automatically confirmed\n        let org_name = CONFIG.invitation_org_name();\n        mail::send_invite_confirmed(&claims.email, &org_name).await?;\n    }\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ConfirmData {\n    id: Option<MembershipId>,\n    key: Option<String>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkConfirmData {\n    keys: Option<Vec<ConfirmData>>,\n}\n\n#[post(\"/organizations/<org_id>/users/confirm\", data = \"<data>\")]\nasync fn bulk_confirm_invite(\n    org_id: OrganizationId,\n    data: Json<BulkConfirmData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    match data.keys {\n        Some(keys) => {\n            for invite in keys {\n                let member_id = invite.id.unwrap();\n                let user_key = invite.key.unwrap_or_default();\n                let err_msg = match _confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await {\n                    Ok(_) => String::new(),\n                    Err(e) => format!(\"{e:?}\"),\n                };\n\n                bulk_response.push(json!(\n                    {\n                        \"object\": \"OrganizationBulkConfirmResponseModel\",\n                        \"id\": member_id,\n                        \"error\": err_msg\n                    }\n                ));\n            }\n        }\n        None => error!(\"No keys to confirm\"),\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\n#[post(\"/organizations/<org_id>/users/<member_id>/confirm\", data = \"<data>\")]\nasync fn confirm_invite(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: Json<ConfirmData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let data = data.into_inner();\n    let user_key = data.key.unwrap_or_default();\n    _confirm_invite(&org_id, &member_id, &user_key, &headers, &conn, &nt).await\n}\n\nasync fn _confirm_invite(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    key: &str,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if key.is_empty() || member_id.is_empty() {\n        err!(\"Key or UserId is not set, unable to process request\");\n    }\n\n    let Some(mut member_to_confirm) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {\n        err!(\"The specified user isn't a member of the organization\")\n    };\n\n    if member_to_confirm.atype != MembershipType::User && headers.membership_type != MembershipType::Owner {\n        err!(\"Only Owners can confirm Managers, Admins or Owners\")\n    }\n\n    if member_to_confirm.status != MembershipStatus::Accepted as i32 {\n        err!(\"User in invalid state\")\n    }\n\n    member_to_confirm.status = MembershipStatus::Confirmed as i32;\n    member_to_confirm.akey = key.to_string();\n\n    // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type\n    OrgPolicy::check_user_allowed(&member_to_confirm, \"confirm\", conn).await?;\n\n    log_event(\n        EventType::OrganizationUserConfirmed as i32,\n        &member_to_confirm.uuid,\n        org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        conn,\n    )\n    .await;\n\n    if CONFIG.mail_enabled() {\n        let org_name = match Organization::find_by_uuid(org_id, conn).await {\n            Some(org) => org.name,\n            None => err!(\"Error looking up organization.\"),\n        };\n        let address = match User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {\n            Some(user) => user.email,\n            None => err!(\"Error looking up user.\"),\n        };\n        mail::send_invite_confirmed(&address, &org_name).await?;\n    }\n\n    let save_result = member_to_confirm.save(conn).await;\n\n    if let Some(user) = User::find_by_uuid(&member_to_confirm.user_uuid, conn).await {\n        nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;\n    }\n\n    save_result\n}\n\n#[get(\"/organizations/<org_id>/users/mini-details\", rank = 1)]\nasync fn get_org_user_mini_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let mut members_json = Vec::new();\n    for m in Membership::find_by_org(&org_id, &conn).await {\n        members_json.push(m.to_json_mini_details(&conn).await);\n    }\n\n    Ok(Json(json!({\n        \"data\": members_json,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\n#[get(\"/organizations/<org_id>/users/<member_id>?<data..>\", rank = 2)]\nasync fn get_user(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: GetOrgUserData,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {\n        err!(\"The specified user isn't a member of the organization\")\n    };\n\n    // In this case, when groups are requested we also need to include collections.\n    // Else these will not be shown in the interface, and could lead to missing collections when saved.\n    let include_groups = data.include_groups.unwrap_or(false);\n    Ok(Json(user.to_json_user_details(data.include_collections.unwrap_or(include_groups), include_groups, &conn).await))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EditUserData {\n    r#type: NumberOrString,\n    collections: Option<Vec<CollectionData>>,\n    groups: Option<Vec<GroupId>>,\n    #[serde(default)]\n    permissions: HashMap<String, Value>,\n}\n\n#[put(\"/organizations/<org_id>/users/<member_id>\", data = \"<data>\", rank = 1)]\nasync fn put_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: Json<EditUserData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    edit_member(org_id, member_id, data, headers, conn).await\n}\n\n#[post(\"/organizations/<org_id>/users/<member_id>\", data = \"<data>\", rank = 1)]\nasync fn edit_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    data: Json<EditUserData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: EditUserData = data.into_inner();\n\n    // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission\n    // The from_str() will convert the custom role type into a manager role type\n    let raw_type = &data.r#type.into_string();\n    // MembershipType::from_str will convert custom (4) to manager (3)\n    let Some(new_type) = MembershipType::from_str(raw_type) else {\n        err!(\"Invalid type\")\n    };\n\n    // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag\n    // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes\n    // If the box is not checked, the user will still be a manager, but not with the access_all permission\n    let access_all = new_type >= MembershipType::Admin\n        || (raw_type.eq(\"4\")\n            && data.permissions.get(\"editAnyCollection\") == Some(&json!(true))\n            && data.permissions.get(\"deleteAnyCollection\") == Some(&json!(true))\n            && data.permissions.get(\"createNewCollections\") == Some(&json!(true)));\n\n    let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await {\n        Some(member) => member,\n        None => err!(\"The specified user isn't member of the organization\"),\n    };\n\n    if new_type != member_to_edit.atype\n        && (member_to_edit.atype >= MembershipType::Admin || new_type >= MembershipType::Admin)\n        && headers.membership_type != MembershipType::Owner\n    {\n        err!(\"Only Owners can grant and remove Admin or Owner privileges\")\n    }\n\n    if member_to_edit.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {\n        err!(\"Only Owners can edit Owner users\")\n    }\n\n    if member_to_edit.atype == MembershipType::Owner\n        && new_type != MembershipType::Owner\n        && member_to_edit.status == MembershipStatus::Confirmed as i32\n    {\n        // Removing owner permission, check that there is at least one other confirmed owner\n        if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 {\n            err!(\"Can't delete the last owner\")\n        }\n    }\n\n    member_to_edit.access_all = access_all;\n    member_to_edit.atype = new_type as i32;\n\n    // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type\n    // We need to perform the check after changing the type since `admin` is exempt.\n    OrgPolicy::check_user_allowed(&member_to_edit, \"modify\", &conn).await?;\n\n    // Delete all the odd collections\n    for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &member_to_edit.user_uuid, &conn).await {\n        c.delete(&conn).await?;\n    }\n\n    // If no accessAll, add the collections received\n    if !access_all {\n        for col in data.collections.iter().flatten() {\n            match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await {\n                None => err!(\"Collection not found in Organization\"),\n                Some(collection) => {\n                    CollectionUser::save(\n                        &member_to_edit.user_uuid,\n                        &collection.uuid,\n                        col.read_only,\n                        col.hide_passwords,\n                        col.manage,\n                        &conn,\n                    )\n                    .await?;\n                }\n            }\n        }\n    }\n\n    GroupUser::delete_all_by_member(&member_to_edit.uuid, &conn).await?;\n\n    for group_id in data.groups.iter().flatten() {\n        let mut group_entry = GroupUser::new(group_id.clone(), member_to_edit.uuid.clone());\n        group_entry.save(&conn).await?;\n    }\n\n    log_event(\n        EventType::OrganizationUserUpdated as i32,\n        &member_to_edit.uuid,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    member_to_edit.save(&conn).await\n}\n\n#[delete(\"/organizations/<org_id>/users\", data = \"<data>\")]\nasync fn bulk_delete_member(\n    org_id: OrganizationId,\n    data: Json<BulkMembershipIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: BulkMembershipIds = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    for member_id in data.ids {\n        let err_msg = match _delete_member(&org_id, &member_id, &headers, &conn, &nt).await {\n            Ok(_) => String::new(),\n            Err(e) => format!(\"{e:?}\"),\n        };\n\n        bulk_response.push(json!(\n            {\n                \"object\": \"OrganizationBulkConfirmResponseModel\",\n                \"id\": member_id,\n                \"error\": err_msg\n            }\n        ))\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\n#[delete(\"/organizations/<org_id>/users/<member_id>\")]\nasync fn delete_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    _delete_member(&org_id, &member_id, &headers, &conn, &nt).await\n}\n\nasync fn _delete_member(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {\n        err!(\"User to delete isn't member of the organization\")\n    };\n\n    if member_to_delete.atype != MembershipType::User && headers.membership_type != MembershipType::Owner {\n        err!(\"Only Owners can delete Admins or Owners\")\n    }\n\n    if member_to_delete.atype == MembershipType::Owner && member_to_delete.status == MembershipStatus::Confirmed as i32\n    {\n        // Removing owner, check that there is at least one other confirmed owner\n        if Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1 {\n            err!(\"Can't delete the last owner\")\n        }\n    }\n\n    log_event(\n        EventType::OrganizationUserRemoved as i32,\n        &member_to_delete.uuid,\n        org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        conn,\n    )\n    .await;\n\n    if let Some(user) = User::find_by_uuid(&member_to_delete.user_uuid, conn).await {\n        nt.send_user_update(UpdateType::SyncOrgKeys, &user, &headers.device.push_uuid, conn).await;\n    }\n\n    member_to_delete.delete(conn).await\n}\n\n#[post(\"/organizations/<org_id>/users/public-keys\", data = \"<data>\")]\nasync fn bulk_public_keys(\n    org_id: OrganizationId,\n    data: Json<BulkMembershipIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: BulkMembershipIds = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    // Check all received Membership UUID's and find the matching User to retrieve the public-key.\n    // If the user does not exists, just ignore it, and do not return any information regarding that Membership UUID.\n    // The web-vault will then ignore that user for the following steps.\n    for member_id in data.ids {\n        match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await {\n            Some(member) => match User::find_by_uuid(&member.user_uuid, &conn).await {\n                Some(user) => bulk_response.push(json!(\n                    {\n                        \"object\": \"organizationUserPublicKeyResponseModel\",\n                        \"id\": member_id,\n                        \"userId\": user.uuid,\n                        \"key\": user.public_key\n                    }\n                )),\n                None => debug!(\"User doesn't exist\"),\n            },\n            None => debug!(\"Membership doesn't exist\"),\n        }\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\nuse super::ciphers::update_cipher_from_data;\nuse super::ciphers::CipherData;\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct ImportData {\n    ciphers: Vec<CipherData>,\n    collections: Vec<FullCollectionData>,\n    collection_relationships: Vec<RelationsData>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RelationsData {\n    // Cipher index\n    key: usize,\n    // Collection index\n    value: usize,\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62\n#[post(\"/ciphers/import-organization?<query..>\", data = \"<data>\")]\nasync fn post_org_import(\n    query: OrgIdData,\n    data: Json<ImportData>,\n    headers: OrgMemberHeaders,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    let org_id = query.organization_id;\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: ImportData = data.into_inner();\n\n    // Validate the import before continuing\n    // Bitwarden does not process the import if there is one item invalid.\n    // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.\n    // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.\n    Cipher::validate_cipher_data(&data.ciphers)?;\n\n    let existing_collections: HashSet<Option<CollectionId>> =\n        Collection::find_by_organization(&org_id, &conn).await.into_iter().map(|c| Some(c.uuid)).collect();\n    let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());\n    for col in data.collections {\n        let collection_uuid = if existing_collections.contains(&col.id) {\n            let col_id = col.id.unwrap();\n            // When not an Owner or Admin, check if the member is allowed to access the collection.\n            if headers.membership.atype < MembershipType::Admin\n                && !Collection::can_access_collection(&headers.membership, &col_id, &conn).await\n            {\n                err!(Compact, \"The current user isn't allowed to manage this collection\")\n            }\n            col_id\n        } else {\n            // We do not allow users or managers which can not manage all collections to create new collections\n            // If there is any collection other than an existing import collection, abort the import.\n            if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() {\n                err!(Compact, \"The current user isn't allowed to create new collections\")\n            }\n            let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);\n            new_collection.save(&conn).await?;\n            new_collection.uuid\n        };\n\n        collections.push(collection_uuid);\n    }\n\n    // Read the relations between collections and ciphers\n    // Ciphers can be in multiple collections at the same time\n    let mut relations = Vec::with_capacity(data.collection_relationships.len());\n    for relation in data.collection_relationships {\n        relations.push((relation.key, relation.value));\n    }\n\n    let headers: Headers = headers.into();\n\n    let mut ciphers: Vec<CipherId> = Vec::with_capacity(data.ciphers.len());\n    for mut cipher_data in data.ciphers {\n        // Always clear folder_id's via an organization import\n        cipher_data.folder_id = None;\n        let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());\n        update_cipher_from_data(\n            &mut cipher,\n            cipher_data,\n            &headers,\n            Some(collections.clone()),\n            &conn,\n            &nt,\n            UpdateType::None,\n        )\n        .await\n        .ok();\n        ciphers.push(cipher.uuid);\n    }\n\n    // Assign the collections\n    for (cipher_index, col_index) in relations {\n        let cipher_id = &ciphers[cipher_index];\n        let col_id = &collections[col_index];\n        CollectionCipher::save(cipher_id, col_id, &conn).await?;\n    }\n\n    let mut user = headers.user;\n    user.update_revision(&conn).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\n#[allow(dead_code)]\nstruct BulkCollectionsData {\n    organization_id: OrganizationId,\n    cipher_ids: Vec<CipherId>,\n    collection_ids: HashSet<CollectionId>,\n    remove_collections: bool,\n}\n\n// This endpoint is only reachable via the organization view, therefore this endpoint is located here\n// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates\n#[post(\"/ciphers/bulk-collections\", data = \"<data>\")]\nasync fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    let data: BulkCollectionsData = data.into_inner();\n\n    // Get all the collection available to the user in one query\n    // Also filter based upon the provided collections\n    let user_collections: HashMap<CollectionId, Collection> =\n        Collection::find_by_organization_and_user_uuid(&data.organization_id, &headers.user.uuid, &conn)\n            .await\n            .into_iter()\n            .filter_map(|c| {\n                if data.collection_ids.contains(&c.uuid) {\n                    Some((c.uuid.clone(), c))\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n    // Verify if all the collections requested exists and are writeable for the user, else abort\n    for collection_uuid in &data.collection_ids {\n        match user_collections.get(collection_uuid) {\n            Some(collection) if collection.is_writable_by_user(&headers.user.uuid, &conn).await => (),\n            _ => err_code!(\"Resource not found\", \"User does not have access to a collection\", 404),\n        }\n    }\n\n    for cipher_id in data.cipher_ids.iter() {\n        // Only act on existing cipher uuid's\n        // Do not abort the operation just ignore it, it could be a cipher was just deleted for example\n        if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await {\n            if cipher.is_write_accessible_to_user(&headers.user.uuid, &conn).await {\n                // When selecting a specific collection from the left filter list, and use the bulk option, you can remove an item from that collection\n                // In these cases the client will call this endpoint twice, once for adding the new collections and a second for deleting.\n                if data.remove_collections {\n                    for collection in &data.collection_ids {\n                        CollectionCipher::delete(&cipher.uuid, collection, &conn).await?;\n                    }\n                } else {\n                    for collection in &data.collection_ids {\n                        CollectionCipher::save(&cipher.uuid, collection, &conn).await?;\n                    }\n                }\n            }\n        };\n    }\n\n    Ok(())\n}\n\n#[get(\"/organizations/<org_id>/policies\")]\nasync fn list_policies(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let policies = OrgPolicy::find_by_org(&org_id, &conn).await;\n    let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();\n\n    Ok(Json(json!({\n        \"data\": policies_json,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\n#[get(\"/organizations/<org_id>/policies/token?<token>\")]\nasync fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn) -> JsonResult {\n    let invite = decode_invite(token)?;\n\n    if invite.org_id != org_id {\n        err!(\"Token doesn't match request organization\");\n    }\n\n    // exit early when we have been invited via /admin panel\n    if org_id.as_ref() == FAKE_ADMIN_UUID {\n        return Ok(Json(json!({})));\n    }\n\n    // TODO: We receive the invite token as ?token=<>, validate it contains the org id\n    let policies = OrgPolicy::find_by_org(&org_id, &conn).await;\n    let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();\n\n    Ok(Json(json!({\n        \"data\": policies_json,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\n// Called during the SSO enrollment.\n// Return the org policy if it exists, otherwise use the default one.\n#[get(\"/organizations/<org_id>/policies/master-password\", rank = 1)]\nasync fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, conn: DbConn) -> JsonResult {\n    let policy =\n        OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| {\n            let (enabled, data) = match CONFIG.sso_master_password_policy_value() {\n                Some(policy) if CONFIG.sso_enabled() => (true, policy.to_string()),\n                _ => (false, \"null\".to_string()),\n            };\n\n            OrgPolicy::new(org_id, OrgPolicyType::MasterPassword, enabled, data)\n        });\n\n    Ok(Json(policy.to_json()))\n}\n\n#[get(\"/organizations/<org_id>/policies/<pol_type>\", rank = 2)]\nasync fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {\n        err!(\"Invalid or unsupported policy type\")\n    };\n\n    let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {\n        Some(p) => p,\n        None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, \"null\".to_string()),\n    };\n\n    Ok(Json(policy.to_json()))\n}\n\n#[derive(Deserialize)]\nstruct PolicyData {\n    enabled: bool,\n    data: Option<Value>,\n}\n\n#[put(\"/organizations/<org_id>/policies/<pol_type>\", data = \"<data>\")]\nasync fn put_policy(\n    org_id: OrganizationId,\n    pol_type: i32,\n    data: Json<PolicyData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: PolicyData = data.into_inner();\n\n    let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {\n        err!(\"Invalid or unsupported policy type\")\n    };\n\n    // Bitwarden only allows the Reset Password policy when Single Org policy is enabled\n    // Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past\n    // Now that groups are available we can enforce this option when wanted.\n    // We put this behind a config option to prevent breaking current installation.\n    // Maybe we want to enable this by default in the future, but currently it is disabled by default.\n    if CONFIG.enforce_single_org_with_reset_pw_policy() {\n        if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled {\n            let single_org_policy_enabled =\n                match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::SingleOrg, &conn).await {\n                    Some(p) => p.enabled,\n                    None => false,\n                };\n\n            if !single_org_policy_enabled {\n                err!(\"Single Organization policy is not enabled. It is mandatory for this policy to be enabled.\")\n            }\n        }\n\n        // Also prevent the Single Org Policy to be disabled if the Reset Password policy is enabled\n        if pol_type_enum == OrgPolicyType::SingleOrg && !data.enabled {\n            let reset_pw_policy_enabled =\n                match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::ResetPassword, &conn).await {\n                    Some(p) => p.enabled,\n                    None => false,\n                };\n\n            if reset_pw_policy_enabled {\n                err!(\"Account recovery policy is enabled. It is not allowed to disable this policy.\")\n            }\n        }\n    }\n\n    // When enabling the TwoFactorAuthentication policy, revoke all members that do not have 2FA\n    if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled {\n        two_factor::enforce_2fa_policy_for_org(\n            &org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await?;\n    }\n\n    // When enabling the SingleOrg policy, remove this org's members that are members of other orgs\n    if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled {\n        for mut member in Membership::find_by_org(&org_id, &conn).await.into_iter() {\n            // Policy only applies to non-Owner/non-Admin members who have accepted joining the org\n            // Exclude invited and revoked users when checking for this policy.\n            // Those users will not be allowed to accept or be activated because of the policy checks done there.\n            if member.atype < MembershipType::Admin\n                && member.status != MembershipStatus::Invited as i32\n                && Membership::count_accepted_and_confirmed_by_user(&member.user_uuid, &member.org_uuid, &conn).await\n                    > 0\n            {\n                if CONFIG.mail_enabled() {\n                    let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap();\n                    let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap();\n\n                    mail::send_single_org_removed_from_org(&user.email, &org.name).await?;\n                }\n\n                log_event(\n                    EventType::OrganizationUserRemoved as i32,\n                    &member.uuid,\n                    &org_id,\n                    &headers.user.uuid,\n                    headers.device.atype,\n                    &headers.ip.ip,\n                    &conn,\n                )\n                .await;\n\n                member.revoke();\n                member.save(&conn).await?;\n            }\n        }\n    }\n\n    let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {\n        Some(p) => p,\n        None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, \"{}\".to_string()),\n    };\n\n    policy.enabled = data.enabled;\n    policy.data = serde_json::to_string(&data.data)?;\n    policy.save(&conn).await?;\n\n    log_event(\n        EventType::PolicyUpdated as i32,\n        policy.uuid.as_ref(),\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(policy.to_json()))\n}\n\n#[derive(Deserialize)]\nstruct PolicyDataVnext {\n    policy: PolicyData,\n    // Ignore metadata for now as we do not yet support this\n    // \"metadata\": {\n    //     \"defaultUserCollectionName\": \"2.xx|xx==|xx=\"\n    // }\n}\n\n#[put(\"/organizations/<org_id>/policies/<pol_type>/vnext\", data = \"<data>\")]\nasync fn put_policy_vnext(\n    org_id: OrganizationId,\n    pol_type: i32,\n    data: Json<PolicyDataVnext>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    let data: PolicyDataVnext = data.into_inner();\n    let policy: PolicyData = data.policy;\n    put_policy(org_id, pol_type, Json(policy), headers, conn).await\n}\n\n#[get(\"/plans\")]\nfn get_plans() -> Json<Value> {\n    // Respond with a minimal json just enough to allow the creation of an new organization.\n    Json(json!({\n        \"object\": \"list\",\n        \"data\": [{\n            \"object\": \"plan\",\n            \"type\": 0,\n            \"product\": 0,\n            \"name\": \"Free\",\n            \"nameLocalizationKey\": \"planNameFree\",\n            \"bitwardenProduct\": 0,\n            \"maxUsers\": 0,\n            \"descriptionLocalizationKey\": \"planDescFree\"\n        },{\n            \"object\": \"plan\",\n            \"type\": 0,\n            \"product\": 1,\n            \"name\": \"Free\",\n            \"nameLocalizationKey\": \"planNameFree\",\n            \"bitwardenProduct\": 1,\n            \"maxUsers\": 0,\n            \"descriptionLocalizationKey\": \"planDescFree\"\n        }],\n        \"continuationToken\": null\n    }))\n}\n\n#[get(\"/organizations/<_org_id>/billing/metadata\")]\nfn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json<Value> {\n    // Prevent a 404 error, which also causes Javascript errors.\n    Json(_empty_data_json())\n}\n\n#[get(\"/organizations/<_org_id>/billing/vnext/warnings\")]\nfn get_billing_warnings(_org_id: OrganizationId, _headers: Headers) -> Json<Value> {\n    Json(json!({\n        \"freeTrial\":null,\n        \"inactiveSubscription\":null,\n        \"resellerRenewal\":null,\n        \"taxId\":null,\n    }))\n}\n\nfn _empty_data_json() -> Value {\n    json!({\n        \"object\": \"list\",\n        \"data\": [],\n        \"continuationToken\": null\n    })\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct BulkRevokeMembershipIds {\n    ids: Option<Vec<MembershipId>>,\n}\n\n#[put(\"/organizations/<org_id>/users/<member_id>/revoke\")]\nasync fn revoke_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    _revoke_member(&org_id, &member_id, &headers, &conn).await\n}\n\n#[put(\"/organizations/<org_id>/users/revoke\", data = \"<data>\")]\nasync fn bulk_revoke_members(\n    org_id: OrganizationId,\n    data: Json<BulkRevokeMembershipIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    match data.ids {\n        Some(members) => {\n            for member_id in members {\n                let err_msg = match _revoke_member(&org_id, &member_id, &headers, &conn).await {\n                    Ok(_) => String::new(),\n                    Err(e) => format!(\"{e:?}\"),\n                };\n\n                bulk_response.push(json!(\n                    {\n                        \"object\": \"OrganizationUserBulkResponseModel\",\n                        \"id\": member_id,\n                        \"error\": err_msg\n                    }\n                ));\n            }\n        }\n        None => error!(\"No users to revoke\"),\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\nasync fn _revoke_member(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {\n        Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {\n            if member.user_uuid == headers.user.uuid {\n                err!(\"You cannot revoke yourself\")\n            }\n            if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {\n                err!(\"Only owners can revoke other owners\")\n            }\n            if member.atype == MembershipType::Owner\n                && Membership::count_confirmed_by_org_and_type(org_id, MembershipType::Owner, conn).await <= 1\n            {\n                err!(\"Organization must have at least one confirmed owner\")\n            }\n\n            member.revoke();\n            member.save(conn).await?;\n\n            log_event(\n                EventType::OrganizationUserRevoked as i32,\n                &member.uuid,\n                org_id,\n                &headers.user.uuid,\n                headers.device.atype,\n                &headers.ip.ip,\n                conn,\n            )\n            .await;\n        }\n        Some(_) => err!(\"User is already revoked\"),\n        None => err!(\"User not found in organization\"),\n    }\n    Ok(())\n}\n\n#[put(\"/organizations/<org_id>/users/<member_id>/restore\")]\nasync fn restore_member(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    _restore_member(&org_id, &member_id, &headers, &conn).await\n}\n\n#[put(\"/organizations/<org_id>/users/restore\", data = \"<data>\")]\nasync fn bulk_restore_members(\n    org_id: OrganizationId,\n    data: Json<BulkMembershipIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data = data.into_inner();\n\n    let mut bulk_response = Vec::new();\n    for member_id in data.ids {\n        let err_msg = match _restore_member(&org_id, &member_id, &headers, &conn).await {\n            Ok(_) => String::new(),\n            Err(e) => format!(\"{e:?}\"),\n        };\n\n        bulk_response.push(json!(\n            {\n                \"object\": \"OrganizationUserBulkResponseModel\",\n                \"id\": member_id,\n                \"error\": err_msg\n            }\n        ));\n    }\n\n    Ok(Json(json!({\n        \"data\": bulk_response,\n        \"object\": \"list\",\n        \"continuationToken\": null\n    })))\n}\n\nasync fn _restore_member(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {\n        Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {\n            if member.user_uuid == headers.user.uuid {\n                err!(\"You cannot restore yourself\")\n            }\n            if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner {\n                err!(\"Only owners can restore other owners\")\n            }\n\n            member.restore();\n            // This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type\n            // This check need to be done after restoring to work with the correct status\n            OrgPolicy::check_user_allowed(&member, \"restore\", conn).await?;\n            member.save(conn).await?;\n\n            log_event(\n                EventType::OrganizationUserRestored as i32,\n                &member.uuid,\n                org_id,\n                &headers.user.uuid,\n                headers.device.atype,\n                &headers.ip.ip,\n                conn,\n            )\n            .await;\n        }\n        Some(_) => err!(\"User is already active\"),\n        None => err!(\"User not found in organization\"),\n    }\n    Ok(())\n}\n\nasync fn get_groups_data(\n    details: bool,\n    org_id: OrganizationId,\n    headers: ManagerHeadersLoose,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let groups: Vec<Value> = if CONFIG.org_groups_enabled() {\n        let groups = Group::find_by_organization(&org_id, &conn).await;\n        let mut groups_json = Vec::with_capacity(groups.len());\n\n        if details {\n            for g in groups {\n                groups_json.push(g.to_json_details(&conn).await)\n            }\n        } else {\n            for g in groups {\n                groups_json.push(g.to_json())\n            }\n        }\n        groups_json\n    } else {\n        // The Bitwarden clients seem to call this API regardless of whether groups are enabled,\n        // so just act as if there are no groups.\n        Vec::with_capacity(0)\n    };\n\n    Ok(Json(json!({\n        \"data\": groups,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    })))\n}\n\n#[get(\"/organizations/<org_id>/groups\")]\nasync fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    get_groups_data(false, org_id, headers, conn).await\n}\n\n#[get(\"/organizations/<org_id>/groups/details\", rank = 1)]\nasync fn get_groups_details(org_id: OrganizationId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {\n    get_groups_data(true, org_id, headers, conn).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct GroupRequest {\n    name: String,\n    #[serde(default)]\n    access_all: bool,\n    external_id: Option<String>,\n    collections: Vec<CollectionData>,\n    users: Vec<MembershipId>,\n}\n\nimpl GroupRequest {\n    pub fn to_group(&self, org_uuid: &OrganizationId) -> Group {\n        Group::new(org_uuid.clone(), self.name.clone(), self.access_all, self.external_id.clone())\n    }\n\n    pub fn update_group(&self, mut group: Group) -> Group {\n        group.name.clone_from(&self.name);\n        group.access_all = self.access_all;\n        // Group Updates do not support changing the external_id\n        // These input fields are in a disabled state, and can only be updated/added via ldap_import\n\n        group\n    }\n}\n\n#[derive(Deserialize, Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct CollectionData {\n    id: CollectionId,\n    read_only: bool,\n    hide_passwords: bool,\n    manage: bool,\n}\n\nimpl CollectionData {\n    pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {\n        CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)\n    }\n}\n\n#[post(\"/organizations/<org_id>/groups/<group_id>\", data = \"<data>\")]\nasync fn post_group(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    data: Json<GroupRequest>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    put_group(org_id, group_id, data, headers, conn).await\n}\n\n#[post(\"/organizations/<org_id>/groups\", data = \"<data>\")]\nasync fn post_groups(\n    org_id: OrganizationId,\n    headers: AdminHeaders,\n    data: Json<GroupRequest>,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let group_request = data.into_inner();\n    let group = group_request.to_group(&org_id);\n\n    log_event(\n        EventType::GroupCreated as i32,\n        &group.uuid,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    add_update_group(group, group_request.collections, group_request.users, org_id, &headers, &conn).await\n}\n\n#[put(\"/organizations/<org_id>/groups/<group_id>\", data = \"<data>\")]\nasync fn put_group(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    data: Json<GroupRequest>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {\n        err!(\"Group not found\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    let group_request = data.into_inner();\n    let updated_group = group_request.update_group(group);\n\n    CollectionGroup::delete_all_by_group(&group_id, &conn).await?;\n    GroupUser::delete_all_by_group(&group_id, &conn).await?;\n\n    log_event(\n        EventType::GroupUpdated as i32,\n        &updated_group.uuid,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    add_update_group(updated_group, group_request.collections, group_request.users, org_id, &headers, &conn).await\n}\n\nasync fn add_update_group(\n    mut group: Group,\n    collections: Vec<CollectionData>,\n    members: Vec<MembershipId>,\n    org_id: OrganizationId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n) -> JsonResult {\n    group.save(conn).await?;\n\n    for col_selection in collections {\n        let mut collection_group = col_selection.to_collection_group(group.uuid.clone());\n        collection_group.save(conn).await?;\n    }\n\n    for assigned_member in members {\n        let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_member.clone());\n        user_entry.save(conn).await?;\n\n        log_event(\n            EventType::OrganizationUserUpdatedGroups as i32,\n            &assigned_member,\n            &org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            conn,\n        )\n        .await;\n    }\n\n    Ok(Json(json!({\n        \"id\": group.uuid,\n        \"organizationId\": group.organizations_uuid,\n        \"name\": group.name,\n        \"accessAll\": group.access_all,\n        \"externalId\": group.external_id,\n        \"object\": \"group\"\n    })))\n}\n\n#[get(\"/organizations/<org_id>/groups/<group_id>/details\")]\nasync fn get_group_details(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {\n        err!(\"Group not found\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    Ok(Json(group.to_json_details(&conn).await))\n}\n\n#[post(\"/organizations/<org_id>/groups/<group_id>/delete\")]\nasync fn post_delete_group(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    _delete_group(&org_id, &group_id, &headers, &conn).await\n}\n\n#[delete(\"/organizations/<org_id>/groups/<group_id>\")]\nasync fn delete_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> EmptyResult {\n    _delete_group(&org_id, &group_id, &headers, &conn).await\n}\n\nasync fn _delete_group(\n    org_id: &OrganizationId,\n    group_id: &GroupId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n) -> EmptyResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let Some(group) = Group::find_by_uuid_and_org(group_id, org_id, conn).await else {\n        err!(\"Group not found\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    log_event(\n        EventType::GroupDeleted as i32,\n        &group.uuid,\n        org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        conn,\n    )\n    .await;\n\n    group.delete(conn).await\n}\n\n#[delete(\"/organizations/<org_id>/groups\", data = \"<data>\")]\nasync fn bulk_delete_groups(\n    org_id: OrganizationId,\n    data: Json<BulkGroupIds>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let data: BulkGroupIds = data.into_inner();\n\n    for group_id in data.ids {\n        _delete_group(&org_id, &group_id, &headers, &conn).await?\n    }\n    Ok(())\n}\n\n#[get(\"/organizations/<org_id>/groups/<group_id>\", rank = 2)]\nasync fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    let Some(group) = Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await else {\n        err!(\"Group not found\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    Ok(Json(group.to_json()))\n}\n\n#[get(\"/organizations/<org_id>/groups/<group_id>/users\")]\nasync fn get_group_members(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {\n        err!(\"Group could not be found!\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    let group_members: Vec<MembershipId> = GroupUser::find_by_group(&group_id, &conn)\n        .await\n        .iter()\n        .map(|entry| entry.users_organizations_uuid.clone())\n        .collect();\n\n    Ok(Json(json!(group_members)))\n}\n\n#[put(\"/organizations/<org_id>/groups/<group_id>/users\", data = \"<data>\")]\nasync fn put_group_members(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    headers: AdminHeaders,\n    data: Json<Vec<MembershipId>>,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {\n        err!(\"Group could not be found!\", \"Group uuid is invalid or does not belong to the organization\")\n    };\n\n    GroupUser::delete_all_by_group(&group_id, &conn).await?;\n\n    let assigned_members = data.into_inner();\n    for assigned_member in assigned_members {\n        let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone());\n        user_entry.save(&conn).await?;\n\n        log_event(\n            EventType::OrganizationUserUpdatedGroups as i32,\n            &assigned_member,\n            &org_id,\n            &headers.user.uuid,\n            headers.device.atype,\n            &headers.ip.ip,\n            &conn,\n        )\n        .await;\n    }\n\n    Ok(())\n}\n\n#[post(\"/organizations/<org_id>/groups/<group_id>/delete-user/<member_id>\")]\nasync fn post_delete_group_member(\n    org_id: OrganizationId,\n    group_id: GroupId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    if !CONFIG.org_groups_enabled() {\n        err!(\"Group support is disabled\");\n    }\n\n    if Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await.is_none() {\n        err!(\"User could not be found or does not belong to the organization.\");\n    }\n\n    if Group::find_by_uuid_and_org(&group_id, &org_id, &conn).await.is_none() {\n        err!(\"Group could not be found or does not belong to the organization.\");\n    }\n\n    log_event(\n        EventType::OrganizationUserUpdatedGroups as i32,\n        &member_id,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    GroupUser::delete_by_group_and_member(&group_id, &member_id, &conn).await\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrganizationUserResetPasswordEnrollmentRequest {\n    reset_password_key: Option<String>,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrganizationUserResetPasswordRequest {\n    new_master_password_hash: String,\n    key: String,\n}\n\n// Upstream reports this is the renamed endpoint instead of `/keys`\n// But the clients do not seem to use this at all\n// Just add it here in case they will\n#[get(\"/organizations/<org_id>/public-key\")]\nasync fn get_organization_public_key(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.membership.org_uuid {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {\n        err!(\"Organization not found\")\n    };\n\n    Ok(Json(json!({\n        \"object\": \"organizationPublicKey\",\n        \"publicKey\": org.public_key,\n    })))\n}\n\n// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L487-L492\n#[get(\"/organizations/<org_id>/keys\")]\nasync fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {\n    get_organization_public_key(org_id, headers, conn).await\n}\n\n#[put(\"/organizations/<org_id>/users/<member_id>/reset-password\", data = \"<data>\")]\nasync fn put_reset_password(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    data: Json<OrganizationUserResetPasswordRequest>,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {\n        err!(\"Required organization not found\")\n    };\n\n    let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org.uuid, &conn).await else {\n        err!(\"User to reset isn't member of required organization\")\n    };\n\n    let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else {\n        err!(\"User not found\")\n    };\n\n    check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &conn).await?;\n\n    if member.reset_password_key.is_none() {\n        err!(\"Password reset not or not correctly enrolled\");\n    }\n    if member.status != (MembershipStatus::Confirmed as i32) {\n        err!(\"Organization user must be confirmed for password reset functionality\");\n    }\n\n    // Sending email before resetting password to ensure working email configuration and the resulting\n    // user notification. Also this might add some protection against security flaws and misuse\n    if let Err(e) = mail::send_admin_reset_password(&user.email, user.display_name(), &org.name).await {\n        err!(format!(\"Error sending user reset password email: {e:#?}\"));\n    }\n\n    let reset_request = data.into_inner();\n\n    let mut user = user;\n    user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None);\n    user.save(&conn).await?;\n\n    nt.send_logout(&user, None, &conn).await;\n\n    log_event(\n        EventType::OrganizationUserAdminResetPassword as i32,\n        &member_id,\n        &org_id,\n        &headers.user.uuid,\n        headers.device.atype,\n        &headers.ip.ip,\n        &conn,\n    )\n    .await;\n\n    Ok(())\n}\n\n#[get(\"/organizations/<org_id>/users/<member_id>/reset-password-details\")]\nasync fn get_reset_password_details(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else {\n        err!(\"Required organization not found\")\n    };\n\n    let Some(member) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {\n        err!(\"User to reset isn't member of required organization\")\n    };\n\n    let Some(user) = User::find_by_uuid(&member.user_uuid, &conn).await else {\n        err!(\"User not found\")\n    };\n\n    check_reset_password_applicable_and_permissions(&org_id, &member_id, &headers, &conn).await?;\n\n    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs#L190\n    Ok(Json(json!({\n        \"object\": \"organizationUserResetPasswordDetails\",\n        \"organizationUserId\": member_id,\n        \"kdf\": user.client_kdf_type,\n        \"kdfIterations\": user.client_kdf_iter,\n        \"kdfMemory\": user.client_kdf_memory,\n        \"kdfParallelism\": user.client_kdf_parallelism,\n        \"resetPasswordKey\": member.reset_password_key,\n        \"encryptedPrivateKey\": org.private_key,\n    })))\n}\n\nasync fn check_reset_password_applicable_and_permissions(\n    org_id: &OrganizationId,\n    member_id: &MembershipId,\n    headers: &AdminHeaders,\n    conn: &DbConn,\n) -> EmptyResult {\n    check_reset_password_applicable(org_id, conn).await?;\n\n    let Some(target_user) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {\n        err!(\"Reset target user not found\")\n    };\n\n    // Resetting user must be higher/equal to user to reset\n    match headers.membership_type {\n        MembershipType::Owner => Ok(()),\n        MembershipType::Admin if target_user.atype <= MembershipType::Admin => Ok(()),\n        _ => err!(\"No permission to reset this user's password\"),\n    }\n}\n\nasync fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {\n    if !CONFIG.mail_enabled() {\n        err!(\"Password reset is not supported on an email-disabled instance.\");\n    }\n\n    let Some(policy) = OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await else {\n        err!(\"Policy not found\")\n    };\n\n    if !policy.enabled {\n        err!(\"Reset password policy not enabled\");\n    }\n\n    Ok(())\n}\n\n#[put(\"/organizations/<org_id>/users/<member_id>/reset-password-enrollment\", data = \"<data>\")]\nasync fn put_reset_password_enrollment(\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    headers: Headers,\n    data: Json<OrganizationUserResetPasswordEnrollmentRequest>,\n    conn: DbConn,\n) -> EmptyResult {\n    let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else {\n        err!(\"User to enroll isn't member of required organization\")\n    };\n\n    check_reset_password_applicable(&org_id, &conn).await?;\n\n    let reset_request = data.into_inner();\n\n    let reset_password_key = match reset_request.reset_password_key {\n        None => None,\n        Some(ref key) if key.is_empty() => None,\n        Some(key) => Some(key),\n    };\n\n    if reset_password_key.is_none() && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &conn).await {\n        err!(\"Reset password can't be withdrawn due to an enterprise policy\");\n    }\n\n    if reset_password_key.is_some() {\n        PasswordOrOtpData {\n            master_password_hash: reset_request.master_password_hash,\n            otp: reset_request.otp,\n        }\n        .validate(&headers.user, true, &conn)\n        .await?;\n    }\n\n    member.reset_password_key = reset_password_key;\n    member.save(&conn).await?;\n\n    let log_id = if member.reset_password_key.is_some() {\n        EventType::OrganizationUserResetPasswordEnroll as i32\n    } else {\n        EventType::OrganizationUserResetPasswordWithdraw as i32\n    };\n\n    log_event(log_id, &member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    Ok(())\n}\n\n// NOTE: It seems clients can't handle uppercase-first keys!!\n//       We need to convert all keys so they have the first character to be a lowercase.\n//       Else the export will be just an empty JSON file.\n// We currently only support exports by members of the Admin or Owner status.\n// Vaultwarden does not yet support exporting only managed collections!\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52\n#[get(\"/organizations/<org_id>/export\")]\nasync fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult {\n    if org_id != headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n\n    Ok(Json(json!({\n        \"collections\": convert_json_key_lcase_first(_get_org_collections(&org_id, &conn).await),\n        \"ciphers\": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &conn).await?),\n    })))\n}\n\nasync fn _api_key(\n    org_id: &OrganizationId,\n    data: Json<PasswordOrOtpData>,\n    rotate: bool,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    if org_id != &headers.org_id {\n        err!(\"Organization not found\", \"Organization id's do not match\");\n    }\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    // Validate the admin users password/otp\n    data.validate(&user, true, &conn).await?;\n\n    let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {\n        Some(mut org_api_key) => {\n            if rotate {\n                org_api_key.api_key = crate::crypto::generate_api_key();\n                org_api_key.revision_date = chrono::Utc::now().naive_utc();\n                org_api_key.save(&conn).await.expect(\"Error rotating organization API Key\");\n            }\n            org_api_key\n        }\n        None => {\n            let api_key = crate::crypto::generate_api_key();\n            let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key);\n            new_org_api_key.save(&conn).await.expect(\"Error creating organization API Key\");\n            new_org_api_key\n        }\n    };\n\n    Ok(Json(json!({\n      \"apiKey\": org_api_key.api_key,\n      \"revisionDate\": crate::util::format_date(&org_api_key.revision_date),\n      \"object\": \"apiKey\",\n    })))\n}\n\n#[post(\"/organizations/<org_id>/api-key\", data = \"<data>\")]\nasync fn api_key(\n    org_id: OrganizationId,\n    data: Json<PasswordOrOtpData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    _api_key(&org_id, data, false, headers, conn).await\n}\n\n#[post(\"/organizations/<org_id>/rotate-api-key\", data = \"<data>\")]\nasync fn rotate_api_key(\n    org_id: OrganizationId,\n    data: Json<PasswordOrOtpData>,\n    headers: AdminHeaders,\n    conn: DbConn,\n) -> JsonResult {\n    _api_key(&org_id, data, true, headers, conn).await\n}\n"
  },
  {
    "path": "src/api/core/public.rs",
    "content": "use chrono::Utc;\nuse rocket::{\n    request::{FromRequest, Outcome},\n    serde::json::Json,\n    Request, Route,\n};\n\nuse std::collections::HashSet;\n\nuse crate::{\n    api::EmptyResult,\n    auth,\n    db::{\n        models::{\n            Group, GroupUser, Invitation, Membership, MembershipStatus, MembershipType, Organization,\n            OrganizationApiKey, OrganizationId, User,\n        },\n        DbConn,\n    },\n    mail, CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![ldap_import]\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgImportGroupData {\n    name: String,\n    external_id: String,\n    member_external_ids: Vec<String>,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgImportUserData {\n    email: String,\n    external_id: String,\n    deleted: bool,\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct OrgImportData {\n    groups: Vec<OrgImportGroupData>,\n    members: Vec<OrgImportUserData>,\n    overwrite_existing: bool,\n    // largeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set.\n}\n\n#[post(\"/public/organization/import\", data = \"<data>\")]\nasync fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn) -> EmptyResult {\n    // Most of the logic for this function can be found here\n    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L1203\n\n    let org_id = token.0;\n    let data = data.into_inner();\n\n    for user_data in &data.members {\n        let mut user_created: bool = false;\n        if user_data.deleted {\n            // If user is marked for deletion and it exists, revoke it\n            if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await {\n                // Only revoke a user if it is not the last confirmed owner\n                let revoked = if member.atype == MembershipType::Owner\n                    && member.status == MembershipStatus::Confirmed as i32\n                {\n                    if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 {\n                        warn!(\"Can't revoke the last owner\");\n                        false\n                    } else {\n                        member.revoke()\n                    }\n                } else {\n                    member.revoke()\n                };\n\n                let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));\n                if revoked || ext_modified {\n                    member.save(&conn).await?;\n                }\n            }\n        // If user is part of the organization, restore it\n        } else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &conn).await {\n            let restored = member.restore();\n            let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));\n            if restored || ext_modified {\n                member.save(&conn).await?;\n            }\n        } else {\n            // If user is not part of the organization\n            let user = match User::find_by_mail(&user_data.email, &conn).await {\n                Some(user) => user, // exists in vaultwarden\n                None => {\n                    // User does not exist yet\n                    let mut new_user = User::new(&user_data.email, None);\n                    new_user.save(&conn).await?;\n\n                    if !CONFIG.mail_enabled() {\n                        Invitation::new(&new_user.email).save(&conn).await?;\n                    }\n                    user_created = true;\n                    new_user\n                }\n            };\n            let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {\n                MembershipStatus::Invited as i32\n            } else {\n                MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites\n            };\n\n            let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &conn).await {\n                Some(org) => (org.name, org.billing_email),\n                None => err!(\"Error looking up organization\"),\n            };\n\n            let mut new_member = Membership::new(user.uuid.clone(), org_id.clone(), Some(org_email.clone()));\n            new_member.set_external_id(Some(user_data.external_id.clone()));\n            new_member.access_all = false;\n            new_member.atype = MembershipType::User as i32;\n            new_member.status = member_status;\n\n            new_member.save(&conn).await?;\n\n            if CONFIG.mail_enabled() {\n                if let Err(e) =\n                    mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await\n                {\n                    // Upon error delete the user, invite and org member records when needed\n                    if user_created {\n                        user.delete(&conn).await?;\n                    } else {\n                        new_member.delete(&conn).await?;\n                    }\n\n                    err!(format!(\"Error sending invite: {e:?} \"));\n                }\n            }\n        }\n    }\n\n    if CONFIG.org_groups_enabled() {\n        for group_data in &data.groups {\n            let group_uuid = match Group::find_by_external_id_and_org(&group_data.external_id, &org_id, &conn).await {\n                Some(group) => group.uuid,\n                None => {\n                    let mut group = Group::new(\n                        org_id.clone(),\n                        group_data.name.clone(),\n                        false,\n                        Some(group_data.external_id.clone()),\n                    );\n                    group.save(&conn).await?;\n                    group.uuid\n                }\n            };\n\n            GroupUser::delete_all_by_group(&group_uuid, &conn).await?;\n\n            for ext_id in &group_data.member_external_ids {\n                if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await {\n                    let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone());\n                    group_user.save(&conn).await?;\n                }\n            }\n        }\n    } else {\n        warn!(\"Group support is disabled, groups will not be imported!\");\n    }\n\n    // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)\n    if data.overwrite_existing {\n        // Generate a HashSet to quickly verify if a member is listed or not.\n        let sync_members: HashSet<String> = data.members.into_iter().map(|m| m.external_id).collect();\n        for member in Membership::find_by_org(&org_id, &conn).await {\n            if let Some(ref user_external_id) = member.external_id {\n                if !sync_members.contains(user_external_id) {\n                    if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {\n                        // Removing owner, check that there is at least one other confirmed owner\n                        if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1\n                        {\n                            warn!(\"Can't delete the last owner\");\n                            continue;\n                        }\n                    }\n                    member.delete(&conn).await?;\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\npub struct PublicToken(OrganizationId);\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for PublicToken {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n        // Get access_token\n        let access_token: &str = match headers.get_one(\"Authorization\") {\n            Some(a) => match a.rsplit(\"Bearer \").next() {\n                Some(split) => split,\n                None => err_handler!(\"No access token provided\"),\n            },\n            None => err_handler!(\"No access token provided\"),\n        };\n        // Check JWT token is valid and get device and user from it\n        let Ok(claims) = auth::decode_api_org(access_token) else {\n            err_handler!(\"Invalid claim\")\n        };\n        // Check if time is between claims.nbf and claims.exp\n        let time_now = Utc::now().timestamp();\n        if time_now < claims.nbf {\n            err_handler!(\"Token issued in the future\");\n        }\n        if time_now > claims.exp {\n            err_handler!(\"Token expired\");\n        }\n        // Check if claims.iss is domain|claims.scope[0]\n        let complete_host = format!(\"{}|{}\", CONFIG.domain_origin(), claims.scope[0]);\n        if complete_host != claims.iss {\n            err_handler!(\"Token not issued by this server\");\n        }\n\n        // Check if claims.sub is org_api_key.uuid\n        // Check if claims.client_sub is org_api_key.org_uuid\n        let conn = match DbConn::from_request(request).await {\n            Outcome::Success(conn) => conn,\n            _ => err_handler!(\"Error getting DB\"),\n        };\n        let Some(org_id) = claims.client_id.strip_prefix(\"organization.\") else {\n            err_handler!(\"Malformed client_id\")\n        };\n        let org_id: OrganizationId = org_id.to_string().into();\n        let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else {\n            err_handler!(\"Invalid client_id\")\n        };\n        if org_api_key.org_uuid != claims.client_sub {\n            err_handler!(\"Token not issued for this org\");\n        }\n        if org_api_key.uuid != claims.sub {\n            err_handler!(\"Token not issued for this client\");\n        }\n\n        Outcome::Success(PublicToken(claims.client_sub))\n    }\n}\n"
  },
  {
    "path": "src/api/core/sends.rs",
    "content": "use std::{path::Path, sync::LazyLock, time::Duration};\n\nuse chrono::{DateTime, TimeDelta, Utc};\nuse num_traits::ToPrimitive;\nuse rocket::{\n    form::Form,\n    fs::{NamedFile, TempFile},\n    serde::json::Json,\n};\nuse serde_json::Value;\n\nuse crate::{\n    api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},\n    auth::{ClientIp, Headers, Host},\n    config::PathType,\n    db::{\n        models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId},\n        DbConn, DbPool,\n    },\n    util::{save_temp_file, NumberOrString},\n    CONFIG,\n};\n\nconst SEND_INACCESSIBLE_MSG: &str = \"Send does not exist or is no longer available\";\nstatic ANON_PUSH_DEVICE: LazyLock<Device> = LazyLock::new(|| {\n    let dt = crate::util::parse_date(\"1970-01-01T00:00:00.000000Z\");\n    Device {\n        uuid: String::from(\"00000000-0000-0000-0000-000000000000\").into(),\n        created_at: dt,\n        updated_at: dt,\n        user_uuid: String::from(\"00000000-0000-0000-0000-000000000000\").into(),\n        name: String::new(),\n        atype: 14, // 14 == Unknown Browser\n        push_uuid: Some(String::from(\"00000000-0000-0000-0000-000000000000\").into()),\n        push_token: None,\n        refresh_token: String::new(),\n        twofactor_remember: None,\n    }\n});\n\n// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues\nconst SIZE_525_MB: i64 = 550_502_400;\n\npub fn routes() -> Vec<rocket::Route> {\n    routes![\n        get_sends,\n        get_send,\n        post_send,\n        post_send_file,\n        post_access,\n        post_access_file,\n        put_send,\n        delete_send,\n        put_remove_password,\n        download_send,\n        post_send_file_v2,\n        post_send_file_v2_data\n    ]\n}\n\npub async fn purge_sends(pool: DbPool) {\n    debug!(\"Purging sends\");\n    if let Ok(conn) = pool.get().await {\n        Send::purge(&conn).await;\n    } else {\n        error!(\"Failed to get DB connection while purging sends\")\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SendData {\n    r#type: i32,\n    key: String,\n    password: Option<String>,\n    max_access_count: Option<NumberOrString>,\n    expiration_date: Option<DateTime<Utc>>,\n    deletion_date: DateTime<Utc>,\n    disabled: bool,\n    hide_email: Option<bool>,\n\n    // Data field\n    name: String,\n    notes: Option<String>,\n    text: Option<Value>,\n    file: Option<Value>,\n    file_length: Option<NumberOrString>,\n\n    // Used for key rotations\n    pub id: Option<SendId>,\n}\n\n/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to\n/// an org with this policy enabled isn't allowed to create new Sends or\n/// modify existing ones, but is allowed to delete them.\n///\n/// Ref: https://bitwarden.com/help/article/policies/#disable-send\n///\n/// There is also a Vaultwarden-specific `sends_allowed` config setting that\n/// controls this policy globally.\nasync fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {\n    let user_id = &headers.user.uuid;\n    if !CONFIG.sends_allowed()\n        || OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await\n    {\n        err!(\"Due to an Enterprise Policy, you are only able to delete an existing Send.\")\n    }\n    Ok(())\n}\n\n/// Enforces the `DisableHideEmail` option of the `Send Options` policy.\n/// A non-owner/admin user belonging to an org with this option enabled isn't\n/// allowed to hide their email address from the recipient of a Bitwarden Send,\n/// but is allowed to remove this option from an existing Send.\n///\n/// Ref: https://bitwarden.com/help/article/policies/#send-options\nasync fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &DbConn) -> EmptyResult {\n    let user_id = &headers.user.uuid;\n    let hide_email = data.hide_email.unwrap_or(false);\n    if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await {\n        err!(\n            \"Due to an Enterprise Policy, you are not allowed to hide your email address \\\n              from recipients when creating or editing a Send.\"\n        )\n    }\n    Ok(())\n}\n\nfn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {\n    let data_val = if data.r#type == SendType::Text as i32 {\n        data.text\n    } else if data.r#type == SendType::File as i32 {\n        data.file\n    } else {\n        err!(\"Invalid Send type\")\n    };\n\n    let data_str = if let Some(mut d) = data_val {\n        d.as_object_mut().and_then(|o| o.remove(\"response\"));\n        serde_json::to_string(&d)?\n    } else {\n        err!(\"Send data not provided\");\n    };\n\n    if data.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() {\n        err!(\n            \"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again.\"\n        );\n    }\n\n    let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());\n    send.user_uuid = Some(user_id);\n    send.notes = data.notes;\n    send.max_access_count = match data.max_access_count {\n        Some(m) => Some(m.into_i32()?),\n        _ => None,\n    };\n    send.expiration_date = data.expiration_date.map(|d| d.naive_utc());\n    send.disabled = data.disabled;\n    send.hide_email = data.hide_email;\n    send.atype = data.r#type;\n\n    send.set_password(data.password.as_deref());\n\n    Ok(send)\n}\n\n#[get(\"/sends\")]\nasync fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {\n    let sends = Send::find_by_user(&headers.user.uuid, &conn);\n    let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect();\n\n    Json(json!({\n      \"data\": sends_json,\n      \"object\": \"list\",\n      \"continuationToken\": null\n    }))\n}\n\n#[get(\"/sends/<send_id>\")]\nasync fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult {\n    match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await {\n        Some(send) => Ok(Json(send.to_json())),\n        None => err!(\"Send not found\", \"Invalid send uuid or does not belong to user\"),\n    }\n}\n\n#[post(\"/sends\", data = \"<data>\")]\nasync fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let data: SendData = data.into_inner();\n    enforce_disable_hide_email_policy(&data, &headers, &conn).await?;\n\n    if data.r#type == SendType::File as i32 {\n        err!(\"File sends should use /api/sends/file\")\n    }\n\n    let mut send = create_send(data, headers.user.uuid)?;\n    send.save(&conn).await?;\n    nt.send_send_update(\n        UpdateType::SyncSendCreate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &headers.device,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(send.to_json()))\n}\n\n#[derive(FromForm)]\nstruct UploadData<'f> {\n    model: Json<SendData>,\n    data: TempFile<'f>,\n}\n\n#[derive(FromForm)]\nstruct UploadDataV2<'f> {\n    data: TempFile<'f>,\n}\n\n// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).\n// This method still exists to support older clients, probably need to remove it sometime.\n// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167\n// 2025: This endpoint doesn't seem to exists anymore in the latest version\n// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs\n#[post(\"/sends/file\", format = \"multipart/form-data\", data = \"<data>\")]\nasync fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let UploadData {\n        model,\n        data,\n    } = data.into_inner();\n    let model = model.into_inner();\n\n    let Some(size) = data.len().to_i64() else {\n        err!(\"Invalid send size\");\n    };\n    if size < 0 {\n        err!(\"Send size can't be negative\")\n    }\n\n    enforce_disable_hide_email_policy(&model, &headers, &conn).await?;\n\n    let size_limit = match CONFIG.user_send_limit() {\n        Some(0) => err!(\"File uploads are disabled\"),\n        Some(limit_kb) => {\n            let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {\n                err!(\"Existing sends overflow\")\n            };\n            let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {\n                err!(\"Send size overflow\");\n            };\n            if left <= 0 {\n                err!(\"Send storage limit reached! Delete some sends to free up space\")\n            }\n            i64::clamp(left, 0, SIZE_525_MB)\n        }\n        None => SIZE_525_MB,\n    };\n\n    if size > size_limit {\n        err!(\"Send storage limit exceeded with this file\");\n    }\n\n    let mut send = create_send(model, headers.user.uuid)?;\n    if send.atype != SendType::File as i32 {\n        err!(\"Send content is not a file\");\n    }\n\n    let file_id = crate::crypto::generate_send_file_id();\n\n    save_temp_file(&PathType::Sends, &format!(\"{}/{file_id}\", send.uuid), data, true).await?;\n\n    let mut data_value: Value = serde_json::from_str(&send.data)?;\n    if let Some(o) = data_value.as_object_mut() {\n        o.insert(String::from(\"id\"), Value::String(file_id));\n        o.insert(String::from(\"size\"), Value::Number(size.into()));\n        o.insert(String::from(\"sizeName\"), Value::String(crate::util::get_display_size(size)));\n    }\n    send.data = serde_json::to_string(&data_value)?;\n\n    // Save the changes in the database\n    send.save(&conn).await?;\n    nt.send_send_update(\n        UpdateType::SyncSendCreate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &headers.device,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(send.to_json()))\n}\n\n// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165\n#[post(\"/sends/file/v2\", data = \"<data>\")]\nasync fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn) -> JsonResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let data = data.into_inner();\n\n    if data.r#type != SendType::File as i32 {\n        err!(\"Send content is not a file\");\n    }\n\n    enforce_disable_hide_email_policy(&data, &headers, &conn).await?;\n\n    let file_length = match &data.file_length {\n        Some(m) => m.into_i64()?,\n        _ => err!(\"Invalid send length\"),\n    };\n    if file_length < 0 {\n        err!(\"Send size can't be negative\")\n    }\n\n    let size_limit = match CONFIG.user_send_limit() {\n        Some(0) => err!(\"File uploads are disabled\"),\n        Some(limit_kb) => {\n            let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {\n                err!(\"Existing sends overflow\")\n            };\n            let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {\n                err!(\"Send size overflow\");\n            };\n            if left <= 0 {\n                err!(\"Send storage limit reached! Delete some sends to free up space\")\n            }\n            i64::clamp(left, 0, SIZE_525_MB)\n        }\n        None => SIZE_525_MB,\n    };\n\n    if file_length > size_limit {\n        err!(\"Send storage limit exceeded with this file\");\n    }\n\n    let mut send = create_send(data, headers.user.uuid)?;\n\n    let file_id = crate::crypto::generate_send_file_id();\n\n    let mut data_value: Value = serde_json::from_str(&send.data)?;\n    if let Some(o) = data_value.as_object_mut() {\n        o.insert(String::from(\"id\"), Value::String(file_id.clone()));\n        o.insert(String::from(\"size\"), Value::Number(file_length.into()));\n        o.insert(String::from(\"sizeName\"), Value::String(crate::util::get_display_size(file_length)));\n    }\n    send.data = serde_json::to_string(&data_value)?;\n    send.save(&conn).await?;\n\n    Ok(Json(json!({\n        \"fileUploadType\": 0, // 0 == Direct | 1 == Azure\n        \"object\": \"send-fileUpload\",\n        \"url\": format!(\"/sends/{}/file/{file_id}\", send.uuid),\n        \"sendResponse\": send.to_json()\n    })))\n}\n\n#[derive(Deserialize)]\n#[allow(non_snake_case)]\npub struct SendFileData {\n    id: SendFileId,\n    size: u64,\n    fileName: String,\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195\n#[post(\"/sends/<send_id>/file/<file_id>\", format = \"multipart/form-data\", data = \"<data>\")]\nasync fn post_send_file_v2_data(\n    send_id: SendId,\n    file_id: SendFileId,\n    data: Form<UploadDataV2<'_>>,\n    headers: Headers,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> EmptyResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let data = data.into_inner();\n\n    let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {\n        err!(\"Send not found. Unable to save the file.\", \"Invalid send uuid or does not belong to user.\")\n    };\n\n    if send.atype != SendType::File as i32 {\n        err!(\"Send is not a file type send.\");\n    }\n\n    let Ok(send_data) = serde_json::from_str::<SendFileData>(&send.data) else {\n        err!(\"Unable to decode send data as json.\")\n    };\n\n    match data.data.raw_name() {\n        Some(raw_file_name)\n            if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName\n            // be less strict only if using CLI, cf. https://github.com/dani-garcia/vaultwarden/issues/5614\n            || (headers.device.is_cli() && send_data.fileName.ends_with(raw_file_name.dangerous_unsafe_unsanitized_raw().as_str())\n            ) => {}\n        Some(raw_file_name) => err!(\n            \"Send file name does not match.\",\n            format!(\n                \"Expected file name '{}' got '{}'\",\n                send_data.fileName,\n                raw_file_name.dangerous_unsafe_unsanitized_raw()\n            )\n        ),\n        _ => err!(\"Send file name does not match or is not provided.\"),\n    }\n\n    if file_id != send_data.id {\n        err!(\"Send file does not match send data.\", format!(\"Expected id {} got {file_id}\", send_data.id));\n    }\n\n    let Some(size) = data.data.len().to_u64() else {\n        err!(\"Send file size overflow.\");\n    };\n\n    if size != send_data.size {\n        err!(\"Send file size does not match.\", format!(\"Expected a file size of {} got {size}\", send_data.size));\n    }\n\n    let file_path = format!(\"{send_id}/{file_id}\");\n\n    save_temp_file(&PathType::Sends, &file_path, data.data, false).await?;\n\n    nt.send_send_update(\n        UpdateType::SyncSendCreate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &headers.device,\n        &conn,\n    )\n    .await;\n\n    Ok(())\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SendAccessData {\n    pub password: Option<String>,\n}\n\n#[post(\"/sends/access/<access_id>\", data = \"<data>\")]\nasync fn post_access(\n    access_id: &str,\n    data: Json<SendAccessData>,\n    conn: DbConn,\n    ip: ClientIp,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let Some(mut send) = Send::find_by_access_id(access_id, &conn).await else {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    };\n\n    if let Some(max_access_count) = send.max_access_count {\n        if send.access_count >= max_access_count {\n            err_code!(SEND_INACCESSIBLE_MSG, 404);\n        }\n    }\n\n    if let Some(expiration) = send.expiration_date {\n        if Utc::now().naive_utc() >= expiration {\n            err_code!(SEND_INACCESSIBLE_MSG, 404)\n        }\n    }\n\n    if Utc::now().naive_utc() >= send.deletion_date {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    }\n\n    if send.disabled {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    }\n\n    if send.password_hash.is_some() {\n        match data.into_inner().password {\n            Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }\n            Some(_) => err!(\"Invalid password\", format!(\"IP: {}.\", ip.ip)),\n            None => err_code!(\"Password not provided\", format!(\"IP: {}.\", ip.ip), 401),\n        }\n    }\n\n    // Files are incremented during the download\n    if send.atype == SendType::Text as i32 {\n        send.access_count += 1;\n    }\n\n    send.save(&conn).await?;\n\n    nt.send_send_update(\n        UpdateType::SyncSendUpdate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &ANON_PUSH_DEVICE,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(send.to_json_access(&conn).await))\n}\n\n#[post(\"/sends/<send_id>/access/file/<file_id>\", data = \"<data>\")]\nasync fn post_access_file(\n    send_id: SendId,\n    file_id: SendFileId,\n    data: Json<SendAccessData>,\n    host: Host,\n    conn: DbConn,\n    nt: Notify<'_>,\n) -> JsonResult {\n    let Some(mut send) = Send::find_by_uuid(&send_id, &conn).await else {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    };\n\n    if let Some(max_access_count) = send.max_access_count {\n        if send.access_count >= max_access_count {\n            err_code!(SEND_INACCESSIBLE_MSG, 404)\n        }\n    }\n\n    if let Some(expiration) = send.expiration_date {\n        if Utc::now().naive_utc() >= expiration {\n            err_code!(SEND_INACCESSIBLE_MSG, 404)\n        }\n    }\n\n    if Utc::now().naive_utc() >= send.deletion_date {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    }\n\n    if send.disabled {\n        err_code!(SEND_INACCESSIBLE_MSG, 404)\n    }\n\n    if send.password_hash.is_some() {\n        match data.into_inner().password {\n            Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }\n            Some(_) => err!(\"Invalid password.\"),\n            None => err_code!(\"Password not provided\", 401),\n        }\n    }\n\n    send.access_count += 1;\n\n    send.save(&conn).await?;\n\n    nt.send_send_update(\n        UpdateType::SyncSendUpdate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &ANON_PUSH_DEVICE,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(json!({\n        \"object\": \"send-fileDownload\",\n        \"id\": file_id,\n        \"url\": download_url(&host, &send_id, &file_id).await?,\n    })))\n}\n\nasync fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {\n    let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;\n\n    if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {\n        let token_claims = crate::auth::generate_send_claims(send_id, file_id);\n        let token = crate::auth::encode_jwt(&token_claims);\n\n        Ok(format!(\"{}/api/sends/{send_id}/{file_id}?t={token}\", &host.host))\n    } else {\n        Ok(operator.presign_read(&format!(\"{send_id}/{file_id}\"), Duration::from_secs(5 * 60)).await?.uri().to_string())\n    }\n}\n\n#[get(\"/sends/<send_id>/<file_id>?<t>\")]\nasync fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {\n    if let Ok(claims) = crate::auth::decode_send(t) {\n        if claims.sub == format!(\"{send_id}/{file_id}\") {\n            return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();\n        }\n    }\n    None\n}\n\n#[put(\"/sends/<send_id>\", data = \"<data>\")]\nasync fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let data: SendData = data.into_inner();\n    enforce_disable_hide_email_policy(&data, &headers, &conn).await?;\n\n    let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {\n        err!(\"Send not found\", \"Send send_id is invalid or does not belong to user\")\n    };\n\n    update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;\n\n    Ok(Json(send.to_json()))\n}\n\npub async fn update_send_from_data(\n    send: &mut Send,\n    data: SendData,\n    headers: &Headers,\n    conn: &DbConn,\n    nt: &Notify<'_>,\n    ut: UpdateType,\n) -> EmptyResult {\n    if send.user_uuid.as_ref() != Some(&headers.user.uuid) {\n        err!(\"Send is not owned by user\")\n    }\n\n    if send.atype != data.r#type {\n        err!(\"Sends can't change type\")\n    }\n\n    if data.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() {\n        err!(\n            \"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again.\"\n        );\n    }\n\n    // When updating a file Send, we receive nulls in the File field, as it's immutable,\n    // so we only need to update the data field in the Text case\n    if data.r#type == SendType::Text as i32 {\n        let data_str = if let Some(mut d) = data.text {\n            d.as_object_mut().and_then(|d| d.remove(\"response\"));\n            serde_json::to_string(&d)?\n        } else {\n            err!(\"Send data not provided\");\n        };\n        send.data = data_str;\n    }\n\n    send.name = data.name;\n    send.akey = data.key;\n    send.deletion_date = data.deletion_date.naive_utc();\n    send.notes = data.notes;\n    send.max_access_count = match data.max_access_count {\n        Some(m) => Some(m.into_i32()?),\n        _ => None,\n    };\n    send.expiration_date = data.expiration_date.map(|d| d.naive_utc());\n    send.hide_email = data.hide_email;\n    send.disabled = data.disabled;\n\n    // Only change the value if it's present\n    if let Some(password) = data.password {\n        send.set_password(Some(&password));\n    }\n\n    send.save(conn).await?;\n    if ut != UpdateType::None {\n        nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await;\n    }\n    Ok(())\n}\n\n#[delete(\"/sends/<send_id>\")]\nasync fn delete_send(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {\n    let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {\n        err!(\"Send not found\", \"Invalid send uuid, or does not belong to user\")\n    };\n\n    send.delete(&conn).await?;\n    nt.send_send_update(\n        UpdateType::SyncSendDelete,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &headers.device,\n        &conn,\n    )\n    .await;\n\n    Ok(())\n}\n\n#[put(\"/sends/<send_id>/remove-password\")]\nasync fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {\n    enforce_disable_send_policy(&headers, &conn).await?;\n\n    let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {\n        err!(\"Send not found\", \"Invalid send uuid, or does not belong to user\")\n    };\n\n    send.set_password(None);\n    send.save(&conn).await?;\n    nt.send_send_update(\n        UpdateType::SyncSendUpdate,\n        &send,\n        &send.update_users_revision(&conn).await,\n        &headers.device,\n        &conn,\n    )\n    .await;\n\n    Ok(Json(send.to_json()))\n}\n"
  },
  {
    "path": "src/api/core/two_factor/authenticator.rs",
    "content": "use data_encoding::BASE32;\nuse rocket::serde::json::Json;\nuse rocket::Route;\n\nuse crate::{\n    api::{core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, PasswordOrOtpData},\n    auth::{ClientIp, Headers},\n    crypto,\n    db::{\n        models::{EventType, TwoFactor, TwoFactorType, UserId},\n        DbConn,\n    },\n    util::NumberOrString,\n};\n\npub use crate::config::CONFIG;\n\npub fn routes() -> Vec<Route> {\n    routes![generate_authenticator, activate_authenticator, activate_authenticator_put, disable_authenticator]\n}\n\n#[post(\"/two-factor/get-authenticator\", data = \"<data>\")]\nasync fn generate_authenticator(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let type_ = TwoFactorType::Authenticator as i32;\n    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await;\n\n    let (enabled, key) = match twofactor {\n        Some(tf) => (true, tf.data),\n        _ => (false, crypto::encode_random_bytes::<20>(&BASE32)),\n    };\n\n    // Upstream seems to also return `userVerificationToken`, but doesn't seem to be used at all.\n    // It should help prevent TOTP disclosure if someone keeps their vault unlocked.\n    // Since it doesn't seem to be used, and also does not cause any issues, lets leave it out of the response.\n    // See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Auth/Controllers/TwoFactorController.cs#L94\n    Ok(Json(json!({\n        \"enabled\": enabled,\n        \"key\": key,\n        \"object\": \"twoFactorAuthenticator\"\n    })))\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EnableAuthenticatorData {\n    key: String,\n    token: NumberOrString,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n#[post(\"/two-factor/authenticator\", data = \"<data>\")]\nasync fn activate_authenticator(data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: EnableAuthenticatorData = data.into_inner();\n    let key = data.key;\n    let token = data.token.into_string();\n\n    let mut user = headers.user;\n\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash,\n        otp: data.otp,\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    // Validate key as base32 and 20 bytes length\n    let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {\n        Ok(decoded) => decoded,\n        _ => err!(\"Invalid totp secret\"),\n    };\n\n    if decoded_key.len() != 20 {\n        err!(\"Invalid key length\")\n    }\n\n    // Validate the token provided with the key, and save new twofactor\n    validate_totp_code(&user.uuid, &token, &key.to_uppercase(), &headers.ip, &conn).await?;\n\n    _generate_recover_code(&mut user, &conn).await;\n\n    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    Ok(Json(json!({\n        \"enabled\": true,\n        \"key\": key,\n        \"object\": \"twoFactorAuthenticator\"\n    })))\n}\n\n#[put(\"/two-factor/authenticator\", data = \"<data>\")]\nasync fn activate_authenticator_put(data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {\n    activate_authenticator(data, headers, conn).await\n}\n\npub async fn validate_totp_code_str(\n    user_id: &UserId,\n    totp_code: &str,\n    secret: &str,\n    ip: &ClientIp,\n    conn: &DbConn,\n) -> EmptyResult {\n    if !totp_code.chars().all(char::is_numeric) {\n        err!(\"TOTP code is not a number\");\n    }\n\n    validate_totp_code(user_id, totp_code, secret, ip, conn).await\n}\n\npub async fn validate_totp_code(\n    user_id: &UserId,\n    totp_code: &str,\n    secret: &str,\n    ip: &ClientIp,\n    conn: &DbConn,\n) -> EmptyResult {\n    use totp_lite::{totp_custom, Sha1};\n\n    let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else {\n        err!(\"Invalid TOTP secret\")\n    };\n\n    let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await\n    {\n        Some(tf) => tf,\n        _ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()),\n    };\n\n    // The amount of steps back and forward in time\n    // Also check if we need to disable time drifted TOTP codes.\n    // If that is the case, we set the steps to 0 so only the current TOTP is valid.\n    let steps = i64::from(!CONFIG.authenticator_disable_time_drift());\n\n    // Get the current system time in UNIX Epoch (UTC)\n    let current_time = chrono::Utc::now();\n    let current_timestamp = current_time.timestamp();\n\n    for step in -steps..=steps {\n        let time_step = current_timestamp / 30i64 + step;\n\n        // We need to calculate the time offsite and cast it as an u64.\n        // Since we only have times into the future and the totp generator needs an u64 instead of the default i64.\n        let time = (current_timestamp + step * 30i64) as u64;\n        let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time);\n\n        // Check the given code equals the generated and if the time_step is larger then the one last used.\n        if generated == totp_code && time_step > twofactor.last_used {\n            // If the step does not equals 0 the time is drifted either server or client side.\n            if step != 0 {\n                warn!(\"TOTP Time drift detected. The step offset is {step}\");\n            }\n\n            // Save the last used time step so only totp time steps higher then this one are allowed.\n            // This will also save a newly created twofactor if the code is correct.\n            twofactor.last_used = time_step;\n            twofactor.save(conn).await?;\n            return Ok(());\n        } else if generated == totp_code && time_step <= twofactor.last_used {\n            warn!(\"This TOTP or a TOTP code within {steps} steps back or forward has already been used!\");\n            err!(\n                format!(\"Invalid TOTP code! Server time: {} IP: {}\", current_time.format(\"%F %T UTC\"), ip.ip),\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn2fa\n                }\n            );\n        }\n    }\n\n    // Else no valid code received, deny access\n    err!(\n        format!(\"Invalid TOTP code! Server time: {} IP: {}\", current_time.format(\"%F %T UTC\"), ip.ip),\n        ErrorEvent {\n            event: EventType::UserFailedLogIn2fa\n        }\n    );\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct DisableAuthenticatorData {\n    key: String,\n    master_password_hash: String,\n    r#type: NumberOrString,\n}\n\n#[delete(\"/two-factor/authenticator\", data = \"<data>\")]\nasync fn disable_authenticator(data: Json<DisableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let user = headers.user;\n    let type_ = data.r#type.into_i32()?;\n\n    if !user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\");\n    }\n\n    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {\n        if twofactor.data == data.key {\n            twofactor.delete(&conn).await?;\n            log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)\n                .await;\n        } else {\n            err!(format!(\"TOTP key for user {} does not match recorded value, cannot deactivate\", &user.email));\n        }\n    }\n\n    if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {\n        super::enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;\n    }\n\n    Ok(Json(json!({\n        \"enabled\": false,\n        \"keys\": type_,\n        \"object\": \"twoFactorProvider\"\n    })))\n}\n"
  },
  {
    "path": "src/api/core/two_factor/duo.rs",
    "content": "use chrono::Utc;\nuse data_encoding::BASE64;\nuse rocket::serde::json::Json;\nuse rocket::Route;\n\nuse crate::{\n    api::{\n        core::log_user_event, core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult,\n        PasswordOrOtpData,\n    },\n    auth::Headers,\n    crypto,\n    db::{\n        models::{EventType, TwoFactor, TwoFactorType, User, UserId},\n        DbConn,\n    },\n    error::MapResult,\n    http_client::make_http_request,\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![get_duo, activate_duo, activate_duo_put,]\n}\n\n#[derive(Serialize, Deserialize)]\nstruct DuoData {\n    host: String, // Duo API hostname\n    ik: String,   // client id\n    sk: String,   // client secret\n}\n\nimpl DuoData {\n    fn global() -> Option<Self> {\n        match (CONFIG._enable_duo(), CONFIG.duo_host()) {\n            (true, Some(host)) => Some(Self {\n                host,\n                ik: CONFIG.duo_ikey().unwrap(),\n                sk: CONFIG.duo_skey().unwrap(),\n            }),\n            _ => None,\n        }\n    }\n    fn msg(s: &str) -> Self {\n        Self {\n            host: s.into(),\n            ik: s.into(),\n            sk: s.into(),\n        }\n    }\n    fn secret() -> Self {\n        Self::msg(\"<global_secret>\")\n    }\n    fn obscure(self) -> Self {\n        let mut host = self.host;\n        let mut ik = self.ik;\n        let mut sk = self.sk;\n\n        let digits = 4;\n        let replaced = \"************\";\n\n        host.replace_range(digits.., replaced);\n        ik.replace_range(digits.., replaced);\n        sk.replace_range(digits.., replaced);\n\n        Self {\n            host,\n            ik,\n            sk,\n        }\n    }\n}\n\nenum DuoStatus {\n    Global(DuoData),\n    // Using the global duo config\n    User(DuoData),\n    // Using the user's config\n    Disabled(bool), // True if there is a global setting\n}\n\nimpl DuoStatus {\n    fn data(self) -> Option<DuoData> {\n        match self {\n            DuoStatus::Global(data) => Some(data),\n            DuoStatus::User(data) => Some(data),\n            DuoStatus::Disabled(_) => None,\n        }\n    }\n}\n\nconst DISABLED_MESSAGE_DEFAULT: &str = \"<To use the global Duo keys, please leave these fields untouched>\";\n\n#[post(\"/two-factor/get-duo\", data = \"<data>\")]\nasync fn get_duo(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let data = get_user_duo_data(&user.uuid, &conn).await;\n\n    let (enabled, data) = match data {\n        DuoStatus::Global(_) => (true, Some(DuoData::secret())),\n        DuoStatus::User(data) => (true, Some(data.obscure())),\n        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),\n        DuoStatus::Disabled(false) => (false, None),\n    };\n\n    let json = if let Some(data) = data {\n        json!({\n            \"enabled\": enabled,\n            \"host\": data.host,\n            \"clientSecret\": data.sk,\n            \"clientId\": data.ik,\n            \"object\": \"twoFactorDuo\"\n        })\n    } else {\n        json!({\n            \"enabled\": enabled,\n            \"host\": null,\n            \"clientSecret\": null,\n            \"clientId\": null,\n            \"object\": \"twoFactorDuo\"\n        })\n    };\n\n    Ok(Json(json))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EnableDuoData {\n    host: String,\n    client_secret: String,\n    client_id: String,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\nimpl From<EnableDuoData> for DuoData {\n    fn from(d: EnableDuoData) -> Self {\n        Self {\n            host: d.host,\n            ik: d.client_id,\n            sk: d.client_secret,\n        }\n    }\n}\n\nfn check_duo_fields_custom(data: &EnableDuoData) -> bool {\n    fn empty_or_default(s: &str) -> bool {\n        let st = s.trim();\n        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT\n    }\n\n    !empty_or_default(&data.host) && !empty_or_default(&data.client_secret) && !empty_or_default(&data.client_id)\n}\n\n#[post(\"/two-factor/duo\", data = \"<data>\")]\nasync fn activate_duo(data: Json<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: EnableDuoData = data.into_inner();\n    let mut user = headers.user;\n\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash.clone(),\n        otp: data.otp.clone(),\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    let (data, data_str) = if check_duo_fields_custom(&data) {\n        let data_req: DuoData = data.into();\n        let data_str = serde_json::to_string(&data_req)?;\n        duo_api_request(\"GET\", \"/auth/v2/check\", \"\", &data_req).await.map_res(\"Failed to validate Duo credentials\")?;\n        (data_req.obscure(), data_str)\n    } else {\n        (DuoData::secret(), String::new())\n    };\n\n    let type_ = TwoFactorType::Duo;\n    let twofactor = TwoFactor::new(user.uuid.clone(), type_, data_str);\n    twofactor.save(&conn).await?;\n\n    _generate_recover_code(&mut user, &conn).await;\n\n    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    Ok(Json(json!({\n        \"enabled\": true,\n        \"host\": data.host,\n        \"clientSecret\": data.sk,\n        \"clientId\": data.ik,\n        \"object\": \"twoFactorDuo\"\n    })))\n}\n\n#[put(\"/two-factor/duo\", data = \"<data>\")]\nasync fn activate_duo_put(data: Json<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {\n    activate_duo(data, headers, conn).await\n}\n\nasync fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {\n    use reqwest::{header, Method};\n    use std::str::FromStr;\n\n    // https://duo.com/docs/authapi#api-details\n    let url = format!(\"https://{}{path}\", &data.host);\n    let date = Utc::now().to_rfc2822();\n    let username = &data.ik;\n    let fields = [&date, method, &data.host, path, params];\n    let password = crypto::hmac_sign(&data.sk, &fields.join(\"\\n\"));\n\n    let m = Method::from_str(method).unwrap_or_default();\n\n    make_http_request(m, &url)?\n        .basic_auth(username, Some(password))\n        .header(header::USER_AGENT, \"vaultwarden:Duo/1.0 (Rust)\")\n        .header(header::DATE, date)\n        .send()\n        .await?\n        .error_for_status()?;\n\n    Ok(())\n}\n\nconst DUO_EXPIRE: i64 = 300;\nconst APP_EXPIRE: i64 = 3600;\n\nconst AUTH_PREFIX: &str = \"AUTH\";\nconst DUO_PREFIX: &str = \"TX\";\nconst APP_PREFIX: &str = \"APP\";\n\nasync fn get_user_duo_data(user_id: &UserId, conn: &DbConn) -> DuoStatus {\n    let type_ = TwoFactorType::Duo as i32;\n\n    // If the user doesn't have an entry, disabled\n    let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await else {\n        return DuoStatus::Disabled(DuoData::global().is_some());\n    };\n\n    // If the user has the required values, we use those\n    if let Ok(data) = serde_json::from_str(&twofactor.data) {\n        return DuoStatus::User(data);\n    }\n\n    // Otherwise, we try to use the globals\n    if let Some(global) = DuoData::global() {\n        return DuoStatus::Global(global);\n    }\n\n    // If there are no globals configured, just disable it\n    DuoStatus::Disabled(false)\n}\n\n// let (ik, sk, ak, host) = get_duo_keys();\npub(crate) async fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {\n    let data = match User::find_by_mail(email, conn).await {\n        Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),\n        _ => DuoData::global(),\n    }\n    .map_res(\"Can't fetch Duo Keys\")?;\n\n    Ok((data.ik, data.sk, CONFIG.get_duo_akey().await, data.host))\n}\n\npub async fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {\n    let now = Utc::now().timestamp();\n\n    let (ik, sk, ak, host) = get_duo_keys_email(email, conn).await?;\n\n    let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);\n    let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);\n\n    Ok((format!(\"{duo_sign}:{app_sign}\"), host))\n}\n\nfn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {\n    let val = format!(\"{email}|{ikey}|{expire}\");\n    let cookie = format!(\"{prefix}|{}\", BASE64.encode(val.as_bytes()));\n\n    format!(\"{cookie}|{}\", crypto::hmac_sign(key, &cookie))\n}\n\npub async fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {\n    let split: Vec<&str> = response.split(':').collect();\n    if split.len() != 2 {\n        err!(\n            \"Invalid response length\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        );\n    }\n\n    let auth_sig = split[0];\n    let app_sig = split[1];\n\n    let now = Utc::now().timestamp();\n\n    let (ik, sk, ak, _host) = get_duo_keys_email(email, conn).await?;\n\n    let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;\n    let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;\n\n    if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {\n        err!(\n            \"Error validating duo authentication\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        )\n    }\n\n    Ok(())\n}\n\nfn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {\n    let split: Vec<&str> = val.split('|').collect();\n    if split.len() != 3 {\n        err!(\"Invalid value length\")\n    }\n\n    let u_prefix = split[0];\n    let u_b64 = split[1];\n    let u_sig = split[2];\n\n    let sig = crypto::hmac_sign(key, &format!(\"{u_prefix}|{u_b64}\"));\n\n    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {\n        err!(\"Duo signatures don't match\")\n    }\n\n    if u_prefix != prefix {\n        err!(\"Prefixes don't match\")\n    }\n\n    let Ok(cookie_vec) = BASE64.decode(u_b64.as_bytes()) else {\n        err!(\"Invalid Duo cookie encoding\")\n    };\n\n    let Ok(cookie) = String::from_utf8(cookie_vec) else {\n        err!(\"Invalid Duo cookie encoding\")\n    };\n\n    let cookie_split: Vec<&str> = cookie.split('|').collect();\n    if cookie_split.len() != 3 {\n        err!(\"Invalid cookie length\")\n    }\n\n    let username = cookie_split[0];\n    let u_ikey = cookie_split[1];\n    let expire = cookie_split[2];\n\n    if !crypto::ct_eq(ikey, u_ikey) {\n        err!(\"Invalid ikey\")\n    }\n\n    let expire: i64 = match expire.parse() {\n        Ok(e) => e,\n        Err(_) => err!(\"Invalid expire time\"),\n    };\n\n    if time >= expire {\n        err!(\"Expired authorization\")\n    }\n\n    Ok(username.into())\n}\n"
  },
  {
    "path": "src/api/core/two_factor/duo_oidc.rs",
    "content": "use chrono::Utc;\nuse data_encoding::HEXLOWER;\nuse jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};\nuse reqwest::{header, StatusCode};\nuse ring::digest::{digest, Digest, SHA512_256};\nuse serde::Serialize;\nuse std::collections::HashMap;\n\nuse crate::{\n    api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},\n    crypto,\n    db::{\n        models::{DeviceId, EventType, TwoFactorDuoContext},\n        DbConn, DbPool,\n    },\n    error::Error,\n    http_client::make_http_request,\n    CONFIG,\n};\nuse url::Url;\n\n// The location on this service that Duo should redirect users to. For us, this is a bridge\n// built in to the Bitwarden clients.\n// See: https://github.com/bitwarden/clients/blob/5fb46df3415aefced0b52f2db86c873962255448/apps/web/src/connectors/duo-redirect.ts\nconst DUO_REDIRECT_LOCATION: &str = \"duo-redirect-connector.html\";\n\n// Number of seconds that a JWT we generate for Duo should be valid for.\nconst JWT_VALIDITY_SECS: i64 = 300;\n\n// Number of seconds that a Duo context stored in the database should be valid for.\nconst CTX_VALIDITY_SECS: i64 = 300;\n\n// Expected algorithm used by Duo to sign JWTs.\nconst DUO_RESP_SIGNATURE_ALG: Algorithm = Algorithm::HS512;\n\n// Signature algorithm we're using to sign JWTs for Duo. Must be either HS512 or HS256.\nconst JWT_SIGNATURE_ALG: Algorithm = Algorithm::HS512;\n\n// Size of random strings for state and nonce. Must be at least 16 characters and at most 1024 characters.\n// If increasing this above 64, also increase the size of the twofactor_duo_ctx.state and\n// twofactor_duo_ctx.nonce database columns for postgres and mariadb.\nconst STATE_LENGTH: usize = 64;\n\n// client_assertion payload for health checks and obtaining MFA results.\n#[derive(Debug, Serialize, Deserialize)]\nstruct ClientAssertion {\n    pub iss: String,\n    pub sub: String,\n    pub aud: String,\n    pub exp: i64,\n    pub jti: String,\n    pub iat: i64,\n}\n\n// authorization request payload sent with clients to Duo for MFA\n#[derive(Debug, Serialize, Deserialize)]\nstruct AuthorizationRequest {\n    pub response_type: String,\n    pub scope: String,\n    pub exp: i64,\n    pub client_id: String,\n    pub redirect_uri: String,\n    pub state: String,\n    pub duo_uname: String,\n    pub iss: String,\n    pub aud: String,\n    pub nonce: String,\n}\n\n// Duo service health check responses\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\nenum HealthCheckResponse {\n    HealthOK {\n        stat: String,\n    },\n    HealthFail {\n        message: String,\n        message_detail: String,\n    },\n}\n\n// Outer structure of response when exchanging authz code for MFA results\n#[derive(Debug, Serialize, Deserialize)]\nstruct IdTokenResponse {\n    id_token: String, // IdTokenClaims\n    access_token: String,\n    expires_in: i64,\n    token_type: String,\n}\n\n// Inner structure of IdTokenResponse.id_token\n#[derive(Debug, Serialize, Deserialize)]\nstruct IdTokenClaims {\n    preferred_username: String,\n    nonce: String,\n}\n\n// Duo OIDC Authorization Client\n// See https://duo.com/docs/oauthapi\nstruct DuoClient {\n    client_id: String,     // Duo Client ID (DuoData.ik)\n    client_secret: String, // Duo Client Secret (DuoData.sk)\n    api_host: String,      // Duo API hostname (DuoData.host)\n    redirect_uri: String,  // URL in this application clients should call for MFA verification\n}\n\nimpl DuoClient {\n    // Construct a new DuoClient\n    fn new(client_id: String, client_secret: String, api_host: String, redirect_uri: String) -> DuoClient {\n        DuoClient {\n            client_id,\n            client_secret,\n            api_host,\n            redirect_uri,\n        }\n    }\n\n    // Generate a client assertion for health checks and authorization code exchange.\n    fn new_client_assertion(&self, url: &str) -> ClientAssertion {\n        let now = Utc::now().timestamp();\n        let jwt_id = crypto::get_random_string_alphanum(STATE_LENGTH);\n\n        ClientAssertion {\n            iss: self.client_id.clone(),\n            sub: self.client_id.clone(),\n            aud: url.to_string(),\n            exp: now + JWT_VALIDITY_SECS,\n            jti: jwt_id,\n            iat: now,\n        }\n    }\n\n    // Given a serde-serializable struct, attempt to encode it as a JWT\n    fn encode_duo_jwt<T: Serialize>(&self, jwt_payload: T) -> Result<String, Error> {\n        match jsonwebtoken::encode(\n            &Header::new(JWT_SIGNATURE_ALG),\n            &jwt_payload,\n            &EncodingKey::from_secret(self.client_secret.as_bytes()),\n        ) {\n            Ok(token) => Ok(token),\n            Err(e) => err!(format!(\"Error encoding Duo JWT: {e:?}\")),\n        }\n    }\n\n    // \"required\" health check to verify the integration is configured and Duo's services\n    // are up.\n    // https://duo.com/docs/oauthapi#health-check\n    async fn health_check(&self) -> Result<(), Error> {\n        let health_check_url: String = format!(\"https://{}/oauth/v1/health_check\", self.api_host);\n\n        let jwt_payload = self.new_client_assertion(&health_check_url);\n\n        let token = match self.encode_duo_jwt(jwt_payload) {\n            Ok(token) => token,\n            Err(e) => return Err(e),\n        };\n\n        let mut post_body = HashMap::new();\n        post_body.insert(\"client_assertion\", token);\n        post_body.insert(\"client_id\", self.client_id.clone());\n\n        let res = match make_http_request(reqwest::Method::POST, &health_check_url)?\n            .header(header::USER_AGENT, \"vaultwarden:Duo/2.0 (Rust)\")\n            .form(&post_body)\n            .send()\n            .await\n        {\n            Ok(r) => r,\n            Err(e) => err!(format!(\"Error requesting Duo health check: {e:?}\")),\n        };\n\n        let response: HealthCheckResponse = match res.json::<HealthCheckResponse>().await {\n            Ok(r) => r,\n            Err(e) => err!(format!(\"Duo health check response decode error: {e:?}\")),\n        };\n\n        let health_stat: String = match response {\n            HealthCheckResponse::HealthOK {\n                stat,\n            } => stat,\n            HealthCheckResponse::HealthFail {\n                message,\n                message_detail,\n            } => err!(format!(\"Duo health check FAIL response, msg: {message}, detail: {message_detail}\")),\n        };\n\n        if health_stat != \"OK\" {\n            err!(format!(\"Duo health check failed, got OK-like body with stat {health_stat}\"));\n        }\n\n        Ok(())\n    }\n\n    // Constructs the URL for the authorization request endpoint on Duo's service.\n    // Clients are sent here to continue authentication.\n    // https://duo.com/docs/oauthapi#authorization-request\n    fn make_authz_req_url(&self, duo_username: &str, state: String, nonce: String) -> Result<String, Error> {\n        let now = Utc::now().timestamp();\n\n        let jwt_payload = AuthorizationRequest {\n            response_type: String::from(\"code\"),\n            scope: String::from(\"openid\"),\n            exp: now + JWT_VALIDITY_SECS,\n            client_id: self.client_id.clone(),\n            redirect_uri: self.redirect_uri.clone(),\n            state,\n            duo_uname: String::from(duo_username),\n            iss: self.client_id.clone(),\n            aud: format!(\"https://{}\", self.api_host),\n            nonce,\n        };\n\n        let token = self.encode_duo_jwt(jwt_payload)?;\n\n        let authz_endpoint = format!(\"https://{}/oauth/v1/authorize\", self.api_host);\n        let mut auth_url = match Url::parse(authz_endpoint.as_str()) {\n            Ok(url) => url,\n            Err(e) => err!(format!(\"Error parsing Duo authorization URL: {e:?}\")),\n        };\n\n        {\n            let mut query_params = auth_url.query_pairs_mut();\n            query_params.append_pair(\"response_type\", \"code\");\n            query_params.append_pair(\"client_id\", self.client_id.as_str());\n            query_params.append_pair(\"request\", token.as_str());\n        }\n\n        let final_auth_url = auth_url.to_string();\n        Ok(final_auth_url)\n    }\n\n    // Exchange the authorization code obtained from an access token provided by the user\n    // for the result of the MFA and validate.\n    // See: https://duo.com/docs/oauthapi#access-token (under Response Format)\n    async fn exchange_authz_code_for_result(\n        &self,\n        duo_code: &str,\n        duo_username: &str,\n        nonce: &str,\n    ) -> Result<(), Error> {\n        if duo_code.is_empty() {\n            err!(\"Empty Duo authorization code\")\n        }\n\n        let token_url = format!(\"https://{}/oauth/v1/token\", self.api_host);\n\n        let jwt_payload = self.new_client_assertion(&token_url);\n\n        let token = match self.encode_duo_jwt(jwt_payload) {\n            Ok(token) => token,\n            Err(e) => return Err(e),\n        };\n\n        let mut post_body = HashMap::new();\n        post_body.insert(\"grant_type\", String::from(\"authorization_code\"));\n        post_body.insert(\"code\", String::from(duo_code));\n\n        // Must be the same URL that was supplied in the authorization request for the supplied duo_code\n        post_body.insert(\"redirect_uri\", self.redirect_uri.clone());\n\n        post_body\n            .insert(\"client_assertion_type\", String::from(\"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\"));\n        post_body.insert(\"client_assertion\", token);\n\n        let res = match make_http_request(reqwest::Method::POST, &token_url)?\n            .header(header::USER_AGENT, \"vaultwarden:Duo/2.0 (Rust)\")\n            .form(&post_body)\n            .send()\n            .await\n        {\n            Ok(r) => r,\n            Err(e) => err!(format!(\"Error exchanging Duo code: {e:?}\")),\n        };\n\n        let status_code = res.status();\n        if status_code != StatusCode::OK {\n            err!(format!(\"Failure response from Duo: {status_code}\"))\n        }\n\n        let response: IdTokenResponse = match res.json::<IdTokenResponse>().await {\n            Ok(r) => r,\n            Err(e) => err!(format!(\"Error decoding ID token response: {e:?}\")),\n        };\n\n        let mut validation = Validation::new(DUO_RESP_SIGNATURE_ALG);\n        validation.set_required_spec_claims(&[\"exp\", \"aud\", \"iss\"]);\n        validation.set_audience(&[&self.client_id]);\n        validation.set_issuer(&[token_url.as_str()]);\n\n        let token_data = match jsonwebtoken::decode::<IdTokenClaims>(\n            &response.id_token,\n            &DecodingKey::from_secret(self.client_secret.as_bytes()),\n            &validation,\n        ) {\n            Ok(c) => c,\n            Err(e) => err!(format!(\"Failed to decode Duo token {e:?}\")),\n        };\n\n        let matching_nonces = crypto::ct_eq(nonce, &token_data.claims.nonce);\n        let matching_usernames = crypto::ct_eq(duo_username, &token_data.claims.preferred_username);\n\n        if !(matching_nonces && matching_usernames) {\n            err!(\"Error validating Duo authorization, nonce or username mismatch.\")\n        };\n\n        Ok(())\n    }\n}\n\nstruct DuoAuthContext {\n    pub state: String,\n    pub user_email: String,\n    pub nonce: String,\n    pub exp: i64,\n}\n\n// Given a state string, retrieve the associated Duo auth context and\n// delete the retrieved state from the database.\nasync fn extract_context(state: &str, conn: &DbConn) -> Option<DuoAuthContext> {\n    let ctx: TwoFactorDuoContext = match TwoFactorDuoContext::find_by_state(state, conn).await {\n        Some(c) => c,\n        None => return None,\n    };\n\n    if ctx.exp < Utc::now().timestamp() {\n        ctx.delete(conn).await.ok();\n        return None;\n    }\n\n    // Copy the context data, so that we can delete the context from\n    // the database before returning.\n    let ret_ctx = DuoAuthContext {\n        state: ctx.state.clone(),\n        user_email: ctx.user_email.clone(),\n        nonce: ctx.nonce.clone(),\n        exp: ctx.exp,\n    };\n\n    ctx.delete(conn).await.ok();\n    Some(ret_ctx)\n}\n\n// Task to clean up expired Duo authentication contexts that may have accumulated in the database.\npub async fn purge_duo_contexts(pool: DbPool) {\n    debug!(\"Purging Duo authentication contexts\");\n    if let Ok(conn) = pool.get().await {\n        TwoFactorDuoContext::purge_expired_duo_contexts(&conn).await;\n    } else {\n        error!(\"Failed to get DB connection while purging expired Duo authentications\")\n    }\n}\n\n// Construct the url that Duo should redirect users to.\nfn make_callback_url(client_name: &str) -> Result<String, Error> {\n    // Get the location of this application as defined in the config.\n    let base = match Url::parse(&format!(\"{}/\", CONFIG.domain())) {\n        Ok(url) => url,\n        Err(e) => err!(format!(\"Error parsing configured domain URL (check your domain configuration): {e:?}\")),\n    };\n\n    // Add the client redirect bridge location\n    let mut callback = match base.join(DUO_REDIRECT_LOCATION) {\n        Ok(url) => url,\n        Err(e) => err!(format!(\"Error constructing Duo redirect URL (check your domain configuration): {e:?}\")),\n    };\n\n    // Add the 'client' string with the authenticating device type. The callback connector uses this\n    // information to figure out how it should handle certain clients.\n    {\n        let mut query_params = callback.query_pairs_mut();\n        query_params.append_pair(\"client\", client_name);\n    }\n    Ok(callback.to_string())\n}\n\n// Pre-redirect first stage of the Duo OIDC authentication flow.\n// Returns the \"AuthUrl\" that should be returned to clients for MFA.\npub async fn get_duo_auth_url(\n    email: &str,\n    client_id: &str,\n    device_identifier: &DeviceId,\n    conn: &DbConn,\n) -> Result<String, Error> {\n    let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;\n\n    let callback_url = match make_callback_url(client_id) {\n        Ok(url) => url,\n        Err(e) => return Err(e),\n    };\n\n    let client = DuoClient::new(ik, sk, host, callback_url);\n\n    match client.health_check().await {\n        Ok(()) => {}\n        Err(e) => return Err(e),\n    };\n\n    // Generate random OAuth2 state and OIDC Nonce\n    let state: String = crypto::get_random_string_alphanum(STATE_LENGTH);\n    let nonce: String = crypto::get_random_string_alphanum(STATE_LENGTH);\n\n    // Bind the nonce to the device that's currently authing by hashing the nonce and device id\n    // and sending the result as the OIDC nonce.\n    let d: Digest = digest(&SHA512_256, format!(\"{nonce}{device_identifier}\").as_bytes());\n    let hash: String = HEXLOWER.encode(d.as_ref());\n\n    match TwoFactorDuoContext::save(state.as_str(), email, nonce.as_str(), CTX_VALIDITY_SECS, conn).await {\n        Ok(()) => client.make_authz_req_url(email, state, hash),\n        Err(e) => err!(format!(\"Error saving Duo authentication context: {e:?}\")),\n    }\n}\n\n// Post-redirect second stage of the Duo OIDC authentication flow.\n// Exchanges an authorization code for the MFA result with Duo's API and validates the result.\npub async fn validate_duo_login(\n    email: &str,\n    two_factor_token: &str,\n    client_id: &str,\n    device_identifier: &DeviceId,\n    conn: &DbConn,\n) -> EmptyResult {\n    // Result supplied to us by clients in the form \"<authz code>|<state>\"\n    let split: Vec<&str> = two_factor_token.split('|').collect();\n    if split.len() != 2 {\n        err!(\n            \"Invalid response length\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        );\n    }\n\n    let code = split[0];\n    let state = split[1];\n\n    let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;\n\n    // Get the context by the state reported by the client. If we don't have one,\n    // it means the context is either missing or expired.\n    let ctx = match extract_context(state, conn).await {\n        Some(c) => c,\n        None => {\n            err!(\n                \"Error validating duo authentication\",\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn2fa\n                }\n            )\n        }\n    };\n\n    // Context validation steps\n    let matching_usernames = crypto::ct_eq(email, &ctx.user_email);\n\n    // Probably redundant, but we're double-checking them anyway.\n    let matching_states = crypto::ct_eq(state, &ctx.state);\n    let unexpired_context = ctx.exp > Utc::now().timestamp();\n\n    if !(matching_usernames && matching_states && unexpired_context) {\n        err!(\n            \"Error validating duo authentication\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        )\n    }\n\n    let callback_url = match make_callback_url(client_id) {\n        Ok(url) => url,\n        Err(e) => return Err(e),\n    };\n\n    let client = DuoClient::new(ik, sk, host, callback_url);\n\n    match client.health_check().await {\n        Ok(()) => {}\n        Err(e) => return Err(e),\n    };\n\n    let d: Digest = digest(&SHA512_256, format!(\"{}{device_identifier}\", ctx.nonce).as_bytes());\n    let hash: String = HEXLOWER.encode(d.as_ref());\n\n    match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {\n        Ok(_) => Ok(()),\n        Err(_) => {\n            err!(\n                \"Error validating duo authentication\",\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn2fa\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "src/api/core/two_factor/email.rs",
    "content": "use chrono::{DateTime, TimeDelta, Utc};\nuse rocket::serde::json::Json;\nuse rocket::Route;\n\nuse crate::{\n    api::{\n        core::{log_user_event, two_factor::_generate_recover_code},\n        EmptyResult, JsonResult, PasswordOrOtpData,\n    },\n    auth::{ClientHeaders, Headers},\n    crypto,\n    db::{\n        models::{AuthRequest, AuthRequestId, DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId},\n        DbConn,\n    },\n    error::{Error, MapResult},\n    mail, CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![get_email, send_email_login, send_email, email,]\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct SendEmailLoginData {\n    #[serde(alias = \"DeviceIdentifier\")]\n    device_identifier: DeviceId,\n    #[serde(alias = \"Email\")]\n    email: Option<String>,\n    #[serde(alias = \"MasterPasswordHash\")]\n    master_password_hash: Option<String>,\n    auth_request_id: Option<AuthRequestId>,\n    auth_request_access_code: Option<String>,\n}\n\n/// User is trying to login and wants to use email 2FA.\n/// Does not require Bearer token\n#[post(\"/two-factor/send-email-login\", data = \"<data>\")] // JsonResult\nasync fn send_email_login(data: Json<SendEmailLoginData>, client_headers: ClientHeaders, conn: DbConn) -> EmptyResult {\n    let data: SendEmailLoginData = data.into_inner();\n\n    if !CONFIG._enable_email_2fa() {\n        err!(\"Email 2FA is disabled\")\n    }\n\n    // Ratelimit the login\n    crate::ratelimit::check_limit_login(&client_headers.ip.ip)?;\n\n    // Get the user\n    let email = match &data.email {\n        Some(email) if !email.is_empty() => Some(email),\n        _ => None,\n    };\n    let master_password_hash = match &data.master_password_hash {\n        Some(password_hash) if !password_hash.is_empty() => Some(password_hash),\n        _ => None,\n    };\n    let auth_request_id = match &data.auth_request_id {\n        Some(auth_request_id) if !auth_request_id.is_empty() => Some(auth_request_id),\n        _ => None,\n    };\n\n    let user = if let Some(email) = email {\n        let Some(user) = User::find_by_mail(email, &conn).await else {\n            err!(\"Username or password is incorrect. Try again.\")\n        };\n\n        if let Some(master_password_hash) = master_password_hash {\n            // Check password\n            if !user.check_valid_password(master_password_hash) {\n                err!(\"Username or password is incorrect. Try again.\")\n            }\n        } else if let Some(auth_request_id) = auth_request_id {\n            let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_id, &conn).await else {\n                err!(\"AuthRequest doesn't exist\", \"User not found\")\n            };\n            let Some(code) = &data.auth_request_access_code else {\n                err!(\"no auth request access code\")\n            };\n\n            if auth_request.device_type != client_headers.device_type\n                || auth_request.request_ip != client_headers.ip.ip.to_string()\n                || !auth_request.check_access_code(code)\n            {\n                err!(\"AuthRequest doesn't exist\", \"Invalid device, IP or code\")\n            }\n        } else {\n            err!(\"No password hash has been submitted.\")\n        }\n\n        user\n    } else {\n        // SSO login only sends device id, so we get the user by the most recently used device\n        let Some(user) = User::find_by_device_for_email2fa(&data.device_identifier, &conn).await else {\n            err!(\"Username or password is incorrect. Try again.\")\n        };\n\n        user\n    };\n\n    send_token(&user.uuid, &conn).await\n}\n\n/// Generate the token, save the data for later verification and send email to user\npub async fn send_token(user_id: &UserId, conn: &DbConn) -> EmptyResult {\n    let type_ = TwoFactorType::Email as i32;\n    let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res(\"Two factor not found\")?;\n\n    let generated_token = crypto::generate_email_token(CONFIG.email_token_size());\n\n    let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;\n    twofactor_data.set_token(generated_token);\n    twofactor.data = twofactor_data.to_json();\n    twofactor.save(conn).await?;\n\n    mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res(\"Token is empty\")?).await?;\n\n    Ok(())\n}\n\n/// When user clicks on Manage email 2FA show the user the related information\n#[post(\"/two-factor/get-email\", data = \"<data>\")]\nasync fn get_email(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let (enabled, mfa_email) =\n        match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &conn).await {\n            Some(x) => {\n                let twofactor_data = EmailTokenData::from_json(&x.data)?;\n                (true, json!(twofactor_data.email))\n            }\n            _ => (false, serde_json::value::Value::Null),\n        };\n\n    Ok(Json(json!({\n        \"email\": mfa_email,\n        \"enabled\": enabled,\n        \"object\": \"twoFactorEmail\"\n    })))\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct SendEmailData {\n    /// Email where 2FA codes will be sent to, can be different than user email account.\n    email: String,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n/// Send a verification email to the specified email address to check whether it exists/belongs to user.\n#[post(\"/two-factor/send-email\", data = \"<data>\")]\nasync fn send_email(data: Json<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {\n    let data: SendEmailData = data.into_inner();\n    let user = headers.user;\n\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash,\n        otp: data.otp,\n    }\n    .validate(&user, false, &conn)\n    .await?;\n\n    if !CONFIG._enable_email_2fa() {\n        err!(\"Email 2FA is disabled\")\n    }\n\n    let type_ = TwoFactorType::Email as i32;\n\n    if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {\n        tf.delete(&conn).await?;\n    }\n\n    let generated_token = crypto::generate_email_token(CONFIG.email_token_size());\n    let twofactor_data = EmailTokenData::new(data.email, generated_token);\n\n    // Uses EmailVerificationChallenge as type to show that it's not verified yet.\n    let twofactor = TwoFactor::new(user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json());\n    twofactor.save(&conn).await?;\n\n    mail::send_token(&twofactor_data.email, &twofactor_data.last_token.map_res(\"Token is empty\")?).await?;\n\n    Ok(())\n}\n\n#[derive(Deserialize, Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EmailData {\n    email: String,\n    token: String,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n/// Verify email belongs to user and can be used for 2FA email codes.\n#[put(\"/two-factor/email\", data = \"<data>\")]\nasync fn email(data: Json<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: EmailData = data.into_inner();\n    let mut user = headers.user;\n\n    // This is the last step in the verification process, delete the otp directly afterwards\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash,\n        otp: data.otp,\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    let type_ = TwoFactorType::EmailVerificationChallenge as i32;\n    let mut twofactor =\n        TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await.map_res(\"Two factor not found\")?;\n\n    let mut email_data = EmailTokenData::from_json(&twofactor.data)?;\n\n    let Some(issued_token) = &email_data.last_token else {\n        err!(\"No token available\")\n    };\n\n    if !crypto::ct_eq(issued_token, data.token) {\n        err!(\"Token is invalid\")\n    }\n\n    email_data.reset_token();\n    twofactor.atype = TwoFactorType::Email as i32;\n    twofactor.data = email_data.to_json();\n    twofactor.save(&conn).await?;\n\n    _generate_recover_code(&mut user, &conn).await;\n\n    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    Ok(Json(json!({\n        \"email\": email_data.email,\n        \"enabled\": \"true\",\n        \"object\": \"twoFactorEmail\"\n    })))\n}\n\n/// Validate the email code when used as TwoFactor token mechanism\npub async fn validate_email_code_str(\n    user_id: &UserId,\n    token: &str,\n    data: &str,\n    ip: &std::net::IpAddr,\n    conn: &DbConn,\n) -> EmptyResult {\n    let mut email_data = EmailTokenData::from_json(data)?;\n    let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn)\n        .await\n        .map_res(\"Two factor not found\")?;\n    let Some(issued_token) = &email_data.last_token else {\n        err!(\n            format!(\"No token available! IP: {ip}\"),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        )\n    };\n\n    if !crypto::ct_eq(issued_token, token) {\n        email_data.add_attempt();\n        if email_data.attempts >= CONFIG.email_attempts_limit() {\n            email_data.reset_token();\n        }\n        twofactor.data = email_data.to_json();\n        twofactor.save(conn).await?;\n\n        err!(\n            format!(\"Token is invalid! IP: {ip}\"),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        )\n    }\n\n    email_data.reset_token();\n    twofactor.data = email_data.to_json();\n    twofactor.save(conn).await?;\n\n    let date = DateTime::from_timestamp(email_data.token_sent, 0).expect(\"Email token timestamp invalid.\").naive_utc();\n    let max_time = CONFIG.email_expiration_time() as i64;\n    if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {\n        err!(\n            \"Token has expired\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        )\n    }\n\n    Ok(())\n}\n\n/// Data stored in the TwoFactor table in the db\n#[derive(Serialize, Deserialize)]\npub struct EmailTokenData {\n    /// Email address where the token will be sent to. Can be different from account email.\n    pub email: String,\n    /// Some(token): last valid token issued that has not been entered.\n    /// None: valid token was used and removed.\n    pub last_token: Option<String>,\n    /// UNIX timestamp of token issue.\n    pub token_sent: i64,\n    /// Amount of token entry attempts for last_token.\n    pub attempts: u64,\n}\n\nimpl EmailTokenData {\n    pub fn new(email: String, token: String) -> EmailTokenData {\n        EmailTokenData {\n            email,\n            last_token: Some(token),\n            token_sent: Utc::now().timestamp(),\n            attempts: 0,\n        }\n    }\n\n    pub fn set_token(&mut self, token: String) {\n        self.last_token = Some(token);\n        self.token_sent = Utc::now().timestamp();\n    }\n\n    pub fn reset_token(&mut self) {\n        self.last_token = None;\n        self.attempts = 0;\n    }\n\n    pub fn add_attempt(&mut self) {\n        self.attempts = self.attempts.saturating_add(1);\n    }\n\n    pub fn to_json(&self) -> String {\n        serde_json::to_string(&self).unwrap()\n    }\n\n    pub fn from_json(string: &str) -> Result<EmailTokenData, Error> {\n        let res: Result<EmailTokenData, serde_json::Error> = serde_json::from_str(string);\n        match res {\n            Ok(x) => Ok(x),\n            Err(_) => err!(\"Could not decode EmailTokenData from string\"),\n        }\n    }\n}\n\npub async fn activate_email_2fa(user: &User, conn: &DbConn) -> EmptyResult {\n    if user.verified_at.is_none() {\n        err!(\"Auto-enabling of email 2FA failed because the users email address has not been verified!\");\n    }\n    let twofactor_data = EmailTokenData::new(user.email.clone(), String::new());\n    let twofactor = TwoFactor::new(user.uuid.clone(), TwoFactorType::Email, twofactor_data.to_json());\n    twofactor.save(conn).await\n}\n\n/// Takes an email address and obscures it by replacing it with asterisks except two characters.\npub fn obscure_email(email: &str) -> String {\n    let split: Vec<&str> = email.rsplitn(2, '@').collect();\n\n    let mut name = split[1].to_string();\n    let domain = &split[0];\n\n    let name_size = name.chars().count();\n\n    let new_name = match name_size {\n        1..=3 => \"*\".repeat(name_size),\n        _ => {\n            let stars = \"*\".repeat(name_size - 2);\n            name.truncate(2);\n            format!(\"{name}{stars}\")\n        }\n    };\n\n    format!(\"{new_name}@{domain}\")\n}\n\npub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &DbConn) -> EmptyResult {\n    if let Some(user) = User::find_by_uuid(user_id, conn).await {\n        activate_email_2fa(&user, conn).await\n    } else {\n        err!(\"User not found!\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_obscure_email_long() {\n        let email = \"bytes@example.ext\";\n\n        let result = obscure_email(email);\n\n        // Only first two characters should be visible.\n        assert_eq!(result, \"by***@example.ext\");\n    }\n\n    #[test]\n    fn test_obscure_email_short() {\n        let email = \"byt@example.ext\";\n\n        let result = obscure_email(email);\n\n        // If it's smaller than 3 characters it should only show asterisks.\n        assert_eq!(result, \"***@example.ext\");\n    }\n}\n"
  },
  {
    "path": "src/api/core/two_factor/mod.rs",
    "content": "use chrono::{TimeDelta, Utc};\nuse data_encoding::BASE32;\nuse rocket::serde::json::Json;\nuse rocket::Route;\nuse serde_json::Value;\n\nuse crate::{\n    api::{\n        core::{log_event, log_user_event},\n        EmptyResult, JsonResult, PasswordOrOtpData,\n    },\n    auth::Headers,\n    crypto,\n    db::{\n        models::{\n            DeviceType, EventType, Membership, MembershipType, OrgPolicyType, Organization, OrganizationId, TwoFactor,\n            TwoFactorIncomplete, User, UserId,\n        },\n        DbConn, DbPool,\n    },\n    mail,\n    util::NumberOrString,\n    CONFIG,\n};\n\npub mod authenticator;\npub mod duo;\npub mod duo_oidc;\npub mod email;\npub mod protected_actions;\npub mod webauthn;\npub mod yubikey;\n\npub fn routes() -> Vec<Route> {\n    let mut routes = routes![\n        get_twofactor,\n        get_recover,\n        disable_twofactor,\n        disable_twofactor_put,\n        get_device_verification_settings,\n    ];\n\n    routes.append(&mut authenticator::routes());\n    routes.append(&mut duo::routes());\n    routes.append(&mut email::routes());\n    routes.append(&mut webauthn::routes());\n    routes.append(&mut yubikey::routes());\n    routes.append(&mut protected_actions::routes());\n\n    routes\n}\n\n#[get(\"/two-factor\")]\nasync fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {\n    let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn).await;\n    let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();\n\n    Json(json!({\n        \"data\": twofactors_json,\n        \"object\": \"list\",\n        \"continuationToken\": null,\n    }))\n}\n\n#[post(\"/two-factor/get-recover\", data = \"<data>\")]\nasync fn get_recover(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, true, &conn).await?;\n\n    Ok(Json(json!({\n        \"code\": user.totp_recover,\n        \"object\": \"twoFactorRecover\"\n    })))\n}\n\nasync fn _generate_recover_code(user: &mut User, conn: &DbConn) {\n    if user.totp_recover.is_none() {\n        let totp_recover = crypto::encode_random_bytes::<20>(&BASE32);\n        user.totp_recover = Some(totp_recover);\n        user.save(conn).await.ok();\n    }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct DisableTwoFactorData {\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n    r#type: NumberOrString,\n}\n\n#[post(\"/two-factor/disable\", data = \"<data>\")]\nasync fn disable_twofactor(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: DisableTwoFactorData = data.into_inner();\n    let user = headers.user;\n\n    // Delete directly after a valid token has been provided\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash,\n        otp: data.otp,\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    let type_ = data.r#type.into_i32()?;\n\n    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {\n        twofactor.delete(&conn).await?;\n        log_user_event(EventType::UserDisabled2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn)\n            .await;\n    }\n\n    if TwoFactor::find_by_user(&user.uuid, &conn).await.is_empty() {\n        enforce_2fa_policy(&user, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await?;\n    }\n\n    Ok(Json(json!({\n        \"enabled\": false,\n        \"type\": type_,\n        \"object\": \"twoFactorProvider\"\n    })))\n}\n\n#[put(\"/two-factor/disable\", data = \"<data>\")]\nasync fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {\n    disable_twofactor(data, headers, conn).await\n}\n\npub async fn enforce_2fa_policy(\n    user: &User,\n    act_user_id: &UserId,\n    device_type: i32,\n    ip: &std::net::IpAddr,\n    conn: &DbConn,\n) -> EmptyResult {\n    for member in\n        Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()\n    {\n        // Policy only applies to non-Owner/non-Admin members who have accepted joining the org\n        if member.atype < MembershipType::Admin {\n            if CONFIG.mail_enabled() {\n                let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap();\n                mail::send_2fa_removed_from_org(&user.email, &org.name).await?;\n            }\n            let mut member = member;\n            member.revoke();\n            member.save(conn).await?;\n\n            log_event(\n                EventType::OrganizationUserRevoked as i32,\n                &member.uuid,\n                &member.org_uuid,\n                act_user_id,\n                device_type,\n                ip,\n                conn,\n            )\n            .await;\n        }\n    }\n\n    Ok(())\n}\n\npub async fn enforce_2fa_policy_for_org(\n    org_id: &OrganizationId,\n    act_user_id: &UserId,\n    device_type: i32,\n    ip: &std::net::IpAddr,\n    conn: &DbConn,\n) -> EmptyResult {\n    let org = Organization::find_by_uuid(org_id, conn).await.unwrap();\n    for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() {\n        // Don't enforce the policy for Admins and Owners.\n        if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {\n            if CONFIG.mail_enabled() {\n                let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap();\n                mail::send_2fa_removed_from_org(&user.email, &org.name).await?;\n            }\n            let mut member = member;\n            member.revoke();\n            member.save(conn).await?;\n\n            log_event(\n                EventType::OrganizationUserRevoked as i32,\n                &member.uuid,\n                org_id,\n                act_user_id,\n                device_type,\n                ip,\n                conn,\n            )\n            .await;\n        }\n    }\n\n    Ok(())\n}\n\npub async fn send_incomplete_2fa_notifications(pool: DbPool) {\n    debug!(\"Sending notifications for incomplete 2FA logins\");\n\n    if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {\n        return;\n    }\n\n    let conn = match pool.get().await {\n        Ok(conn) => conn,\n        _ => {\n            error!(\"Failed to get DB connection in send_incomplete_2fa_notifications()\");\n            return;\n        }\n    };\n\n    let now = Utc::now().naive_utc();\n    let time_limit = TimeDelta::try_minutes(CONFIG.incomplete_2fa_time_limit()).unwrap();\n    let time_before = now - time_limit;\n    let incomplete_logins = TwoFactorIncomplete::find_logins_before(&time_before, &conn).await;\n    for login in incomplete_logins {\n        let user = User::find_by_uuid(&login.user_uuid, &conn).await.expect(\"User not found\");\n        info!(\n            \"User {} did not complete a 2FA login within the configured time limit. IP: {}\",\n            user.email, login.ip_address\n        );\n        match mail::send_incomplete_2fa_login(\n            &user.email,\n            &login.ip_address,\n            &login.login_time,\n            &login.device_name,\n            &DeviceType::from_i32(login.device_type).to_string(),\n        )\n        .await\n        {\n            Ok(_) => {\n                if let Err(e) = login.delete(&conn).await {\n                    error!(\"Error deleting incomplete 2FA record: {e:#?}\");\n                }\n            }\n            Err(e) => {\n                error!(\"Error sending incomplete 2FA email: {e:#?}\");\n            }\n        }\n    }\n}\n\n// This function currently is just a dummy and the actual part is not implemented yet.\n// This also prevents 404 errors.\n//\n// See the following Bitwarden PR's regarding this feature.\n// https://github.com/bitwarden/clients/pull/2843\n// https://github.com/bitwarden/clients/pull/2839\n// https://github.com/bitwarden/server/pull/2016\n//\n// The HTML part is hidden via the CSS patches done via the bw_web_build repo\n#[get(\"/two-factor/get-device-verification-settings\")]\nfn get_device_verification_settings(_headers: Headers, _conn: DbConn) -> Json<Value> {\n    Json(json!({\n        \"isDeviceVerificationSectionEnabled\":false,\n        \"unknownDeviceVerificationEnabled\":false,\n        \"object\":\"deviceVerificationSettings\"\n    }))\n}\n"
  },
  {
    "path": "src/api/core/two_factor/protected_actions.rs",
    "content": "use chrono::{naive::serde::ts_seconds, NaiveDateTime, TimeDelta, Utc};\nuse rocket::{serde::json::Json, Route};\n\nuse crate::{\n    api::EmptyResult,\n    auth::Headers,\n    crypto,\n    db::{\n        models::{TwoFactor, TwoFactorType, UserId},\n        DbConn,\n    },\n    error::{Error, MapResult},\n    mail, CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![request_otp, verify_otp]\n}\n\n/// Data stored in the TwoFactor table in the db\n#[derive(Debug, Serialize, Deserialize)]\npub struct ProtectedActionData {\n    /// Token issued to validate the protected action\n    pub token: String,\n    /// UNIX timestamp of token issue.\n    #[serde(with = \"ts_seconds\")]\n    pub token_sent: NaiveDateTime,\n    // The total amount of attempts\n    pub attempts: u64,\n}\n\nimpl ProtectedActionData {\n    pub fn new(token: String) -> Self {\n        Self {\n            token,\n            token_sent: Utc::now().naive_utc(),\n            attempts: 0,\n        }\n    }\n\n    pub fn to_json(&self) -> String {\n        serde_json::to_string(&self).unwrap()\n    }\n\n    pub fn from_json(string: &str) -> Result<Self, Error> {\n        let res: Result<Self, serde_json::Error> = serde_json::from_str(string);\n        match res {\n            Ok(x) => Ok(x),\n            Err(_) => err!(\"Could not decode ProtectedActionData from string\"),\n        }\n    }\n\n    pub fn add_attempt(&mut self) {\n        self.attempts = self.attempts.saturating_add(1);\n    }\n\n    pub fn time_since_sent(&self) -> TimeDelta {\n        Utc::now().naive_utc() - self.token_sent\n    }\n}\n\n#[post(\"/accounts/request-otp\")]\nasync fn request_otp(headers: Headers, conn: DbConn) -> EmptyResult {\n    if !CONFIG.mail_enabled() {\n        err!(\"Email is disabled for this server. Either enable email or login using your master password instead of login via device.\");\n    }\n\n    let user = headers.user;\n\n    // Only one Protected Action per user is allowed to take place, delete the previous one\n    if let Some(pa) = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &conn).await\n    {\n        let pa_data = ProtectedActionData::from_json(&pa.data)?;\n        let elapsed = pa_data.time_since_sent().num_seconds();\n        let delay = 30;\n        if elapsed < delay {\n            err!(format!(\"Please wait {} seconds before requesting another code.\", (delay - elapsed)));\n        }\n\n        pa.delete(&conn).await?;\n    }\n\n    let generated_token = crypto::generate_email_token(CONFIG.email_token_size());\n    let pa_data = ProtectedActionData::new(generated_token);\n\n    // Uses EmailVerificationChallenge as type to show that it's not verified yet.\n    let twofactor = TwoFactor::new(user.uuid, TwoFactorType::ProtectedActions, pa_data.to_json());\n    twofactor.save(&conn).await?;\n\n    mail::send_protected_action_token(&user.email, &pa_data.token).await?;\n\n    Ok(())\n}\n\n#[derive(Deserialize, Serialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\nstruct ProtectedActionVerify {\n    #[serde(rename = \"OTP\", alias = \"otp\")]\n    otp: String,\n}\n\n#[post(\"/accounts/verify-otp\", data = \"<data>\")]\nasync fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, conn: DbConn) -> EmptyResult {\n    if !CONFIG.mail_enabled() {\n        err!(\"Email is disabled for this server. Either enable email or login using your master password instead of login via device.\");\n    }\n\n    let user = headers.user;\n    let data: ProtectedActionVerify = data.into_inner();\n\n    // Delete the token after one validation attempt\n    // This endpoint only gets called for the vault export, and doesn't need a second attempt\n    validate_protected_action_otp(&data.otp, &user.uuid, true, &conn).await\n}\n\npub async fn validate_protected_action_otp(\n    otp: &str,\n    user_id: &UserId,\n    delete_if_valid: bool,\n    conn: &DbConn,\n) -> EmptyResult {\n    let mut pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)\n        .await\n        .map_res(\"Protected action token not found, try sending the code again or restart the process\")?;\n    let mut pa_data = ProtectedActionData::from_json(&pa.data)?;\n    pa_data.add_attempt();\n    pa.data = pa_data.to_json();\n\n    // Fail after x attempts if the token has been used too many times.\n    // Don't delete it, as we use it to keep track of attempts.\n    if pa_data.attempts >= CONFIG.email_attempts_limit() {\n        err!(\"Token has expired\")\n    }\n\n    // Check if the token has expired (Using the email 2fa expiration time)\n    let max_time = CONFIG.email_expiration_time() as i64;\n    if pa_data.time_since_sent().num_seconds() > max_time {\n        pa.delete(conn).await?;\n        err!(\"Token has expired\")\n    }\n\n    if !crypto::ct_eq(&pa_data.token, otp) {\n        pa.save(conn).await?;\n        err!(\"Token is invalid\")\n    }\n\n    if delete_if_valid {\n        pa.delete(conn).await?;\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/api/core/two_factor/webauthn.rs",
    "content": "use crate::{\n    api::{\n        core::{log_user_event, two_factor::_generate_recover_code},\n        EmptyResult, JsonResult, PasswordOrOtpData,\n    },\n    auth::Headers,\n    crypto::ct_eq,\n    db::{\n        models::{EventType, TwoFactor, TwoFactorType, UserId},\n        DbConn,\n    },\n    error::Error,\n    util::NumberOrString,\n    CONFIG,\n};\nuse rocket::serde::json::Json;\nuse rocket::Route;\nuse serde_json::Value;\nuse std::str::FromStr;\nuse std::sync::LazyLock;\nuse std::time::Duration;\nuse url::Url;\nuse uuid::Uuid;\nuse webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration};\nuse webauthn_rs::{Webauthn, WebauthnBuilder};\nuse webauthn_rs_proto::{\n    AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,\n    PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,\n    RequestAuthenticationExtensions, UserVerificationPolicy,\n};\n\nstatic WEBAUTHN: LazyLock<Webauthn> = LazyLock::new(|| {\n    let domain = CONFIG.domain();\n    let domain_origin = CONFIG.domain_origin();\n    let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default();\n    let rp_origin = Url::parse(&domain_origin).unwrap();\n\n    let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)\n        .expect(\"Creating WebauthnBuilder failed\")\n        .rp_name(&domain)\n        .timeout(Duration::from_millis(60000));\n\n    webauthn.build().expect(\"Building Webauthn failed\")\n});\n\npub fn routes() -> Vec<Route> {\n    routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,]\n}\n\n// Some old u2f structs still needed for migrating from u2f to WebAuthn\n// Both `struct Registration` and `struct U2FRegistration` can be removed if we remove the u2f to WebAuthn migration\n#[derive(Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Registration {\n    pub key_handle: Vec<u8>,\n    pub pub_key: Vec<u8>,\n    pub attestation_cert: Option<Vec<u8>>,\n    pub device_name: Option<String>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct U2FRegistration {\n    pub id: i32,\n    pub name: String,\n    #[serde(with = \"Registration\")]\n    pub reg: Registration,\n    pub counter: u32,\n    compromised: bool,\n    pub migrated: Option<bool>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct WebauthnRegistration {\n    pub id: i32,\n    pub name: String,\n    pub migrated: bool,\n\n    pub credential: Passkey,\n}\n\nimpl WebauthnRegistration {\n    fn to_json(&self) -> Value {\n        json!({\n            \"id\": self.id,\n            \"name\": self.name,\n            \"migrated\": self.migrated,\n        })\n    }\n\n    fn set_backup_eligible(&mut self, backup_eligible: bool, backup_state: bool) -> bool {\n        let mut changed = false;\n        let mut cred: Credential = self.credential.clone().into();\n\n        if cred.backup_state != backup_state {\n            cred.backup_state = backup_state;\n            changed = true;\n        }\n\n        if backup_eligible && !cred.backup_eligible {\n            cred.backup_eligible = true;\n            changed = true;\n        }\n\n        self.credential = cred.into();\n        changed\n    }\n}\n\n#[post(\"/two-factor/get-webauthn\", data = \"<data>\")]\nasync fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    if !CONFIG.domain_set() {\n        err!(\"`DOMAIN` environment variable is not set. Webauthn disabled\")\n    }\n\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?;\n    let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();\n\n    Ok(Json(json!({\n        \"enabled\": enabled,\n        \"keys\": registrations_json,\n        \"object\": \"twoFactorWebAuthn\"\n    })))\n}\n\n#[post(\"/two-factor/get-webauthn-challenge\", data = \"<data>\")]\nasync fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let registrations = get_webauthn_registrations(&user.uuid, &conn)\n        .await?\n        .1\n        .into_iter()\n        .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering\n        .collect();\n\n    let (mut challenge, state) = WEBAUTHN.start_passkey_registration(\n        Uuid::from_str(&user.uuid).expect(\"Failed to parse UUID\"), // Should never fail\n        &user.email,\n        user.display_name(),\n        Some(registrations),\n    )?;\n\n    let mut state = serde_json::to_value(&state)?;\n    state[\"rs\"][\"policy\"] = Value::String(\"discouraged\".to_string());\n    state[\"rs\"][\"extensions\"].as_object_mut().unwrap().clear();\n\n    let type_ = TwoFactorType::WebauthnRegisterChallenge;\n    TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&conn).await?;\n\n    // Because for this flow we abuse the passkeys as 2FA, and use it more like a securitykey\n    // we need to modify some of the default settings defined by `start_passkey_registration()`.\n    challenge.public_key.extensions = None;\n    if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() {\n        asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;\n    }\n\n    let mut challenge_value = serde_json::to_value(challenge.public_key)?;\n    challenge_value[\"status\"] = \"ok\".into();\n    challenge_value[\"errorMessage\"] = \"\".into();\n    Ok(Json(challenge_value))\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EnableWebauthnData {\n    id: NumberOrString, // 1..5\n    name: String,\n    device_response: RegisterPublicKeyCredentialCopy,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RegisterPublicKeyCredentialCopy {\n    pub id: String,\n    pub raw_id: Base64UrlSafeData,\n    pub response: AuthenticatorAttestationResponseRawCopy,\n    pub r#type: String,\n}\n\n// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AuthenticatorAttestationResponseRawCopy {\n    #[serde(rename = \"AttestationObject\", alias = \"attestationObject\")]\n    pub attestation_object: Base64UrlSafeData,\n    #[serde(rename = \"clientDataJson\", alias = \"clientDataJSON\")]\n    pub client_data_json: Base64UrlSafeData,\n}\n\nimpl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential {\n    fn from(r: RegisterPublicKeyCredentialCopy) -> Self {\n        Self {\n            id: r.id,\n            raw_id: r.raw_id,\n            response: AuthenticatorAttestationResponseRaw {\n                attestation_object: r.response.attestation_object,\n                client_data_json: r.response.client_data_json,\n                transports: None,\n            },\n            type_: r.r#type,\n            extensions: RegistrationExtensionsClientOutputs::default(),\n        }\n    }\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct PublicKeyCredentialCopy {\n    pub id: String,\n    pub raw_id: Base64UrlSafeData,\n    pub response: AuthenticatorAssertionResponseRawCopy,\n    pub extensions: AuthenticationExtensionsClientOutputs,\n    pub r#type: String,\n}\n\n// This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct AuthenticatorAssertionResponseRawCopy {\n    pub authenticator_data: Base64UrlSafeData,\n    #[serde(rename = \"clientDataJson\", alias = \"clientDataJSON\")]\n    pub client_data_json: Base64UrlSafeData,\n    pub signature: Base64UrlSafeData,\n    pub user_handle: Option<Base64UrlSafeData>,\n}\n\nimpl From<PublicKeyCredentialCopy> for PublicKeyCredential {\n    fn from(r: PublicKeyCredentialCopy) -> Self {\n        Self {\n            id: r.id,\n            raw_id: r.raw_id,\n            response: AuthenticatorAssertionResponseRaw {\n                authenticator_data: r.response.authenticator_data,\n                client_data_json: r.response.client_data_json,\n                signature: r.response.signature,\n                user_handle: r.response.user_handle,\n            },\n            extensions: r.extensions,\n            type_: r.r#type,\n        }\n    }\n}\n\n#[post(\"/two-factor/webauthn\", data = \"<data>\")]\nasync fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: EnableWebauthnData = data.into_inner();\n    let mut user = headers.user;\n\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash,\n        otp: data.otp,\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    // Retrieve and delete the saved challenge state\n    let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;\n    let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await {\n        Some(tf) => {\n            let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;\n            tf.delete(&conn).await?;\n            state\n        }\n        None => err!(\"Can't recover challenge\"),\n    };\n\n    // Verify the credentials with the saved state\n    let credential = WEBAUTHN.finish_passkey_registration(&data.device_response.into(), &state)?;\n\n    let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1;\n    // TODO: Check for repeated ID's\n    registrations.push(WebauthnRegistration {\n        id: data.id.into_i32()?,\n        name: data.name,\n        migrated: false,\n\n        credential,\n    });\n\n    // Save the registrations and return them\n    TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)\n        .save(&conn)\n        .await?;\n    _generate_recover_code(&mut user, &conn).await;\n\n    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    let keys_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();\n    Ok(Json(json!({\n        \"enabled\": true,\n        \"keys\": keys_json,\n        \"object\": \"twoFactorU2f\"\n    })))\n}\n\n#[put(\"/two-factor/webauthn\", data = \"<data>\")]\nasync fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {\n    activate_webauthn(data, headers, conn).await\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct DeleteU2FData {\n    id: NumberOrString,\n    master_password_hash: String,\n}\n\n#[delete(\"/two-factor/webauthn\", data = \"<data>\")]\nasync fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let id = data.id.into_i32()?;\n    if !headers.user.check_valid_password(&data.master_password_hash) {\n        err!(\"Invalid password\");\n    }\n\n    let Some(mut tf) =\n        TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn).await\n    else {\n        err!(\"Webauthn data not found!\")\n    };\n\n    let mut data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?;\n\n    let Some(item_pos) = data.iter().position(|r| r.id == id) else {\n        err!(\"Webauthn entry not found\")\n    };\n\n    let removed_item = data.remove(item_pos);\n    tf.data = serde_json::to_string(&data)?;\n    tf.save(&conn).await?;\n    drop(tf);\n\n    // If entry is migrated from u2f, delete the u2f entry as well\n    if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn).await\n    {\n        let mut data: Vec<U2FRegistration> = match serde_json::from_str(&u2f.data) {\n            Ok(d) => d,\n            Err(_) => err!(\"Error parsing U2F data\"),\n        };\n\n        data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice());\n        let new_data_str = serde_json::to_string(&data)?;\n\n        u2f.data = new_data_str;\n        u2f.save(&conn).await?;\n    }\n\n    let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect();\n\n    Ok(Json(json!({\n        \"enabled\": true,\n        \"keys\": keys_json,\n        \"object\": \"twoFactorU2f\"\n    })))\n}\n\npub async fn get_webauthn_registrations(\n    user_id: &UserId,\n    conn: &DbConn,\n) -> Result<(bool, Vec<WebauthnRegistration>), Error> {\n    let type_ = TwoFactorType::Webauthn as i32;\n    match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {\n        Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),\n        None => Ok((false, Vec::new())), // If no data, return empty list\n    }\n}\n\npub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonResult {\n    // Load saved credentials\n    let creds: Vec<Passkey> =\n        get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();\n\n    if creds.is_empty() {\n        err!(\"No Webauthn devices registered\")\n    }\n\n    // Generate a challenge based on the credentials\n    let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?;\n\n    // Modify to discourage user verification\n    let mut state = serde_json::to_value(&state)?;\n    state[\"ast\"][\"policy\"] = Value::String(\"discouraged\".to_string());\n\n    // Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well\n    let app_id = format!(\"{}/app-id.json\", &CONFIG.domain());\n    state[\"ast\"][\"appid\"] = Value::String(app_id.clone());\n\n    response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;\n    response\n        .public_key\n        .extensions\n        .get_or_insert(RequestAuthenticationExtensions {\n            appid: None,\n            uvm: None,\n            hmac_get_secret: None,\n        })\n        .appid = Some(app_id);\n\n    // Save the challenge state for later validation\n    TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)\n        .save(conn)\n        .await?;\n\n    // Return challenge to the clients\n    Ok(Json(serde_json::to_value(response.public_key)?))\n}\n\npub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &DbConn) -> EmptyResult {\n    let type_ = TwoFactorType::WebauthnLoginChallenge as i32;\n    let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {\n        Some(tf) => {\n            let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;\n            tf.delete(conn).await?;\n            state\n        }\n        None => err!(\n            \"Can't recover login challenge\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        ),\n    };\n\n    let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?;\n    let rsp: PublicKeyCredential = rsp.into();\n\n    let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;\n\n    // We need to check for and update the backup_eligible flag when needed.\n    // Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x\n    // Because of this we check the flag at runtime and update the registrations and state when needed\n    let backup_flags_updated = check_and_update_backup_eligible(&rsp, &mut registrations, &mut state)?;\n\n    let authentication_result = WEBAUTHN.finish_passkey_authentication(&rsp, &state)?;\n\n    for reg in &mut registrations {\n        if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {\n            // If the cred id matches and the credential is updated, Some(true) is returned\n            // In those cases, update the record, else leave it alone\n            let credential_updated = reg.credential.update_credential(&authentication_result) == Some(true);\n            if credential_updated || backup_flags_updated {\n                TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)\n                    .save(conn)\n                    .await?;\n            }\n            return Ok(());\n        }\n    }\n\n    err!(\n        \"Credential not present\",\n        ErrorEvent {\n            event: EventType::UserFailedLogIn2fa\n        }\n    )\n}\n\nfn check_and_update_backup_eligible(\n    rsp: &PublicKeyCredential,\n    registrations: &mut Vec<WebauthnRegistration>,\n    state: &mut PasskeyAuthentication,\n) -> Result<bool, Error> {\n    // The feature flags from the response\n    // For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data\n    const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000;\n    const FLAG_BACKUP_STATE: u8 = 0b0001_0000;\n\n    if let Some(bits) = rsp.response.authenticator_data.get(32) {\n        let backup_eligible = 0 != (bits & FLAG_BACKUP_ELIGIBLE);\n        let backup_state = 0 != (bits & FLAG_BACKUP_STATE);\n\n        // If the current key is backup eligible, then we probably need to update one of the keys already stored in the database\n        // This is needed because Vaultwarden didn't store this information when using the previous version of webauthn-rs since it was a new addition to the protocol\n        // Because we store multiple keys in one json string, we need to fetch the correct key first, and update its information before we let it verify\n        if backup_eligible {\n            let rsp_id = rsp.raw_id.as_slice();\n            for reg in &mut *registrations {\n                if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) {\n                    if reg.set_backup_eligible(backup_eligible, backup_state) {\n                        // We also need to adjust the current state which holds the challenge used to start the authentication verification\n                        // Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update\n                        let mut raw_state = serde_json::to_value(&state)?;\n                        if let Some(credentials) = raw_state\n                            .get_mut(\"ast\")\n                            .and_then(|v| v.get_mut(\"credentials\"))\n                            .and_then(|v| v.as_array_mut())\n                        {\n                            for cred in credentials.iter_mut() {\n                                if cred.get(\"cred_id\").is_some_and(|v| {\n                                    // Deserialize to a [u8] so it can be compared using `ct_eq` with the `rsp_id`\n                                    let cred_id_slice: Base64UrlSafeData = serde_json::from_value(v.clone()).unwrap();\n                                    ct_eq(cred_id_slice, rsp_id)\n                                }) {\n                                    cred[\"backup_eligible\"] = Value::Bool(backup_eligible);\n                                    cred[\"backup_state\"] = Value::Bool(backup_state);\n                                }\n                            }\n                        }\n\n                        *state = serde_json::from_value(raw_state)?;\n                        return Ok(true);\n                    }\n                    break;\n                }\n            }\n        }\n    }\n    Ok(false)\n}\n"
  },
  {
    "path": "src/api/core/two_factor/yubikey.rs",
    "content": "use rocket::serde::json::Json;\nuse rocket::Route;\nuse serde_json::Value;\nuse yubico::{config::Config, verify_async};\n\nuse crate::{\n    api::{\n        core::{log_user_event, two_factor::_generate_recover_code},\n        EmptyResult, JsonResult, PasswordOrOtpData,\n    },\n    auth::Headers,\n    db::{\n        models::{EventType, TwoFactor, TwoFactorType},\n        DbConn,\n    },\n    error::{Error, MapResult},\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct EnableYubikeyData {\n    key1: Option<String>,\n    key2: Option<String>,\n    key3: Option<String>,\n    key4: Option<String>,\n    key5: Option<String>,\n    nfc: bool,\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\n#[derive(Deserialize, Serialize, Debug)]\n#[serde(rename_all = \"camelCase\")]\npub struct YubikeyMetadata {\n    #[serde(rename = \"keys\", alias = \"Keys\")]\n    keys: Vec<String>,\n    #[serde(rename = \"nfc\", alias = \"Nfc\")]\n    pub nfc: bool,\n}\n\nfn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {\n    let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5];\n\n    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()\n}\n\nfn jsonify_yubikeys(yubikeys: Vec<String>) -> Value {\n    let mut result = Value::Object(serde_json::Map::new());\n\n    for (i, key) in yubikeys.into_iter().enumerate() {\n        result[format!(\"Key{}\", i + 1)] = Value::String(key);\n    }\n\n    result\n}\n\nfn get_yubico_credentials() -> Result<(String, String), Error> {\n    if !CONFIG._enable_yubico() {\n        err!(\"Yubico support is disabled\");\n    }\n\n    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {\n        (Some(id), Some(secret)) => Ok((id, secret)),\n        _ => err!(\"`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled\"),\n    }\n}\n\nasync fn verify_yubikey_otp(otp: String) -> EmptyResult {\n    let (yubico_id, yubico_secret) = get_yubico_credentials()?;\n\n    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);\n\n    match CONFIG.yubico_server() {\n        Some(server) => verify_async(otp, config.set_api_hosts(vec![server])).await,\n        None => verify_async(otp, config).await,\n    }\n    .map_res(\"Failed to verify OTP\")\n}\n\n#[post(\"/two-factor/get-yubikey\", data = \"<data>\")]\nasync fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {\n    // Make sure the credentials are set\n    get_yubico_credentials()?;\n\n    let data: PasswordOrOtpData = data.into_inner();\n    let user = headers.user;\n\n    data.validate(&user, false, &conn).await?;\n\n    let user_id = &user.uuid;\n    let yubikey_type = TwoFactorType::YubiKey as i32;\n\n    let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &conn).await;\n\n    if let Some(r) = r {\n        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;\n\n        let mut result = jsonify_yubikeys(yubikey_metadata.keys);\n\n        result[\"enabled\"] = Value::Bool(true);\n        result[\"nfc\"] = Value::Bool(yubikey_metadata.nfc);\n        result[\"object\"] = Value::String(\"twoFactorU2f\".to_owned());\n\n        Ok(Json(result))\n    } else {\n        Ok(Json(json!({\n            \"enabled\": false,\n            \"object\": \"twoFactorU2f\",\n        })))\n    }\n}\n\n#[post(\"/two-factor/yubikey\", data = \"<data>\")]\nasync fn activate_yubikey(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {\n    let data: EnableYubikeyData = data.into_inner();\n    let mut user = headers.user;\n\n    PasswordOrOtpData {\n        master_password_hash: data.master_password_hash.clone(),\n        otp: data.otp.clone(),\n    }\n    .validate(&user, true, &conn)\n    .await?;\n\n    // Check if we already have some data\n    let mut yubikey_data =\n        match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn).await {\n            Some(data) => data,\n            None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),\n        };\n\n    let yubikeys = parse_yubikeys(&data);\n\n    if yubikeys.is_empty() {\n        return Ok(Json(json!({\n            \"enabled\": false,\n            \"object\": \"twoFactorU2f\",\n        })));\n    }\n\n    // Ensure they are valid OTPs\n    for yubikey in &yubikeys {\n        if yubikey.is_empty() || yubikey.len() == 12 {\n            continue;\n        }\n\n        verify_yubikey_otp(yubikey.to_owned()).await.map_res(\"Invalid Yubikey OTP provided\")?;\n    }\n\n    let yubikey_ids: Vec<String> = yubikeys.into_iter().filter_map(|x| x.get(..12).map(str::to_owned)).collect();\n\n    let yubikey_metadata = YubikeyMetadata {\n        keys: yubikey_ids,\n        nfc: data.nfc,\n    };\n\n    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();\n    yubikey_data.save(&conn).await?;\n\n    _generate_recover_code(&mut user, &conn).await;\n\n    log_user_event(EventType::UserUpdated2fa as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &conn).await;\n\n    let mut result = jsonify_yubikeys(yubikey_metadata.keys);\n\n    result[\"enabled\"] = Value::Bool(true);\n    result[\"nfc\"] = Value::Bool(yubikey_metadata.nfc);\n    result[\"object\"] = Value::String(\"twoFactorU2f\".to_owned());\n\n    Ok(Json(result))\n}\n\n#[put(\"/two-factor/yubikey\", data = \"<data>\")]\nasync fn activate_yubikey_put(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {\n    activate_yubikey(data, headers, conn).await\n}\n\npub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {\n    if response.len() != 44 {\n        err!(\"Invalid Yubikey OTP length\");\n    }\n\n    let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect(\"Can't parse Yubikey Metadata\");\n    let response_id = &response[..12];\n\n    if !yubikey_metadata.keys.contains(&response_id.to_owned()) {\n        err!(\"Given Yubikey is not registered\");\n    }\n\n    verify_yubikey_otp(response.to_owned()).await.map_res(\"Failed to verify Yubikey against OTP server\")?;\n    Ok(())\n}\n"
  },
  {
    "path": "src/api/icons.rs",
    "content": "use std::{\n    collections::HashMap,\n    net::IpAddr,\n    sync::{Arc, LazyLock},\n    time::{Duration, SystemTime},\n};\n\nuse bytes::{Bytes, BytesMut};\nuse futures::{stream::StreamExt, TryFutureExt};\nuse html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer};\nuse regex::Regex;\nuse reqwest::{\n    header::{self, HeaderMap, HeaderValue},\n    Client, Response,\n};\nuse rocket::{http::ContentType, response::Redirect, Route};\nuse svg_hush::{data_url_filter, Filter};\n\nuse crate::{\n    config::PathType,\n    error::Error,\n    http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError},\n    util::Cached,\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    match CONFIG.icon_service().as_str() {\n        \"internal\" => routes![icon_internal],\n        _ => routes![icon_external],\n    }\n}\n\nstatic CLIENT: LazyLock<Client> = LazyLock::new(|| {\n    // Generate the default headers\n    let mut default_headers = HeaderMap::new();\n    default_headers.insert(\n        header::USER_AGENT,\n        HeaderValue::from_static(\n            \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36\",\n        ),\n    );\n    default_headers.insert(header::ACCEPT, HeaderValue::from_static(\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\"));\n    default_headers.insert(header::ACCEPT_LANGUAGE, HeaderValue::from_static(\"en-US,en;q=0.9\"));\n    default_headers.insert(header::CACHE_CONTROL, HeaderValue::from_static(\"no-cache\"));\n    default_headers.insert(header::PRAGMA, HeaderValue::from_static(\"no-cache\"));\n    default_headers.insert(header::UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static(\"1\"));\n\n    default_headers.insert(\"Sec-Ch-Ua-Mobile\", HeaderValue::from_static(\"?0\"));\n    default_headers.insert(\"Sec-Ch-Ua-Platform\", HeaderValue::from_static(\"Linux\"));\n    default_headers.insert(\n        \"Sec-Ch-Ua\",\n        HeaderValue::from_static(\"\\\"Not)A;Brand\\\";v=\\\"8\\\", \\\"Chromium\\\";v=\\\"138\\\", \\\"Google Chrome\\\";v=\\\"138\\\"\"),\n    );\n\n    default_headers.insert(\"Sec-Fetch-Site\", HeaderValue::from_static(\"none\"));\n    default_headers.insert(\"Sec-Fetch-Mode\", HeaderValue::from_static(\"navigate\"));\n    default_headers.insert(\"Sec-Fetch-User\", HeaderValue::from_static(\"?1\"));\n    default_headers.insert(\"Sec-Fetch-Dest\", HeaderValue::from_static(\"document\"));\n\n    // Generate the cookie store\n    let cookie_store = Arc::new(Jar::default());\n\n    let icon_download_timeout = Duration::from_secs(CONFIG.icon_download_timeout());\n    let pool_idle_timeout = Duration::from_secs(10);\n    // Reuse the client between requests\n    get_reqwest_client_builder()\n        .cookie_provider(Arc::clone(&cookie_store))\n        .timeout(icon_download_timeout)\n        .pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections\n        .pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds\n        .default_headers(default_headers.clone())\n        .http1_title_case_headers()\n        .build()\n        .expect(\"Failed to build client\")\n});\n\n// Build Regex only once since this takes a lot of time.\nstatic ICON_SIZE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r\"(?x)(\\d+)\\D*(\\d+)\").unwrap());\n\n// The function name `icon_external` is checked in the `on_response` function in `AppHeaders`\n// It is used to prevent sending a specific header which breaks icon downloads.\n// If this function needs to be renamed, also adjust the code in `util.rs`\n#[get(\"/<domain>/icon.png\")]\nfn icon_external(domain: &str) -> Cached<Option<Redirect>> {\n    if !is_valid_domain(domain) {\n        warn!(\"Invalid domain: {domain}\");\n        return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);\n    }\n\n    if should_block_address(domain) {\n        warn!(\"Blocked address: {domain}\");\n        return Cached::ttl(None, CONFIG.icon_cache_negttl(), true);\n    }\n\n    let url = CONFIG._icon_service_url().replace(\"{}\", domain);\n    let redir = match CONFIG.icon_redirect_code() {\n        301 => Some(Redirect::moved(url)), // legacy permanent redirect\n        302 => Some(Redirect::found(url)), // legacy temporary redirect\n        307 => Some(Redirect::temporary(url)),\n        308 => Some(Redirect::permanent(url)),\n        _ => {\n            error!(\"Unexpected redirect code {}\", CONFIG.icon_redirect_code());\n            None\n        }\n    };\n    Cached::ttl(redir, CONFIG.icon_cache_ttl(), true)\n}\n\n#[get(\"/<domain>/icon.png\")]\nasync fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {\n    const FALLBACK_ICON: &[u8] = include_bytes!(\"../static/images/fallback-icon.png\");\n\n    if !is_valid_domain(domain) {\n        warn!(\"Invalid domain: {domain}\");\n        return Cached::ttl(\n            (ContentType::new(\"image\", \"png\"), FALLBACK_ICON.to_vec()),\n            CONFIG.icon_cache_negttl(),\n            true,\n        );\n    }\n\n    if should_block_address(domain) {\n        warn!(\"Blocked address: {domain}\");\n        return Cached::ttl(\n            (ContentType::new(\"image\", \"png\"), FALLBACK_ICON.to_vec()),\n            CONFIG.icon_cache_negttl(),\n            true,\n        );\n    }\n\n    match get_icon(domain).await {\n        Some((icon, icon_type)) => {\n            Cached::ttl((ContentType::new(\"image\", icon_type), icon), CONFIG.icon_cache_ttl(), true)\n        }\n        _ => Cached::ttl((ContentType::new(\"image\", \"png\"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl(), true),\n    }\n}\n\n/// Returns if the domain provided is valid or not.\n///\n/// This does some manual checks and makes use of Url to do some basic checking.\n/// domains can't be larger then 63 characters (not counting multiple subdomains) according to the RFC's, but we limit the total size to 255.\nfn is_valid_domain(domain: &str) -> bool {\n    const ALLOWED_CHARS: &str = \"-.\";\n\n    // If parsing the domain fails using Url, it will not work with reqwest.\n    if let Err(parse_error) = url::Url::parse(format!(\"https://{domain}\").as_str()) {\n        debug!(\"Domain parse error: '{domain}' - {parse_error:?}\");\n        return false;\n    } else if domain.is_empty()\n        || domain.contains(\"..\")\n        || domain.starts_with('.')\n        || domain.starts_with('-')\n        || domain.ends_with('-')\n    {\n        debug!(\n            \"Domain validation error: '{domain}' is either empty, contains '..', starts with an '.', starts or ends with a '-'\"\n        );\n        return false;\n    } else if domain.len() > 255 {\n        debug!(\"Domain validation error: '{domain}' exceeds 255 characters\");\n        return false;\n    }\n\n    for c in domain.chars() {\n        if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {\n            debug!(\"Domain validation error: '{domain}' contains an invalid character '{c}'\");\n            return false;\n        }\n    }\n\n    true\n}\n\nasync fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {\n    let path = format!(\"{domain}.png\");\n\n    // Check for expiration of negatively cached copy\n    if icon_is_negcached(&path).await {\n        return None;\n    }\n\n    if let Some(icon) = get_cached_icon(&path).await {\n        let icon_type = get_icon_type(&icon).unwrap_or(\"x-icon\");\n        return Some((icon, icon_type.to_string()));\n    }\n\n    if CONFIG.disable_icon_download() {\n        return None;\n    }\n\n    // Get the icon, or None in case of error\n    match download_icon(domain).await {\n        Ok((icon, icon_type)) => {\n            save_icon(&path, icon.to_vec()).await;\n            Some((icon.to_vec(), icon_type.unwrap_or(\"x-icon\").to_string()))\n        }\n        Err(e) => {\n            // If this error comes from the custom resolver, this means this is a blocked domain\n            // or non global IP, don't save the miss file in this case to avoid leaking it\n            if let Some(error) = CustomHttpClientError::downcast_ref(&e) {\n                warn!(\"{error}\");\n                return None;\n            }\n\n            warn!(\"Unable to download icon: {e:?}\");\n            let miss_indicator = path + \".miss\";\n            save_icon(&miss_indicator, vec![]).await;\n            None\n        }\n    }\n}\n\nasync fn get_cached_icon(path: &str) -> Option<Vec<u8>> {\n    // Check for expiration of successfully cached copy\n    if icon_is_expired(path).await {\n        return None;\n    }\n\n    // Try to read the cached icon, and return it if it exists\n    if let Ok(operator) = CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {\n        if let Ok(buf) = operator.read(path).await {\n            return Some(buf.to_vec());\n        }\n    }\n\n    None\n}\n\nasync fn file_is_expired(path: &str, ttl: u64) -> Result<bool, Error> {\n    let operator = CONFIG.opendal_operator_for_path_type(&PathType::IconCache)?;\n    let meta = operator.stat(path).await?;\n    let modified =\n        meta.last_modified().ok_or_else(|| std::io::Error::other(format!(\"No last modified time for `{path}`\")))?;\n    let age = SystemTime::now().duration_since(modified.into())?;\n\n    Ok(ttl > 0 && ttl <= age.as_secs())\n}\n\nasync fn icon_is_negcached(path: &str) -> bool {\n    let miss_indicator = path.to_owned() + \".miss\";\n    let expired = file_is_expired(&miss_indicator, CONFIG.icon_cache_negttl()).await;\n\n    match expired {\n        // No longer negatively cached, drop the marker\n        Ok(true) => {\n            match CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {\n                Ok(operator) => {\n                    if let Err(e) = operator.delete(&miss_indicator).await {\n                        error!(\"Could not remove negative cache indicator for icon {path:?}: {e:?}\");\n                    }\n                }\n                Err(e) => error!(\"Could not remove negative cache indicator for icon {path:?}: {e:?}\"),\n            }\n            false\n        }\n        // The marker hasn't expired yet.\n        Ok(false) => true,\n        // The marker is missing or inaccessible in some way.\n        Err(_) => false,\n    }\n}\n\nasync fn icon_is_expired(path: &str) -> bool {\n    let expired = file_is_expired(path, CONFIG.icon_cache_ttl()).await;\n    expired.unwrap_or(true)\n}\n\nstruct Icon {\n    priority: u8,\n    href: String,\n}\n\nimpl Icon {\n    const fn new(priority: u8, href: String) -> Self {\n        Self {\n            priority,\n            href,\n        }\n    }\n}\n\nfn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &mut Vec<Icon>, url: &url::Url) {\n    const TAG_LINK: &[u8] = b\"link\";\n    const TAG_BASE: &[u8] = b\"base\";\n    const TAG_HEAD: &[u8] = b\"head\";\n    const ATTR_HREF: &[u8] = b\"href\";\n    const ATTR_SIZES: &[u8] = b\"sizes\";\n\n    let mut base_url = url.clone();\n    let mut icon_tags: Vec<Tag> = Vec::new();\n    for Ok(token) in dom {\n        let tag_name: &[u8] = &token.tag.name;\n        match tag_name {\n            TAG_LINK => {\n                icon_tags.push(token.tag);\n            }\n            TAG_BASE => {\n                base_url = if let Some(href) = token.tag.attributes.get(ATTR_HREF) {\n                    let href = std::str::from_utf8(href).unwrap_or_default();\n                    debug!(\"Found base href: {href}\");\n                    match base_url.join(href) {\n                        Ok(inner_url) => inner_url,\n                        _ => continue,\n                    }\n                } else {\n                    continue;\n                };\n            }\n            TAG_HEAD if token.closing => {\n                break;\n            }\n            _ => {}\n        }\n    }\n\n    for icon_tag in icon_tags {\n        if let Some(icon_href) = icon_tag.attributes.get(ATTR_HREF) {\n            if let Ok(full_href) = base_url.join(std::str::from_utf8(icon_href).unwrap_or_default()) {\n                let sizes = if let Some(v) = icon_tag.attributes.get(ATTR_SIZES) {\n                    std::str::from_utf8(v).unwrap_or_default()\n                } else {\n                    \"\"\n                };\n                let priority = get_icon_priority(full_href.as_str(), sizes);\n                icons.push(Icon::new(priority, full_href.to_string()));\n            }\n        };\n    }\n}\n\nstruct IconUrlResult {\n    iconlist: Vec<Icon>,\n    referer: String,\n}\n\n/// Returns a IconUrlResult which holds a Vector IconList and a string which holds the referer.\n/// There will always two items within the iconlist which holds http(s)://domain.tld/favicon.ico.\n/// This does not mean that location exists, but (it) is the default location the browser uses.\n///\n/// # Argument\n/// * `domain` - A string which holds the domain with extension.\n///\n/// # Example\n/// ```\n/// let icon_result = get_icon_url(\"github.com\").await?;\n/// let icon_result = get_icon_url(\"vaultwarden.discourse.group\").await?;\n/// ```\nasync fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {\n    // Default URL with secure and insecure schemes\n    let ssldomain = format!(\"https://{domain}\");\n    let httpdomain = format!(\"http://{domain}\");\n\n    // First check the domain as given during the request for HTTPS.\n    let resp = match get_page(&ssldomain).await {\n        Err(e) if CustomHttpClientError::downcast_ref(&e).is_none() => {\n            // If we get an error that is not caused by the blacklist, we retry with HTTP\n            match get_page(&httpdomain).await {\n                mut sub_resp @ Err(_) => {\n                    // When the domain is not an IP, and has more then one dot, remove all subdomains.\n                    let is_ip = domain.parse::<IpAddr>();\n                    if is_ip.is_err() && domain.matches('.').count() > 1 {\n                        let mut domain_parts = domain.split('.');\n                        let base_domain = format!(\n                            \"{base}.{tld}\",\n                            tld = domain_parts.next_back().unwrap(),\n                            base = domain_parts.next_back().unwrap()\n                        );\n                        if is_valid_domain(&base_domain) {\n                            let sslbase = format!(\"https://{base_domain}\");\n                            let httpbase = format!(\"http://{base_domain}\");\n                            debug!(\"[get_icon_url]: Trying without subdomains '{base_domain}'\");\n\n                            sub_resp = get_page(&sslbase).or_else(|_| get_page(&httpbase)).await;\n                        }\n\n                    // When the domain is not an IP, and has less then 2 dots, try to add www. infront of it.\n                    } else if is_ip.is_err() && domain.matches('.').count() < 2 {\n                        let www_domain = format!(\"www.{domain}\");\n                        if is_valid_domain(&www_domain) {\n                            let sslwww = format!(\"https://{www_domain}\");\n                            let httpwww = format!(\"http://{www_domain}\");\n                            debug!(\"[get_icon_url]: Trying with www. prefix '{www_domain}'\");\n\n                            sub_resp = get_page(&sslwww).or_else(|_| get_page(&httpwww)).await;\n                        }\n                    }\n                    sub_resp\n                }\n                res => res,\n            }\n        }\n\n        // If we get a result or a blacklist error, just continue\n        res => res,\n    };\n\n    // Create the iconlist\n    let mut iconlist: Vec<Icon> = Vec::new();\n    let mut referer = String::new();\n\n    if let Ok(content) = resp {\n        // Extract the URL from the response in case redirects occurred (like @ gitlab.com)\n        let url = content.url().clone();\n\n        // Set the referer to be used on the final request, some sites check this.\n        // Mostly used to prevent direct linking and other security reasons.\n        referer = url.to_string();\n\n        // Add the fallback favicon.ico and apple-touch-icon.png to the list with the domain the content responded from.\n        iconlist.push(Icon::new(35, String::from(url.join(\"/favicon.ico\").unwrap())));\n        iconlist.push(Icon::new(40, String::from(url.join(\"/apple-touch-icon.png\").unwrap())));\n\n        // 384KB should be more than enough for the HTML, though as we only really need the HTML header.\n        let limited_reader = stream_to_bytes_limit(content, 384 * 1024).await?.to_vec();\n\n        let dom = Tokenizer::new_with_emitter(limited_reader.to_reader(), FaviconEmitter::default());\n        get_favicons_node(dom, &mut iconlist, &url);\n    } else {\n        // Add the default favicon.ico to the list with just the given domain\n        iconlist.push(Icon::new(35, format!(\"{ssldomain}/favicon.ico\")));\n        iconlist.push(Icon::new(40, format!(\"{ssldomain}/apple-touch-icon.png\")));\n        iconlist.push(Icon::new(35, format!(\"{httpdomain}/favicon.ico\")));\n        iconlist.push(Icon::new(40, format!(\"{httpdomain}/apple-touch-icon.png\")));\n    }\n\n    // Sort the iconlist by priority\n    iconlist.sort_by_key(|x| x.priority);\n\n    // There always is an icon in the list, so no need to check if it exists, and just return the first one\n    Ok(IconUrlResult {\n        iconlist,\n        referer,\n    })\n}\n\nasync fn get_page(url: &str) -> Result<Response, Error> {\n    get_page_with_referer(url, \"\").await\n}\n\nasync fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {\n    let mut client = CLIENT.get(url);\n    if !referer.is_empty() {\n        client = client.header(\"Referer\", referer)\n    }\n\n    Ok(client.send().await?.error_for_status()?)\n}\n\n/// Returns a Integer with the priority of the type of the icon which to prefer.\n/// The lower the number the better.\n///\n/// # Arguments\n/// * `href`  - A string which holds the href value or relative path.\n/// * `sizes` - The size of the icon if available as a <width>x<height> value like 32x32.\n///\n/// # Example\n/// ```\n/// priority1 = get_icon_priority(\"http://example.com/path/to/a/favicon.png\", \"32x32\");\n/// priority2 = get_icon_priority(\"https://example.com/path/to/a/favicon.ico\", \"\");\n/// ```\nfn get_icon_priority(href: &str, sizes: &str) -> u8 {\n    static PRIORITY_MAP: LazyLock<HashMap<&'static str, u8>> =\n        LazyLock::new(|| [(\".png\", 10), (\".jpg\", 20), (\".jpeg\", 20)].into_iter().collect());\n\n    // Check if there is a dimension set\n    let (width, height) = parse_sizes(sizes);\n\n    // Check if there is a size given\n    if width != 0 && height != 0 {\n        // Only allow square dimensions\n        if width == height {\n            // Change priority by given size\n            if width == 32 {\n                1\n            } else if width == 64 {\n                2\n            } else if (24..=192).contains(&width) {\n                3\n            } else if width == 16 {\n                4\n            } else {\n                5\n            }\n        // There are dimensions available, but the image is not a square\n        } else {\n            200\n        }\n    } else {\n        match href.rsplit_once('.') {\n            Some((_, extension)) => PRIORITY_MAP.get(&*extension.to_ascii_lowercase()).copied().unwrap_or(30),\n            None => 30,\n        }\n    }\n}\n\n/// Returns a Tuple with the width and height as a separate value extracted from the sizes attribute\n/// It will return 0 for both values if no match has been found.\n///\n/// # Arguments\n/// * `sizes` - The size of the icon if available as a <width>x<height> value like 32x32.\n///\n/// # Example\n/// ```\n/// let (width, height) = parse_sizes(\"64x64\"); // (64, 64)\n/// let (width, height) = parse_sizes(\"x128x128\"); // (128, 128)\n/// let (width, height) = parse_sizes(\"32\"); // (0, 0)\n/// ```\nfn parse_sizes(sizes: &str) -> (u16, u16) {\n    let mut width: u16 = 0;\n    let mut height: u16 = 0;\n\n    if !sizes.is_empty() {\n        match ICON_SIZE_REGEX.captures(sizes.trim()) {\n            Some(dimensions) if dimensions.len() >= 3 => {\n                width = dimensions[1].parse::<u16>().unwrap_or_default();\n                height = dimensions[2].parse::<u16>().unwrap_or_default();\n            }\n            _ => {}\n        }\n    }\n\n    (width, height)\n}\n\nasync fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {\n    let icon_result = get_icon_url(domain).await?;\n\n    let mut buffer = Bytes::new();\n    let mut icon_type: Option<&str> = None;\n\n    use data_url::DataUrl;\n\n    for icon in icon_result.iconlist.iter().take(5) {\n        if icon.href.starts_with(\"data:image\") {\n            let Ok(datauri) = DataUrl::process(&icon.href) else {\n                continue;\n            };\n            // Check if we are able to decode the data uri\n            let mut body = BytesMut::new();\n            match datauri.decode::<_, ()>(|bytes| {\n                body.extend_from_slice(bytes);\n                Ok(())\n            }) {\n                Ok(_) => {\n                    // Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create\n                    if body.len() >= 67 {\n                        // Check if the icon type is allowed, else try an icon from the list.\n                        icon_type = get_icon_type(&body);\n                        if icon_type.is_none() {\n                            debug!(\"Icon from {domain} data:image uri, is not a valid image type\");\n                            continue;\n                        }\n                        info!(\"Extracted icon from data:image uri for {domain}\");\n                        buffer = body.freeze();\n                        break;\n                    }\n                }\n                _ => debug!(\"Extracted icon from data:image uri is invalid\"),\n            };\n        } else {\n            let res = get_page_with_referer(&icon.href, &icon_result.referer).await?;\n\n            buffer = stream_to_bytes_limit(res, 5120 * 1024).await?; // 5120KB/5MB for each icon max (Same as icons.bitwarden.net)\n\n            // Check if the icon type is allowed, else try an icon from the list.\n            icon_type = get_icon_type(&buffer);\n            if icon_type.is_none() {\n                buffer.clear();\n                debug!(\"Icon from {}, is not a valid image type\", icon.href);\n                continue;\n            }\n            info!(\"Downloaded icon from {}\", icon.href);\n            break;\n        }\n    }\n\n    if buffer.is_empty() {\n        err_silent!(\"Empty response or unable find a valid icon\", domain);\n    } else if icon_type == Some(\"svg+xml\") {\n        let mut svg_filter = Filter::new();\n        svg_filter.set_data_url_filter(data_url_filter::allow_standard_images);\n        let mut sanitized_svg = Vec::new();\n        if svg_filter.filter(&*buffer, &mut sanitized_svg).is_err() {\n            icon_type = None;\n            buffer.clear();\n        } else {\n            buffer = sanitized_svg.into();\n        }\n    }\n\n    Ok((buffer, icon_type))\n}\n\nasync fn save_icon(path: &str, icon: Vec<u8>) {\n    let operator = match CONFIG.opendal_operator_for_path_type(&PathType::IconCache) {\n        Ok(operator) => operator,\n        Err(e) => {\n            warn!(\"Failed to get OpenDAL operator while saving icon: {e}\");\n            return;\n        }\n    };\n\n    if let Err(e) = operator.write(path, icon).await {\n        warn!(\"Unable to save icon: {e:?}\");\n    }\n}\n\nfn get_icon_type(bytes: &[u8]) -> Option<&'static str> {\n    fn check_svg_after_xml_declaration(bytes: &[u8]) -> Option<&'static str> {\n        // Look for SVG tag within the first 1KB\n        if let Ok(content) = std::str::from_utf8(&bytes[..bytes.len().min(1024)]) {\n            if content.contains(\"<svg\") || content.contains(\"<SVG\") {\n                return Some(\"svg+xml\");\n            }\n        }\n        None\n    }\n\n    match bytes {\n        [137, 80, 78, 71, ..] => Some(\"png\"),\n        [0, 0, 1, 0, ..] => Some(\"x-icon\"),\n        [82, 73, 70, 70, ..] => Some(\"webp\"),\n        [255, 216, 255, ..] => Some(\"jpeg\"),\n        [71, 73, 70, 56, ..] => Some(\"gif\"),\n        [66, 77, ..] => Some(\"bmp\"),\n        [60, 115, 118, 103, ..] => Some(\"svg+xml\"), // Normal svg\n        [60, 63, 120, 109, 108, ..] => check_svg_after_xml_declaration(bytes), // An svg starting with <?xml\n        _ => None,\n    }\n}\n\n/// Minimize the amount of bytes to be parsed from a reqwest result.\n/// This prevents very long parsing and memory usage.\nasync fn stream_to_bytes_limit(res: Response, max_size: usize) -> Result<Bytes, reqwest::Error> {\n    let mut stream = res.bytes_stream().take(max_size);\n    let mut buf = BytesMut::new();\n    let mut size = 0;\n    while let Some(chunk) = stream.next().await {\n        // It is possible that there might occur UnexpectedEof errors or others\n        // This is most of the time no issue, and if there is no chunked data anymore or at all parsing the HTML will not happen anyway.\n        // Therefore if chunk is an err, just break and continue with the data be have received.\n        if chunk.is_err() {\n            break;\n        }\n        let chunk = &chunk?;\n        size += chunk.len();\n        buf.extend(chunk);\n        if size >= max_size {\n            break;\n        }\n    }\n    Ok(buf.freeze())\n}\n\n/// This is an implementation of the default Cookie Jar from Reqwest and reqwest_cookie_store build by pfernie.\n/// The default cookie jar used by Reqwest keeps all the cookies based upon the Max-Age or Expires which could be a long time.\n/// That could be used for tracking, to prevent this we force the lifespan of the cookies to always be max two minutes.\n/// A Cookie Jar is needed because some sites force a redirect with cookies to verify if a request uses cookies or not.\nuse cookie_store::CookieStore;\n#[derive(Default)]\npub struct Jar(std::sync::RwLock<CookieStore>);\n\nimpl reqwest::cookie::CookieStore for Jar {\n    fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &url::Url) {\n        use cookie::{Cookie as RawCookie, ParseError as RawCookieParseError};\n        use time::Duration;\n\n        let mut cookie_store = self.0.write().unwrap();\n        let cookies = cookie_headers.filter_map(|val| {\n            std::str::from_utf8(val.as_bytes())\n                .map_err(RawCookieParseError::from)\n                .and_then(RawCookie::parse)\n                .map(|mut c| {\n                    c.set_expires(None);\n                    c.set_max_age(Some(Duration::minutes(2)));\n                    c.into_owned()\n                })\n                .ok()\n        });\n        cookie_store.store_response_cookies(cookies, url);\n    }\n\n    fn cookies(&self, url: &url::Url) -> Option<HeaderValue> {\n        let cookie_store = self.0.read().unwrap();\n        let s = cookie_store\n            .get_request_values(url)\n            .map(|(name, value)| format!(\"{name}={value}\"))\n            .collect::<Vec<_>>()\n            .join(\"; \");\n\n        if s.is_empty() {\n            return None;\n        }\n\n        HeaderValue::from_maybe_shared(Bytes::from(s)).ok()\n    }\n}\n\n/// Custom FaviconEmitter for the html5gum parser.\n/// The FaviconEmitter is using an optimized version of the DefaultEmitter.\n/// This prevents emitting tags like comments, doctype and also strings between the tags.\n/// But it will also only emit the tags we need and only if they have the correct attributes\n/// Therefore parsing the HTML content is faster.\nuse std::collections::BTreeMap;\n\n#[derive(Default)]\npub struct Tag {\n    /// The tag's name, such as `\"link\"` or `\"base\"`.\n    pub name: HtmlString,\n\n    /// A mapping for any HTML attributes this start tag may have.\n    ///\n    /// Duplicate attributes are ignored after the first one as per WHATWG spec.\n    pub attributes: BTreeMap<HtmlString, HtmlString>,\n}\n\nstruct FaviconToken {\n    tag: Tag,\n    closing: bool,\n}\n\n#[derive(Default)]\nstruct FaviconEmitter {\n    current_token: Option<FaviconToken>,\n    last_start_tag: HtmlString,\n    current_attribute: Option<(HtmlString, HtmlString)>,\n    emit_token: bool,\n}\n\nimpl FaviconEmitter {\n    fn flush_current_attribute(&mut self, emit_current_tag: bool) {\n        const ATTR_HREF: &[u8] = b\"href\";\n        const ATTR_REL: &[u8] = b\"rel\";\n        const TAG_LINK: &[u8] = b\"link\";\n        const TAG_BASE: &[u8] = b\"base\";\n        const TAG_HEAD: &[u8] = b\"head\";\n\n        if let Some(ref mut token) = self.current_token {\n            let tag_name: &[u8] = &token.tag.name;\n\n            if self.current_attribute.is_some() && (tag_name == TAG_BASE || tag_name == TAG_LINK) {\n                let (k, v) = self.current_attribute.take().unwrap();\n                token.tag.attributes.entry(k).and_modify(|_| {}).or_insert(v);\n            }\n\n            let tag_attr = &token.tag.attributes;\n            match tag_name {\n                TAG_HEAD if token.closing => self.emit_token = true,\n                TAG_BASE if tag_attr.contains_key(ATTR_HREF) => self.emit_token = true,\n                TAG_LINK if emit_current_tag && tag_attr.contains_key(ATTR_REL) && tag_attr.contains_key(ATTR_HREF) => {\n                    let rel_value =\n                        std::str::from_utf8(token.tag.attributes.get(ATTR_REL).unwrap()).unwrap_or_default();\n                    if rel_value.contains(\"icon\") && !rel_value.contains(\"mask-icon\") {\n                        self.emit_token = true\n                    }\n                }\n                _ => (),\n            }\n        }\n    }\n}\n\nimpl Emitter for FaviconEmitter {\n    type Token = FaviconToken;\n\n    fn set_last_start_tag(&mut self, last_start_tag: Option<&[u8]>) {\n        self.last_start_tag.clear();\n        self.last_start_tag.extend(last_start_tag.unwrap_or_default());\n    }\n\n    fn pop_token(&mut self) -> Option<Self::Token> {\n        if self.emit_token {\n            self.emit_token = false;\n            return self.current_token.take();\n        }\n        None\n    }\n\n    fn init_start_tag(&mut self) {\n        self.current_token = Some(FaviconToken {\n            tag: Tag::default(),\n            closing: false,\n        });\n    }\n\n    fn init_end_tag(&mut self) {\n        self.current_token = Some(FaviconToken {\n            tag: Tag::default(),\n            closing: true,\n        });\n    }\n\n    fn emit_current_tag(&mut self) -> Option<html5gum::State> {\n        self.flush_current_attribute(true);\n        self.last_start_tag.clear();\n        match &self.current_token {\n            Some(token) if !token.closing => {\n                self.last_start_tag.extend(&*token.tag.name);\n            }\n            _ => {}\n        }\n        html5gum::naive_next_state(&self.last_start_tag)\n    }\n\n    fn push_tag_name(&mut self, s: &[u8]) {\n        if let Some(ref mut token) = self.current_token {\n            token.tag.name.extend(s);\n        }\n    }\n\n    fn init_attribute(&mut self) {\n        self.flush_current_attribute(false);\n        self.current_attribute = match &self.current_token {\n            Some(token) => {\n                let tag_name: &[u8] = &token.tag.name;\n                match tag_name {\n                    b\"link\" | b\"head\" | b\"base\" => Some(Default::default()),\n                    _ => None,\n                }\n            }\n            _ => None,\n        };\n    }\n\n    fn push_attribute_name(&mut self, s: &[u8]) {\n        if let Some(attr) = &mut self.current_attribute {\n            attr.0.extend(s)\n        }\n    }\n\n    fn push_attribute_value(&mut self, s: &[u8]) {\n        if let Some(attr) = &mut self.current_attribute {\n            attr.1.extend(s)\n        }\n    }\n\n    fn current_is_appropriate_end_tag_token(&mut self) -> bool {\n        match &self.current_token {\n            Some(token) if token.closing => !self.last_start_tag.is_empty() && self.last_start_tag == token.tag.name,\n            _ => false,\n        }\n    }\n\n    // We do not want and need these parts of the HTML document\n    // These will be skipped and ignored during the tokenization and iteration.\n    fn emit_current_comment(&mut self) {}\n    fn emit_current_doctype(&mut self) {}\n    fn emit_eof(&mut self) {}\n    fn emit_error(&mut self, _: html5gum::Error) {}\n    fn emit_string(&mut self, _: &[u8]) {}\n    fn init_comment(&mut self) {}\n    fn init_doctype(&mut self) {}\n    fn push_comment(&mut self, _: &[u8]) {}\n    fn push_doctype_name(&mut self, _: &[u8]) {}\n    fn push_doctype_public_identifier(&mut self, _: &[u8]) {}\n    fn push_doctype_system_identifier(&mut self, _: &[u8]) {}\n    fn set_doctype_public_identifier(&mut self, _: &[u8]) {}\n    fn set_doctype_system_identifier(&mut self, _: &[u8]) {}\n    fn set_force_quirks(&mut self) {}\n    fn set_self_closing(&mut self) {}\n}\n"
  },
  {
    "path": "src/api/identity.rs",
    "content": "use chrono::Utc;\nuse num_traits::FromPrimitive;\nuse rocket::{\n    form::{Form, FromForm},\n    http::Status,\n    response::Redirect,\n    serde::json::Json,\n    Route,\n};\nuse serde_json::Value;\n\nuse crate::{\n    api::{\n        core::{\n            accounts::{PreloginData, RegisterData, _prelogin, _register, kdf_upgrade},\n            log_user_event,\n            two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},\n        },\n        master_password_policy,\n        push::register_push_device,\n        ApiResult, EmptyResult, JsonResult,\n    },\n    auth,\n    auth::{generate_organization_api_key_login_claims, AuthMethod, ClientHeaders, ClientIp, ClientVersion},\n    db::{\n        models::{\n            AuthRequest, AuthRequestId, Device, DeviceId, EventType, Invitation, OIDCCodeWrapper, OrganizationApiKey,\n            OrganizationId, SsoAuth, SsoUser, TwoFactor, TwoFactorIncomplete, TwoFactorType, User, UserId,\n        },\n        DbConn,\n    },\n    error::MapResult,\n    mail, sso,\n    sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},\n    util, CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    routes![\n        login,\n        prelogin,\n        identity_register,\n        register_verification_email,\n        register_finish,\n        prevalidate,\n        authorize,\n        oidcsignin,\n        oidcsignin_error\n    ]\n}\n\n#[post(\"/connect/token\", data = \"<data>\")]\nasync fn login(\n    data: Form<ConnectData>,\n    client_header: ClientHeaders,\n    client_version: Option<ClientVersion>,\n    conn: DbConn,\n) -> JsonResult {\n    let data: ConnectData = data.into_inner();\n\n    let mut user_id: Option<UserId> = None;\n\n    let login_result = match data.grant_type.as_ref() {\n        \"refresh_token\" => {\n            _check_is_some(&data.refresh_token, \"refresh_token cannot be blank\")?;\n            _refresh_login(data, &conn, &client_header.ip).await\n        }\n        \"password\" if CONFIG.sso_enabled() && CONFIG.sso_only() => err!(\"SSO sign-in is required\"),\n        \"password\" => {\n            _check_is_some(&data.client_id, \"client_id cannot be blank\")?;\n            _check_is_some(&data.password, \"password cannot be blank\")?;\n            _check_is_some(&data.scope, \"scope cannot be blank\")?;\n            _check_is_some(&data.username, \"username cannot be blank\")?;\n\n            _check_is_some(&data.device_identifier, \"device_identifier cannot be blank\")?;\n            _check_is_some(&data.device_name, \"device_name cannot be blank\")?;\n            _check_is_some(&data.device_type, \"device_type cannot be blank\")?;\n\n            _password_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await\n        }\n        \"client_credentials\" => {\n            _check_is_some(&data.client_id, \"client_id cannot be blank\")?;\n            _check_is_some(&data.client_secret, \"client_secret cannot be blank\")?;\n            _check_is_some(&data.scope, \"scope cannot be blank\")?;\n\n            _check_is_some(&data.device_identifier, \"device_identifier cannot be blank\")?;\n            _check_is_some(&data.device_name, \"device_name cannot be blank\")?;\n            _check_is_some(&data.device_type, \"device_type cannot be blank\")?;\n\n            _api_key_login(data, &mut user_id, &conn, &client_header.ip).await\n        }\n        \"authorization_code\" if CONFIG.sso_enabled() => {\n            _check_is_some(&data.client_id, \"client_id cannot be blank\")?;\n            _check_is_some(&data.code, \"code cannot be blank\")?;\n            _check_is_some(&data.code_verifier, \"code verifier cannot be blank\")?;\n\n            _check_is_some(&data.device_identifier, \"device_identifier cannot be blank\")?;\n            _check_is_some(&data.device_name, \"device_name cannot be blank\")?;\n            _check_is_some(&data.device_type, \"device_type cannot be blank\")?;\n\n            _sso_login(data, &mut user_id, &conn, &client_header.ip, &client_version).await\n        }\n        \"authorization_code\" => err!(\"SSO sign-in is not available\"),\n        t => err!(\"Invalid type\", t),\n    };\n\n    if let Some(user_id) = user_id {\n        match &login_result {\n            Ok(_) => {\n                log_user_event(\n                    EventType::UserLoggedIn as i32,\n                    &user_id,\n                    client_header.device_type,\n                    &client_header.ip.ip,\n                    &conn,\n                )\n                .await;\n            }\n            Err(e) => {\n                if let Some(ev) = e.get_event() {\n                    log_user_event(ev.event as i32, &user_id, client_header.device_type, &client_header.ip.ip, &conn)\n                        .await\n                }\n            }\n        }\n    }\n\n    login_result\n}\n\n// Return Status::Unauthorized to trigger logout\nasync fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {\n    // Extract token\n    let refresh_token = match data.refresh_token {\n        Some(token) => token,\n        None => err_code!(\"Missing refresh_token\", Status::Unauthorized.code),\n    };\n\n    // ---\n    // Disabled this variable, it was used to generate the JWT\n    // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out\n    // See: https://github.com/dani-garcia/vaultwarden/issues/4156\n    // ---\n    // let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;\n    match auth::refresh_tokens(ip, &refresh_token, data.client_id, conn).await {\n        Err(err) => {\n            err_code!(format!(\"Unable to refresh login credentials: {}\", err.message()), Status::Unauthorized.code)\n        }\n        Ok((mut device, auth_tokens)) => {\n            // Save to update `device.updated_at` to track usage and toggle new status\n            device.save(true, conn).await?;\n\n            let result = json!({\n                \"refresh_token\": auth_tokens.refresh_token(),\n                \"access_token\": auth_tokens.access_token(),\n                \"expires_in\": auth_tokens.expires_in(),\n                \"token_type\": \"Bearer\",\n                \"scope\": auth_tokens.scope(),\n            });\n\n            Ok(Json(result))\n        }\n    }\n}\n\n// After exchanging the code we need to check first if 2FA is needed before continuing\nasync fn _sso_login(\n    data: ConnectData,\n    user_id: &mut Option<UserId>,\n    conn: &DbConn,\n    ip: &ClientIp,\n    client_version: &Option<ClientVersion>,\n) -> JsonResult {\n    AuthMethod::Sso.check_scope(data.scope.as_ref())?;\n\n    // Ratelimit the login\n    crate::ratelimit::check_limit_login(&ip.ip)?;\n\n    let (state, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) {\n        (None, _) => err!(\n            \"Got no code in OIDC data\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        ),\n        (_, None) => err!(\n            \"Got no code verifier in OIDC data\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        ),\n        (Some(code), Some(code_verifier)) => (code, code_verifier.clone()),\n    };\n\n    let (sso_auth, user_infos) = sso::exchange_code(state, code_verifier, conn).await?;\n    let user_with_sso = match SsoUser::find_by_identifier(&user_infos.identifier, conn).await {\n        None => match SsoUser::find_by_mail(&user_infos.email, conn).await {\n            None => None,\n            Some((user, Some(_))) => {\n                error!(\n                    \"Login failure ({}), existing SSO user ({}) with same email ({})\",\n                    user_infos.identifier, user.uuid, user.email\n                );\n                err_silent!(\n                    \"Existing SSO user with same email\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                )\n            }\n            Some((user, None)) if user.private_key.is_some() && !CONFIG.sso_signups_match_email() => {\n                error!(\n                    \"Login failure ({}), existing non SSO user ({}) with same email ({}) and association is disabled\",\n                    user_infos.identifier, user.uuid, user.email\n                );\n                err_silent!(\n                    \"Existing non SSO user with same email\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                )\n            }\n            Some((user, None)) => Some((user, None)),\n        },\n        Some((user, sso_user)) => Some((user, Some(sso_user))),\n    };\n\n    let now = Utc::now().naive_utc();\n    // Will trigger 2FA flow if needed\n    let (user, mut device, twofactor_token, sso_user) = match user_with_sso {\n        None => {\n            if !CONFIG.is_email_domain_allowed(&user_infos.email) {\n                err!(\n                    \"Email domain not allowed\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                );\n            }\n\n            match user_infos.email_verified {\n                None if !CONFIG.sso_allow_unknown_email_verification() => err!(\n                    \"Your provider does not send email verification status.\\n\\\n                    You will need to change the server configuration (check `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`) to log in.\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                ),\n                Some(false) => err!(\n                    \"You need to verify your email with your provider before you can log in\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                ),\n                _ => (),\n            }\n\n            let mut user = User::new(&user_infos.email, user_infos.user_name.clone());\n            user.verified_at = Some(now);\n            user.save(conn).await?;\n\n            let device = get_device(&data, conn, &user).await?;\n\n            (user, device, None, None)\n        }\n        Some((user, _)) if !user.enabled => {\n            err!(\n                \"This user has been disabled\",\n                format!(\"IP: {}. Username: {}.\", ip.ip, user.display_name()),\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn\n                }\n            )\n        }\n        Some((mut user, sso_user)) => {\n            let mut device = get_device(&data, conn, &user).await?;\n\n            let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;\n\n            if user.private_key.is_none() {\n                // User was invited a stub was created\n                user.verified_at = Some(now);\n                if let Some(ref user_name) = user_infos.user_name {\n                    user.name = user_name.clone();\n                }\n\n                user.save(conn).await?;\n            }\n\n            if user.email != user_infos.email {\n                if CONFIG.mail_enabled() {\n                    mail::send_sso_change_email(&user_infos.email).await?;\n                }\n                info!(\"User {} email changed in SSO provider from {} to {}\", user.uuid, user.email, user_infos.email);\n            }\n\n            (user, device, twofactor_token, sso_user)\n        }\n    };\n\n    // Set the user_uuid here to be passed back used for event logging.\n    *user_id = Some(user.uuid.clone());\n\n    // We passed 2FA get auth tokens\n    let auth_tokens = sso::redeem(&device, &user, data.client_id, sso_user, sso_auth, user_infos, conn).await?;\n\n    authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await\n}\n\nasync fn _password_login(\n    data: ConnectData,\n    user_id: &mut Option<UserId>,\n    conn: &DbConn,\n    ip: &ClientIp,\n    client_version: &Option<ClientVersion>,\n) -> JsonResult {\n    // Validate scope\n    AuthMethod::Password.check_scope(data.scope.as_ref())?;\n\n    // Ratelimit the login\n    crate::ratelimit::check_limit_login(&ip.ip)?;\n\n    // Get the user\n    let username = data.username.as_ref().unwrap().trim();\n    let Some(mut user) = User::find_by_mail(username, conn).await else {\n        err!(\"Username or password is incorrect. Try again\", format!(\"IP: {}. Username: {username}.\", ip.ip))\n    };\n\n    // Set the user_id here to be passed back used for event logging.\n    *user_id = Some(user.uuid.clone());\n\n    // Check if the user is disabled\n    if !user.enabled {\n        err!(\n            \"This user has been disabled\",\n            format!(\"IP: {}. Username: {username}.\", ip.ip),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        )\n    }\n\n    let password = data.password.as_ref().unwrap();\n\n    // If we get an auth request, we don't check the user's password, but the access code of the auth request\n    if let Some(ref auth_request_id) = data.auth_request {\n        let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else {\n            err!(\n                \"Auth request not found. Try again.\",\n                format!(\"IP: {}. Username: {username}.\", ip.ip),\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn,\n                }\n            )\n        };\n\n        let expiration_time = auth_request.creation_date + chrono::Duration::minutes(5);\n        let request_expired = Utc::now().naive_utc() >= expiration_time;\n\n        if auth_request.user_uuid != user.uuid\n            || !auth_request.approved.unwrap_or(false)\n            || request_expired\n            || ip.ip.to_string() != auth_request.request_ip\n            || !auth_request.check_access_code(password)\n        {\n            err!(\n                \"Username or access code is incorrect. Try again\",\n                format!(\"IP: {}. Username: {username}.\", ip.ip),\n                ErrorEvent {\n                    event: EventType::UserFailedLogIn,\n                }\n            )\n        }\n    } else if !user.check_valid_password(password) {\n        err!(\n            \"Username or password is incorrect. Try again\",\n            format!(\"IP: {}. Username: {username}.\", ip.ip),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn,\n            }\n        )\n    }\n\n    // Change the KDF Iterations (only when not logging in with an auth request)\n    if data.auth_request.is_none() {\n        kdf_upgrade(&mut user, password, conn).await?;\n    }\n\n    let now = Utc::now().naive_utc();\n\n    if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {\n        if user.last_verifying_at.is_none()\n            || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds()\n                > CONFIG.signups_verify_resend_time() as i64\n        {\n            let resend_limit = CONFIG.signups_verify_resend_limit() as i32;\n            if resend_limit == 0 || user.login_verify_count < resend_limit {\n                // We want to send another email verification if we require signups to verify\n                // their email address, and we haven't sent them a reminder in a while...\n                user.last_verifying_at = Some(now);\n                user.login_verify_count += 1;\n\n                if let Err(e) = user.save(conn).await {\n                    error!(\"Error updating user: {e:#?}\");\n                }\n\n                if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {\n                    error!(\"Error auto-sending email verification email: {e:#?}\");\n                }\n            }\n        }\n\n        // We still want the login to fail until they actually verified the email address\n        err!(\n            \"Please verify your email before trying again.\",\n            format!(\"IP: {}. Username: {username}.\", ip.ip),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        )\n    }\n\n    let mut device = get_device(&data, conn, &user).await?;\n\n    let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?;\n\n    let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);\n\n    authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await\n}\n\nasync fn authenticated_response(\n    user: &User,\n    device: &mut Device,\n    auth_tokens: auth::AuthTokens,\n    twofactor_token: Option<String>,\n    conn: &DbConn,\n    ip: &ClientIp,\n) -> JsonResult {\n    if CONFIG.mail_enabled() && device.is_new() {\n        let now = Utc::now().naive_utc();\n        if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, device).await {\n            error!(\"Error sending new device email: {e:#?}\");\n\n            if CONFIG.require_device_email() {\n                err!(\n                    \"Could not send login notification email. Please contact your administrator.\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                )\n            }\n        }\n    }\n\n    // register push device\n    if !device.is_new() {\n        register_push_device(device, conn).await?;\n    }\n\n    // Save to update `device.updated_at` to track usage and toggle new status\n    device.save(true, conn).await?;\n\n    let master_password_policy = master_password_policy(user, conn).await;\n\n    let has_master_password = !user.password_hash.is_empty();\n    let master_password_unlock = if has_master_password {\n        json!({\n            \"Kdf\": {\n                \"KdfType\": user.client_kdf_type,\n                \"Iterations\": user.client_kdf_iter,\n                \"Memory\": user.client_kdf_memory,\n                \"Parallelism\": user.client_kdf_parallelism\n            },\n            // This field is named inconsistently and will be removed and replaced by the \"wrapped\" variant in the apps.\n            // https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26\n            \"MasterKeyEncryptedUserKey\": user.akey,\n            \"MasterKeyWrappedUserKey\": user.akey,\n            \"Salt\": user.email\n        })\n    } else {\n        Value::Null\n    };\n\n    let account_keys = if user.private_key.is_some() {\n        json!({\n            \"publicKeyEncryptionKeyPair\": {\n                \"wrappedPrivateKey\": user.private_key,\n                \"publicKey\": user.public_key,\n                \"Object\": \"publicKeyEncryptionKeyPair\"\n            },\n            \"Object\": \"privateKeys\"\n        })\n    } else {\n        Value::Null\n    };\n\n    let mut result = json!({\n        \"access_token\": auth_tokens.access_token(),\n        \"expires_in\": auth_tokens.expires_in(),\n        \"token_type\": \"Bearer\",\n        \"refresh_token\": auth_tokens.refresh_token(),\n        \"PrivateKey\": user.private_key,\n        \"Kdf\": user.client_kdf_type,\n        \"KdfIterations\": user.client_kdf_iter,\n        \"KdfMemory\": user.client_kdf_memory,\n        \"KdfParallelism\": user.client_kdf_parallelism,\n        \"ResetMasterPassword\": false, // TODO: Same as above\n        \"ForcePasswordReset\": false,\n        \"MasterPasswordPolicy\": master_password_policy,\n        \"scope\": auth_tokens.scope(),\n        \"AccountKeys\": account_keys,\n        \"UserDecryptionOptions\": {\n            \"HasMasterPassword\": has_master_password,\n            \"MasterPasswordUnlock\": master_password_unlock,\n            \"Object\": \"userDecryptionOptions\"\n        },\n    });\n\n    if !user.akey.is_empty() {\n        result[\"Key\"] = Value::String(user.akey.clone());\n    }\n\n    if let Some(token) = twofactor_token {\n        result[\"TwoFactorToken\"] = Value::String(token);\n    }\n\n    info!(\"User {} logged in successfully. IP: {}\", user.display_name(), ip.ip);\n    Ok(Json(result))\n}\n\nasync fn _api_key_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &DbConn, ip: &ClientIp) -> JsonResult {\n    // Ratelimit the login\n    crate::ratelimit::check_limit_login(&ip.ip)?;\n\n    // Validate scope\n    match data.scope.as_ref() {\n        Some(scope) if scope == &AuthMethod::UserApiKey.scope() => _user_api_key_login(data, user_id, conn, ip).await,\n        Some(scope) if scope == &AuthMethod::OrgApiKey.scope() => _organization_api_key_login(data, conn, ip).await,\n        _ => err!(\"Scope not supported\"),\n    }\n}\n\nasync fn _user_api_key_login(\n    data: ConnectData,\n    user_id: &mut Option<UserId>,\n    conn: &DbConn,\n    ip: &ClientIp,\n) -> JsonResult {\n    // Get the user via the client_id\n    let client_id = data.client_id.as_ref().unwrap();\n    let Some(client_user_id) = client_id.strip_prefix(\"user.\") else {\n        err!(\"Malformed client_id\", format!(\"IP: {}.\", ip.ip))\n    };\n    let client_user_id: UserId = client_user_id.into();\n    let Some(user) = User::find_by_uuid(&client_user_id, conn).await else {\n        err!(\"Invalid client_id\", format!(\"IP: {}.\", ip.ip))\n    };\n\n    // Set the user_id here to be passed back used for event logging.\n    *user_id = Some(user.uuid.clone());\n\n    // Check if the user is disabled\n    if !user.enabled {\n        err!(\n            \"This user has been disabled (API key login)\",\n            format!(\"IP: {}. Username: {}.\", ip.ip, user.email),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        )\n    }\n\n    // Check API key. Note that API key logins bypass 2FA.\n    let client_secret = data.client_secret.as_ref().unwrap();\n    if !user.check_valid_api_key(client_secret) {\n        err!(\n            \"Incorrect client_secret\",\n            format!(\"IP: {}. Username: {}.\", ip.ip, user.email),\n            ErrorEvent {\n                event: EventType::UserFailedLogIn\n            }\n        )\n    }\n\n    let mut device = get_device(&data, conn, &user).await?;\n\n    if CONFIG.mail_enabled() && device.is_new() {\n        let now = Utc::now().naive_utc();\n        if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {\n            error!(\"Error sending new device email: {e:#?}\");\n\n            if CONFIG.require_device_email() {\n                err!(\n                    \"Could not send login notification email. Please contact your administrator.\",\n                    ErrorEvent {\n                        event: EventType::UserFailedLogIn\n                    }\n                )\n            }\n        }\n    }\n\n    // ---\n    // Disabled this variable, it was used to generate the JWT\n    // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out\n    // See: https://github.com/dani-garcia/vaultwarden/issues/4156\n    // ---\n    // let orgs = Membership::find_confirmed_by_user(&user.uuid, conn).await;\n    let access_claims = auth::LoginJwtClaims::default(&device, &user, &AuthMethod::UserApiKey, data.client_id);\n\n    // Save to update `device.updated_at` to track usage and toggle new status\n    device.save(true, conn).await?;\n\n    info!(\"User {} logged in successfully via API key. IP: {}\", user.email, ip.ip);\n\n    let has_master_password = !user.password_hash.is_empty();\n    let master_password_unlock = if has_master_password {\n        json!({\n            \"Kdf\": {\n                \"KdfType\": user.client_kdf_type,\n                \"Iterations\": user.client_kdf_iter,\n                \"Memory\": user.client_kdf_memory,\n                \"Parallelism\": user.client_kdf_parallelism\n            },\n            // This field is named inconsistently and will be removed and replaced by the \"wrapped\" variant in the apps.\n            // https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26\n            \"MasterKeyEncryptedUserKey\": user.akey,\n            \"MasterKeyWrappedUserKey\": user.akey,\n            \"Salt\": user.email\n        })\n    } else {\n        Value::Null\n    };\n\n    let account_keys = if user.private_key.is_some() {\n        json!({\n            \"publicKeyEncryptionKeyPair\": {\n                \"wrappedPrivateKey\": user.private_key,\n                \"publicKey\": user.public_key,\n                \"Object\": \"publicKeyEncryptionKeyPair\"\n            },\n            \"Object\": \"privateKeys\"\n        })\n    } else {\n        Value::Null\n    };\n\n    // Note: No refresh_token is returned. The CLI just repeats the\n    // client_credentials login flow when the existing token expires.\n    let result = json!({\n        \"access_token\": access_claims.token(),\n        \"expires_in\": access_claims.expires_in(),\n        \"token_type\": \"Bearer\",\n        \"Key\": user.akey,\n        \"PrivateKey\": user.private_key,\n\n        \"Kdf\": user.client_kdf_type,\n        \"KdfIterations\": user.client_kdf_iter,\n        \"KdfMemory\": user.client_kdf_memory,\n        \"KdfParallelism\": user.client_kdf_parallelism,\n        \"ResetMasterPassword\": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing\n        \"ForcePasswordReset\": false,\n        \"scope\": AuthMethod::UserApiKey.scope(),\n        \"AccountKeys\": account_keys,\n        \"UserDecryptionOptions\": {\n            \"HasMasterPassword\": has_master_password,\n            \"MasterPasswordUnlock\": master_password_unlock,\n            \"Object\": \"userDecryptionOptions\"\n        },\n    });\n\n    Ok(Json(result))\n}\n\nasync fn _organization_api_key_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonResult {\n    // Get the org via the client_id\n    let client_id = data.client_id.as_ref().unwrap();\n    let Some(org_id) = client_id.strip_prefix(\"organization.\") else {\n        err!(\"Malformed client_id\", format!(\"IP: {}.\", ip.ip))\n    };\n    let org_id: OrganizationId = org_id.to_string().into();\n    let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else {\n        err!(\"Invalid client_id\", format!(\"IP: {}.\", ip.ip))\n    };\n\n    // Check API key.\n    let client_secret = data.client_secret.as_ref().unwrap();\n    if !org_api_key.check_valid_api_key(client_secret) {\n        err!(\"Incorrect client_secret\", format!(\"IP: {}. Organization: {}.\", ip.ip, org_api_key.org_uuid))\n    }\n\n    let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid);\n    let access_token = auth::encode_jwt(&claim);\n\n    Ok(Json(json!({\n        \"access_token\": access_token,\n        \"expires_in\": 3600,\n        \"token_type\": \"Bearer\",\n        \"scope\": AuthMethod::OrgApiKey.scope(),\n    })))\n}\n\n/// Retrieves an existing device or creates a new device from ConnectData and the User\nasync fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult<Device> {\n    // On iOS, device_type sends \"iOS\", on others it sends a number\n    // When unknown or unable to parse, return 14, which is 'Unknown Browser'\n    let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14);\n    let device_id = data.device_identifier.clone().expect(\"No device id provided\");\n    let device_name = data.device_name.clone().expect(\"No device name provided\");\n\n    // Find device or create new\n    match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await {\n        Some(device) => Ok(device),\n        None => {\n            let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type);\n            // save device without updating `device.updated_at`\n            device.save(false, conn).await?;\n            Ok(device)\n        }\n    }\n}\n\nasync fn twofactor_auth(\n    user: &mut User,\n    data: &ConnectData,\n    device: &mut Device,\n    ip: &ClientIp,\n    client_version: &Option<ClientVersion>,\n    conn: &DbConn,\n) -> ApiResult<Option<String>> {\n    let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;\n\n    // No twofactor token if twofactor is disabled\n    if twofactors.is_empty() {\n        enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?;\n        return Ok(None);\n    }\n\n    TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?;\n\n    let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();\n    let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one\n\n    let twofactor_code = match data.two_factor_token {\n        Some(ref code) => code,\n        None => {\n            err_json!(\n                _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,\n                \"2FA token not provided\"\n            )\n        }\n    };\n\n    let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);\n\n    use crate::crypto::ct_eq;\n\n    let selected_data = _selected_data(selected_twofactor);\n    let mut remember = data.two_factor_remember.unwrap_or(0);\n\n    match TwoFactorType::from_i32(selected_id) {\n        Some(TwoFactorType::Authenticator) => {\n            authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?\n        }\n        Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,\n        Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,\n        Some(TwoFactorType::Duo) => {\n            match CONFIG.duo_use_iframe() {\n                true => {\n                    // Legacy iframe prompt flow\n                    duo::validate_duo_login(&user.email, twofactor_code, conn).await?\n                }\n                false => {\n                    // OIDC based flow\n                    duo_oidc::validate_duo_login(\n                        &user.email,\n                        twofactor_code,\n                        data.client_id.as_ref().unwrap(),\n                        data.device_identifier.as_ref().unwrap(),\n                        conn,\n                    )\n                    .await?\n                }\n            }\n        }\n        Some(TwoFactorType::Email) => {\n            email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?\n        }\n        Some(TwoFactorType::Remember) => {\n            match device.twofactor_remember {\n                Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {\n                    remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time\n                }\n                _ => {\n                    err_json!(\n                        _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,\n                        \"2FA Remember token not provided\"\n                    )\n                }\n            }\n        }\n        Some(TwoFactorType::RecoveryCode) => {\n            // Check if recovery code is correct\n            if !user.check_valid_recovery_code(twofactor_code) {\n                err!(\"Recovery code is incorrect. Try again.\")\n            }\n\n            // Remove all twofactors from the user\n            TwoFactor::delete_all_by_user(&user.uuid, conn).await?;\n            enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?;\n\n            log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await;\n\n            // Remove the recovery code, not needed without twofactors\n            user.totp_recover = None;\n            user.save(conn).await?;\n        }\n        _ => err!(\n            \"Invalid two factor provider\",\n            ErrorEvent {\n                event: EventType::UserFailedLogIn2fa\n            }\n        ),\n    }\n\n    TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?;\n\n    let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 {\n        Some(device.refresh_twofactor_remember())\n    } else {\n        device.delete_twofactor_remember();\n        None\n    };\n    Ok(two_factor)\n}\n\nfn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {\n    tf.map(|t| t.data).map_res(\"Two factor doesn't exist\")\n}\n\nasync fn _json_err_twofactor(\n    providers: &[i32],\n    user_id: &UserId,\n    data: &ConnectData,\n    client_version: &Option<ClientVersion>,\n    conn: &DbConn,\n) -> ApiResult<Value> {\n    let mut result = json!({\n        \"error\" : \"invalid_grant\",\n        \"error_description\" : \"Two factor required.\",\n        \"TwoFactorProviders\" : providers.iter().map(ToString::to_string).collect::<Vec<String>>(),\n        \"TwoFactorProviders2\" : {}, // { \"0\" : null }\n        \"MasterPasswordPolicy\": {\n            \"Object\": \"masterPasswordPolicy\"\n        }\n    });\n\n    for provider in providers {\n        result[\"TwoFactorProviders2\"][provider.to_string()] = Value::Null;\n\n        match TwoFactorType::from_i32(*provider) {\n            Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }\n\n            Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {\n                let request = webauthn::generate_webauthn_login(user_id, conn).await?;\n                result[\"TwoFactorProviders2\"][provider.to_string()] = request.0;\n            }\n\n            Some(TwoFactorType::Duo) => {\n                let email = match User::find_by_uuid(user_id, conn).await {\n                    Some(u) => u.email,\n                    None => err!(\"User does not exist\"),\n                };\n\n                match CONFIG.duo_use_iframe() {\n                    true => {\n                        // Legacy iframe prompt flow\n                        let (signature, host) = duo::generate_duo_signature(&email, conn).await?;\n                        result[\"TwoFactorProviders2\"][provider.to_string()] = json!({\n                            \"Host\": host,\n                            \"Signature\": signature,\n                        })\n                    }\n                    false => {\n                        // OIDC based flow\n                        let auth_url = duo_oidc::get_duo_auth_url(\n                            &email,\n                            data.client_id.as_ref().unwrap(),\n                            data.device_identifier.as_ref().unwrap(),\n                            conn,\n                        )\n                        .await?;\n\n                        result[\"TwoFactorProviders2\"][provider.to_string()] = json!({\n                            \"AuthUrl\": auth_url,\n                        })\n                    }\n                }\n            }\n\n            Some(tf_type @ TwoFactorType::YubiKey) => {\n                let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {\n                    err!(\"No YubiKey devices registered\")\n                };\n\n                let yubikey_metadata: yubikey::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;\n\n                result[\"TwoFactorProviders2\"][provider.to_string()] = json!({\n                    \"Nfc\": yubikey_metadata.nfc,\n                })\n            }\n\n            Some(tf_type @ TwoFactorType::Email) => {\n                let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {\n                    err!(\"No twofactor email registered\")\n                };\n\n                // Starting with version 2025.5.0 the client will call `/api/two-factor/send-email-login`.\n                let disabled_send = if let Some(cv) = client_version {\n                    let ver_match = semver::VersionReq::parse(\">=2025.5.0\").unwrap();\n                    ver_match.matches(&cv.0)\n                } else {\n                    false\n                };\n\n                // Send email immediately if email is the only 2FA option.\n                if providers.len() == 1 && !disabled_send {\n                    email::send_token(user_id, conn).await?\n                }\n\n                let email_data = email::EmailTokenData::from_json(&twofactor.data)?;\n                result[\"TwoFactorProviders2\"][provider.to_string()] = json!({\n                    \"Email\": email::obscure_email(&email_data.email),\n                })\n            }\n\n            _ => {}\n        }\n    }\n\n    Ok(result)\n}\n\n#[post(\"/accounts/prelogin\", data = \"<data>\")]\nasync fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {\n    _prelogin(data, conn).await\n}\n\n#[post(\"/accounts/register\", data = \"<data>\")]\nasync fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {\n    _register(data, false, conn).await\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct RegisterVerificationData {\n    email: String,\n    name: Option<String>,\n    // receiveMarketingEmails: bool,\n}\n\n#[derive(rocket::Responder)]\nenum RegisterVerificationResponse {\n    #[response(status = 204)]\n    NoContent(()),\n    Token(Json<String>),\n}\n\n#[post(\"/accounts/register/send-verification-email\", data = \"<data>\")]\nasync fn register_verification_email(\n    data: Json<RegisterVerificationData>,\n    conn: DbConn,\n) -> ApiResult<RegisterVerificationResponse> {\n    let data = data.into_inner();\n\n    // the registration can only continue if signup is allowed or there exists an invitation\n    if !(CONFIG.is_signup_allowed(&data.email)\n        || (!CONFIG.mail_enabled() && Invitation::find_by_mail(&data.email, &conn).await.is_some()))\n    {\n        err!(\"Registration not allowed or user already exists\")\n    }\n\n    let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify();\n\n    let token_claims = auth::generate_register_verify_claims(data.email.clone(), data.name.clone(), should_send_mail);\n    let token = auth::encode_jwt(&token_claims);\n\n    if should_send_mail {\n        let user = User::find_by_mail(&data.email, &conn).await;\n        if user.filter(|u| u.private_key.is_some()).is_some() {\n            // There is still a timing side channel here in that the code\n            // paths that send mail take noticeably longer than ones that don't.\n            // Add a randomized sleep to mitigate this somewhat.\n            use rand::{rngs::SmallRng, RngExt};\n            let mut rng: SmallRng = rand::make_rng();\n            let sleep_ms = rng.random_range(900..=1100) as u64;\n            tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;\n        } else {\n            mail::send_register_verify_email(&data.email, &token).await?;\n        }\n\n        Ok(RegisterVerificationResponse::NoContent(()))\n    } else {\n        // If email verification is not required, return the token directly\n        // the clients will use this token to finish the registration\n        Ok(RegisterVerificationResponse::Token(Json(token)))\n    }\n}\n\n#[post(\"/accounts/register/finish\", data = \"<data>\")]\nasync fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {\n    _register(data, true, conn).await\n}\n\n// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts\n// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs\n#[derive(Debug, Clone, Default, FromForm)]\nstruct ConnectData {\n    #[field(name = uncased(\"grant_type\"))]\n    #[field(name = uncased(\"granttype\"))]\n    grant_type: String, // refresh_token, password, client_credentials (API key)\n\n    // Needed for grant_type=\"refresh_token\"\n    #[field(name = uncased(\"refresh_token\"))]\n    #[field(name = uncased(\"refreshtoken\"))]\n    refresh_token: Option<String>,\n\n    // Needed for grant_type = \"password\" | \"client_credentials\"\n    #[field(name = uncased(\"client_id\"))]\n    #[field(name = uncased(\"clientid\"))]\n    client_id: Option<String>, // web, cli, desktop, browser, mobile\n    #[field(name = uncased(\"client_secret\"))]\n    #[field(name = uncased(\"clientsecret\"))]\n    client_secret: Option<String>,\n    #[field(name = uncased(\"password\"))]\n    password: Option<String>,\n    #[field(name = uncased(\"scope\"))]\n    scope: Option<String>,\n    #[field(name = uncased(\"username\"))]\n    username: Option<String>,\n\n    #[field(name = uncased(\"device_identifier\"))]\n    #[field(name = uncased(\"deviceidentifier\"))]\n    device_identifier: Option<DeviceId>,\n    #[field(name = uncased(\"device_name\"))]\n    #[field(name = uncased(\"devicename\"))]\n    device_name: Option<String>,\n    #[field(name = uncased(\"device_type\"))]\n    #[field(name = uncased(\"devicetype\"))]\n    device_type: Option<String>,\n    #[allow(unused)]\n    #[field(name = uncased(\"device_push_token\"))]\n    #[field(name = uncased(\"devicepushtoken\"))]\n    _device_push_token: Option<String>, // Unused; mobile device push not yet supported.\n\n    // Needed for two-factor auth\n    #[field(name = uncased(\"two_factor_provider\"))]\n    #[field(name = uncased(\"twofactorprovider\"))]\n    two_factor_provider: Option<i32>,\n    #[field(name = uncased(\"two_factor_token\"))]\n    #[field(name = uncased(\"twofactortoken\"))]\n    two_factor_token: Option<String>,\n    #[field(name = uncased(\"two_factor_remember\"))]\n    #[field(name = uncased(\"twofactorremember\"))]\n    two_factor_remember: Option<i32>,\n    #[field(name = uncased(\"authrequest\"))]\n    auth_request: Option<AuthRequestId>,\n\n    // Needed for authorization code\n    #[field(name = uncased(\"code\"))]\n    code: Option<OIDCState>,\n    #[field(name = uncased(\"code_verifier\"))]\n    code_verifier: Option<OIDCCodeVerifier>,\n}\nfn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {\n    if value.is_none() {\n        err!(msg)\n    }\n    Ok(())\n}\n\n#[get(\"/sso/prevalidate\")]\nfn prevalidate() -> JsonResult {\n    if CONFIG.sso_enabled() {\n        let sso_token = sso::encode_ssotoken_claims();\n        Ok(Json(json!({\n            \"token\": sso_token,\n        })))\n    } else {\n        err!(\"SSO sign-in is not available\")\n    }\n}\n\n#[get(\"/connect/oidc-signin?<code>&<state>\", rank = 1)]\nasync fn oidcsignin(code: OIDCCode, state: String, mut conn: DbConn) -> ApiResult<Redirect> {\n    _oidcsignin_redirect(\n        state,\n        OIDCCodeWrapper::Ok {\n            code,\n        },\n        &mut conn,\n    )\n    .await\n}\n\n// Bitwarden client appear to only care for code and state so we pipe it through\n// cf: https://github.com/bitwarden/clients/blob/80b74b3300e15b4ae414dc06044cc9b02b6c10a6/libs/auth/src/angular/sso/sso.component.ts#L141\n#[get(\"/connect/oidc-signin?<state>&<error>&<error_description>\", rank = 2)]\nasync fn oidcsignin_error(\n    state: String,\n    error: String,\n    error_description: Option<String>,\n    mut conn: DbConn,\n) -> ApiResult<Redirect> {\n    _oidcsignin_redirect(\n        state,\n        OIDCCodeWrapper::Error {\n            error,\n            error_description,\n        },\n        &mut conn,\n    )\n    .await\n}\n\n// The state was encoded using Base64 to ensure no issue with providers.\n// iss and scope parameters are needed for redirection to work on IOS.\n// We pass the state as the code to get it back later on.\nasync fn _oidcsignin_redirect(\n    base64_state: String,\n    code_response: OIDCCodeWrapper,\n    conn: &mut DbConn,\n) -> ApiResult<Redirect> {\n    let state = sso::decode_state(&base64_state)?;\n\n    let mut sso_auth = match SsoAuth::find(&state, conn).await {\n        None => err!(format!(\"Cannot retrieve sso_auth for {state}\")),\n        Some(sso_auth) => sso_auth,\n    };\n    sso_auth.code_response = Some(code_response);\n    sso_auth.updated_at = Utc::now().naive_utc();\n    sso_auth.save(conn).await?;\n\n    let mut url = match url::Url::parse(&sso_auth.redirect_uri) {\n        Ok(url) => url,\n        Err(err) => err!(format!(\"Failed to parse redirect uri ({}): {err}\", sso_auth.redirect_uri)),\n    };\n\n    url.query_pairs_mut()\n        .append_pair(\"code\", &state)\n        .append_pair(\"state\", &state)\n        .append_pair(\"scope\", &AuthMethod::Sso.scope())\n        .append_pair(\"iss\", &CONFIG.domain());\n\n    debug!(\"Redirection to {url}\");\n\n    Ok(Redirect::temporary(String::from(url)))\n}\n\n#[derive(Debug, Clone, Default, FromForm)]\nstruct AuthorizeData {\n    #[field(name = uncased(\"client_id\"))]\n    #[field(name = uncased(\"clientid\"))]\n    client_id: String,\n    #[field(name = uncased(\"redirect_uri\"))]\n    #[field(name = uncased(\"redirecturi\"))]\n    redirect_uri: String,\n    #[allow(unused)]\n    response_type: Option<String>,\n    #[allow(unused)]\n    scope: Option<String>,\n    state: OIDCState,\n    code_challenge: OIDCCodeChallenge,\n    code_challenge_method: String,\n    #[allow(unused)]\n    response_mode: Option<String>,\n    #[allow(unused)]\n    domain_hint: Option<String>,\n    #[allow(unused)]\n    #[field(name = uncased(\"ssoToken\"))]\n    sso_token: Option<String>,\n}\n\n// The `redirect_uri` will change depending of the client (web, android, ios ..)\n#[get(\"/connect/authorize?<data..>\")]\nasync fn authorize(data: AuthorizeData, conn: DbConn) -> ApiResult<Redirect> {\n    let AuthorizeData {\n        client_id,\n        redirect_uri,\n        state,\n        code_challenge,\n        code_challenge_method,\n        ..\n    } = data;\n\n    if code_challenge_method != \"S256\" {\n        err!(\"Unsupported code challenge method\");\n    }\n\n    let auth_url = sso::authorize_url(state, code_challenge, &client_id, &redirect_uri, conn).await?;\n\n    Ok(Redirect::temporary(String::from(auth_url)))\n}\n"
  },
  {
    "path": "src/api/mod.rs",
    "content": "mod admin;\npub mod core;\nmod icons;\nmod identity;\nmod notifications;\nmod push;\nmod web;\n\nuse rocket::serde::json::Json;\nuse serde_json::Value;\n\npub use crate::api::{\n    admin::catchers as admin_catchers,\n    admin::routes as admin_routes,\n    core::catchers as core_catchers,\n    core::purge_auth_requests,\n    core::purge_sends,\n    core::purge_trashed_ciphers,\n    core::routes as core_routes,\n    core::two_factor::send_incomplete_2fa_notifications,\n    core::{emergency_notification_reminder_job, emergency_request_timeout_job},\n    core::{event_cleanup_job, events_routes as core_events_routes},\n    icons::routes as icons_routes,\n    identity::routes as identity_routes,\n    notifications::routes as notifications_routes,\n    notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},\n    push::{\n        push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device,\n        unregister_push_device,\n    },\n    web::catchers as web_catchers,\n    web::routes as web_routes,\n    web::static_files,\n};\nuse crate::db::{\n    models::{OrgPolicy, OrgPolicyType, User},\n    DbConn,\n};\nuse crate::CONFIG;\n\n// Type aliases for API methods results\npub type ApiResult<T> = Result<T, crate::error::Error>;\npub type JsonResult = ApiResult<Json<Value>>;\npub type EmptyResult = ApiResult<()>;\n\n// Common structs representing JSON data received\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct PasswordOrOtpData {\n    #[serde(alias = \"MasterPasswordHash\")]\n    master_password_hash: Option<String>,\n    otp: Option<String>,\n}\n\nimpl PasswordOrOtpData {\n    /// Tokens used via this struct can be used multiple times during the process\n    /// First for the validation to continue, after that to enable or validate the following actions\n    /// This is different per caller, so it can be adjusted to delete the token or not\n    pub async fn validate(&self, user: &User, delete_if_valid: bool, conn: &DbConn) -> EmptyResult {\n        use crate::api::core::two_factor::protected_actions::validate_protected_action_otp;\n\n        match (self.master_password_hash.as_deref(), self.otp.as_deref()) {\n            (Some(pw_hash), None) => {\n                if !user.check_valid_password(pw_hash) {\n                    err!(\"Invalid password\");\n                }\n            }\n            (None, Some(otp)) => {\n                validate_protected_action_otp(otp, &user.uuid, delete_if_valid, conn).await?;\n            }\n            _ => err!(\"No validation provided\"),\n        }\n        Ok(())\n    }\n}\n\n#[derive(Debug, Default, Deserialize, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct MasterPasswordPolicy {\n    min_complexity: Option<u8>,\n    min_length: Option<u32>,\n    require_lower: bool,\n    require_upper: bool,\n    require_numbers: bool,\n    require_special: bool,\n    enforce_on_login: bool,\n}\n\n// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy\nasync fn master_password_policy(user: &User, conn: &DbConn) -> Value {\n    let master_password_policies: Vec<MasterPasswordPolicy> =\n        OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(\n            &user.uuid,\n            OrgPolicyType::MasterPassword,\n            conn,\n        )\n        .await\n        .into_iter()\n        .filter_map(|p| serde_json::from_str(&p.data).ok())\n        .collect();\n\n    let mut mpp_json = if !master_password_policies.is_empty() {\n        json!(master_password_policies.into_iter().reduce(|acc, policy| {\n            MasterPasswordPolicy {\n                min_complexity: acc.min_complexity.max(policy.min_complexity),\n                min_length: acc.min_length.max(policy.min_length),\n                require_lower: acc.require_lower || policy.require_lower,\n                require_upper: acc.require_upper || policy.require_upper,\n                require_numbers: acc.require_numbers || policy.require_numbers,\n                require_special: acc.require_special || policy.require_special,\n                enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,\n            }\n        }))\n    } else if CONFIG.sso_enabled() {\n        CONFIG.sso_master_password_policy_value().unwrap_or(json!({}))\n    } else {\n        json!({})\n    };\n\n    // NOTE: Upstream still uses PascalCase here for `Object`!\n    mpp_json[\"Object\"] = json!(\"masterPasswordPolicy\");\n    mpp_json\n}\n"
  },
  {
    "path": "src/api/notifications.rs",
    "content": "use std::{\n    net::IpAddr,\n    sync::{Arc, LazyLock},\n    time::Duration,\n};\n\nuse chrono::{NaiveDateTime, Utc};\nuse rmpv::Value;\nuse rocket::{futures::StreamExt, Route};\nuse rocket_ws::{Message, WebSocket};\nuse tokio::sync::mpsc::Sender;\n\nuse crate::{\n    auth::{ClientIp, WsAccessTokenHeader},\n    db::{\n        models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},\n        DbConn,\n    },\n    Error, CONFIG,\n};\n\npub static WS_USERS: LazyLock<Arc<WebSocketUsers>> = LazyLock::new(|| {\n    Arc::new(WebSocketUsers {\n        map: Arc::new(dashmap::DashMap::new()),\n    })\n});\n\npub static WS_ANONYMOUS_SUBSCRIPTIONS: LazyLock<Arc<AnonymousWebSocketSubscriptions>> = LazyLock::new(|| {\n    Arc::new(AnonymousWebSocketSubscriptions {\n        map: Arc::new(dashmap::DashMap::new()),\n    })\n});\n\nuse super::{\n    push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout,\n    push_send_update, push_user_update,\n};\n\nstatic NOTIFICATIONS_DISABLED: LazyLock<bool> = LazyLock::new(|| !CONFIG.enable_websocket() && !CONFIG.push_enabled());\n\npub fn routes() -> Vec<Route> {\n    if CONFIG.enable_websocket() {\n        routes![websockets_hub, anonymous_websockets_hub]\n    } else {\n        info!(\"WebSocket are disabled, realtime sync functionality will not work!\");\n        routes![]\n    }\n}\n\n#[derive(FromForm, Debug)]\nstruct WsAccessToken {\n    access_token: Option<String>,\n}\n\nstruct WSEntryMapGuard {\n    users: Arc<WebSocketUsers>,\n    user_uuid: UserId,\n    entry_uuid: uuid::Uuid,\n    addr: IpAddr,\n}\n\nimpl WSEntryMapGuard {\n    fn new(users: Arc<WebSocketUsers>, user_uuid: UserId, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {\n        Self {\n            users,\n            user_uuid,\n            entry_uuid,\n            addr,\n        }\n    }\n}\n\nimpl Drop for WSEntryMapGuard {\n    fn drop(&mut self) {\n        info!(\"Closing WS connection from {}\", self.addr);\n        if let Some(mut entry) = self.users.map.get_mut(self.user_uuid.as_ref()) {\n            entry.retain(|(uuid, _)| uuid != &self.entry_uuid);\n        }\n    }\n}\n\nstruct WSAnonymousEntryMapGuard {\n    subscriptions: Arc<AnonymousWebSocketSubscriptions>,\n    token: String,\n    addr: IpAddr,\n}\n\nimpl WSAnonymousEntryMapGuard {\n    fn new(subscriptions: Arc<AnonymousWebSocketSubscriptions>, token: String, addr: IpAddr) -> Self {\n        Self {\n            subscriptions,\n            token,\n            addr,\n        }\n    }\n}\n\nimpl Drop for WSAnonymousEntryMapGuard {\n    fn drop(&mut self) {\n        info!(\"Closing WS connection from {}\", self.addr);\n        self.subscriptions.map.remove(&self.token);\n    }\n}\n\n#[allow(tail_expr_drop_order)]\n#[get(\"/hub?<data..>\")]\nfn websockets_hub<'r>(\n    ws: WebSocket,\n    data: WsAccessToken,\n    ip: ClientIp,\n    header_token: WsAccessTokenHeader,\n) -> Result<rocket_ws::Stream!['r], Error> {\n    info!(\"Accepting Rocket WS connection from {}\", ip.ip);\n\n    let token = if let Some(token) = data.access_token {\n        token\n    } else if let Some(token) = header_token.access_token {\n        token\n    } else {\n        err_code!(\"Invalid claim\", 401)\n    };\n\n    let Ok(claims) = crate::auth::decode_login(&token) else {\n        err_code!(\"Invalid token\", 401)\n    };\n\n    let (mut rx, guard) = {\n        let users = Arc::clone(&WS_USERS);\n\n        // Add a channel to send messages to this client to the map\n        let entry_uuid = uuid::Uuid::new_v4();\n        let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);\n        users.map.entry(claims.sub.to_string()).or_default().push((entry_uuid, tx));\n\n        // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map\n        (rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, ip.ip))\n    };\n\n    Ok({\n        rocket_ws::Stream! { ws => {\n            let mut ws = ws;\n            let _guard = guard;\n            let mut interval = tokio::time::interval(Duration::from_secs(15));\n            loop {\n                tokio::select! {\n                    res = ws.next() =>  {\n                        match res {\n                            Some(Ok(message)) => {\n                                match message {\n                                    // Respond to any pings\n                                    Message::Ping(ping) => yield Message::Pong(ping),\n                                    Message::Pong(_) => {/* Ignored */},\n\n                                    // We should receive an initial message with the protocol and version, and we will reply to it\n                                    Message::Text(ref message) => {\n                                        let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message);\n\n                                        if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {\n                                            yield Message::binary(INITIAL_RESPONSE);\n                                        }\n                                    }\n\n                                    // Prevent sending anything back when a `Close` Message is received.\n                                    // Just break the loop\n                                    Message::Close(_) => break,\n\n                                    // Just echo anything else the client sends\n                                    _ => yield message,\n                                }\n                            }\n                            _ => break,\n                        }\n                    }\n\n                    res = rx.recv() => {\n                        match res {\n                            Some(res) => yield res,\n                            None => break,\n                        }\n                    }\n\n                    _ = interval.tick() => yield Message::Ping(create_ping())\n                }\n            }\n        }}\n    })\n}\n\n#[allow(tail_expr_drop_order)]\n#[get(\"/anonymous-hub?<token..>\")]\nfn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> Result<rocket_ws::Stream!['r], Error> {\n    info!(\"Accepting Anonymous Rocket WS connection from {}\", ip.ip);\n\n    let (mut rx, guard) = {\n        let subscriptions = Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS);\n\n        // Add a channel to send messages to this client to the map\n        let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);\n        subscriptions.map.insert(token.clone(), tx);\n\n        // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map\n        (rx, WSAnonymousEntryMapGuard::new(subscriptions, token, ip.ip))\n    };\n\n    Ok({\n        rocket_ws::Stream! { ws => {\n            let mut ws = ws;\n            let _guard = guard;\n            let mut interval = tokio::time::interval(Duration::from_secs(15));\n            loop {\n                tokio::select! {\n                    res = ws.next() =>  {\n                        match res {\n                            Some(Ok(message)) => {\n                                match message {\n                                    // Respond to any pings\n                                    Message::Ping(ping) => yield Message::Pong(ping),\n                                    Message::Pong(_) => {/* Ignored */},\n\n                                    // We should receive an initial message with the protocol and version, and we will reply to it\n                                    Message::Text(ref message) => {\n                                        let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message);\n\n                                        if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {\n                                            yield Message::binary(INITIAL_RESPONSE);\n                                        }\n                                    }\n\n                                    // Prevent sending anything back when a `Close` Message is received.\n                                    // Just break the loop\n                                    Message::Close(_) => break,\n\n                                    // Just echo anything else the client sends\n                                    _ => yield message,\n                                }\n                            }\n                            _ => break,\n                        }\n                    }\n\n                    res = rx.recv() => {\n                        match res {\n                            Some(res) => yield res,\n                            None => break,\n                        }\n                    }\n\n                    _ = interval.tick() => yield Message::Ping(create_ping())\n                }\n            }\n        }}\n    })\n}\n\n//\n// Websockets server\n//\n\nfn serialize(val: &Value) -> Vec<u8> {\n    use rmpv::encode::write_value;\n\n    let mut buf = Vec::new();\n    write_value(&mut buf, val).expect(\"Error encoding MsgPack\");\n\n    // Add size bytes at the start\n    // Extracted from BinaryMessageFormat.js\n    let mut size: usize = buf.len();\n    let mut len_buf: Vec<u8> = Vec::new();\n\n    loop {\n        let mut size_part = size & 0x7f;\n        size >>= 7;\n\n        if size > 0 {\n            size_part |= 0x80;\n        }\n\n        len_buf.push(size_part as u8);\n\n        if size == 0 {\n            break;\n        }\n    }\n\n    len_buf.append(&mut buf);\n    len_buf\n}\n\nfn serialize_date(date: NaiveDateTime) -> Value {\n    let seconds: i64 = date.and_utc().timestamp();\n    let nanos: i64 = date.and_utc().timestamp_subsec_nanos().into();\n    let timestamp = (nanos << 34) | seconds;\n\n    let bs = timestamp.to_be_bytes();\n\n    // -1 is Timestamp\n    // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type\n    Value::Ext(-1, bs.to_vec())\n}\n\nfn convert_option<T: Into<Value>>(option: Option<T>) -> Value {\n    match option {\n        Some(a) => a.into(),\n        None => Value::Nil,\n    }\n}\n\nconst RECORD_SEPARATOR: u8 = 0x1e;\nconst INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS>\n\n#[derive(Deserialize, Copy, Clone, Eq, PartialEq)]\nstruct InitialMessage<'a> {\n    protocol: &'a str,\n    version: i32,\n}\n\nstatic INITIAL_MESSAGE: InitialMessage<'static> = InitialMessage {\n    protocol: \"messagepack\",\n    version: 1,\n};\n\n// We attach the UUID to the sender so we can differentiate them when we need to remove them from the Vec\ntype UserSenders = (uuid::Uuid, Sender<Message>);\n#[derive(Clone)]\npub struct WebSocketUsers {\n    map: Arc<dashmap::DashMap<String, Vec<UserSenders>>>,\n}\n\nimpl WebSocketUsers {\n    async fn send_update(&self, user_id: &UserId, data: &[u8]) {\n        if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) {\n            for (_, sender) in user.iter() {\n                if let Err(e) = sender.send(Message::binary(data)).await {\n                    error!(\"Error sending WS update {e}\");\n                }\n            }\n        }\n    }\n\n    // NOTE: The last modified date needs to be updated before calling these methods\n    pub async fn send_user_update(&self, ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let data = create_update(\n            vec![(\"UserId\".into(), user.uuid.to_string().into()), (\"Date\".into(), serialize_date(user.updated_at))],\n            ut,\n            None,\n        );\n\n        if CONFIG.enable_websocket() {\n            self.send_update(&user.uuid, &data).await;\n        }\n\n        if CONFIG.push_enabled() {\n            push_user_update(ut, user, push_uuid, conn).await;\n        }\n    }\n\n    pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>, conn: &DbConn) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let data = create_update(\n            vec![(\"UserId\".into(), user.uuid.to_string().into()), (\"Date\".into(), serialize_date(user.updated_at))],\n            UpdateType::LogOut,\n            acting_device_id.clone(),\n        );\n\n        if CONFIG.enable_websocket() {\n            self.send_update(&user.uuid, &data).await;\n        }\n\n        if CONFIG.push_enabled() {\n            push_logout(user, acting_device_id.clone(), conn).await;\n        }\n    }\n\n    pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, device: &Device, conn: &DbConn) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let data = create_update(\n            vec![\n                (\"Id\".into(), folder.uuid.to_string().into()),\n                (\"UserId\".into(), folder.user_uuid.to_string().into()),\n                (\"RevisionDate\".into(), serialize_date(folder.updated_at)),\n            ],\n            ut,\n            Some(device.uuid.clone()),\n        );\n\n        if CONFIG.enable_websocket() {\n            self.send_update(&folder.user_uuid, &data).await;\n        }\n\n        if CONFIG.push_enabled() {\n            push_folder_update(ut, folder, device, conn).await;\n        }\n    }\n\n    pub async fn send_cipher_update(\n        &self,\n        ut: UpdateType,\n        cipher: &Cipher,\n        user_ids: &[UserId],\n        device: &Device,\n        collection_uuids: Option<Vec<CollectionId>>,\n        conn: &DbConn,\n    ) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let org_id = convert_option(cipher.organization_uuid.as_deref());\n        // Depending if there are collections provided or not, we need to have different values for the following variables.\n        // The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change.\n        let (user_id, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {\n            (\n                Value::Nil,\n                Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::<Vec<Value>>()),\n                serialize_date(Utc::now().naive_utc()),\n            )\n        } else {\n            (convert_option(cipher.user_uuid.as_deref()), Value::Nil, serialize_date(cipher.updated_at))\n        };\n\n        let data = create_update(\n            vec![\n                (\"Id\".into(), cipher.uuid.to_string().into()),\n                (\"UserId\".into(), user_id),\n                (\"OrganizationId\".into(), org_id),\n                (\"CollectionIds\".into(), collection_uuids),\n                (\"RevisionDate\".into(), revision_date),\n            ],\n            ut,\n            Some(device.uuid.clone()), // Acting device id (unique device/app uuid)\n        );\n\n        if CONFIG.enable_websocket() {\n            for uuid in user_ids {\n                self.send_update(uuid, &data).await;\n            }\n        }\n\n        if CONFIG.push_enabled() && user_ids.len() == 1 {\n            push_cipher_update(ut, cipher, device, conn).await;\n        }\n    }\n\n    pub async fn send_send_update(\n        &self,\n        ut: UpdateType,\n        send: &DbSend,\n        user_ids: &[UserId],\n        device: &Device,\n        conn: &DbConn,\n    ) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let user_id = convert_option(send.user_uuid.as_deref());\n\n        let data = create_update(\n            vec![\n                (\"Id\".into(), send.uuid.to_string().into()),\n                (\"UserId\".into(), user_id),\n                (\"RevisionDate\".into(), serialize_date(send.revision_date)),\n            ],\n            ut,\n            None,\n        );\n\n        if CONFIG.enable_websocket() {\n            for uuid in user_ids {\n                self.send_update(uuid, &data).await;\n            }\n        }\n        if CONFIG.push_enabled() && user_ids.len() == 1 {\n            push_send_update(ut, send, device, conn).await;\n        }\n    }\n\n    pub async fn send_auth_request(&self, user_id: &UserId, auth_request_uuid: &str, device: &Device, conn: &DbConn) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let data = create_update(\n            vec![(\"Id\".into(), auth_request_uuid.to_owned().into()), (\"UserId\".into(), user_id.to_string().into())],\n            UpdateType::AuthRequest,\n            Some(device.uuid.clone()),\n        );\n        if CONFIG.enable_websocket() {\n            self.send_update(user_id, &data).await;\n        }\n\n        if CONFIG.push_enabled() {\n            push_auth_request(user_id, auth_request_uuid, device, conn).await;\n        }\n    }\n\n    pub async fn send_auth_response(\n        &self,\n        user_id: &UserId,\n        auth_request_id: &AuthRequestId,\n        device: &Device,\n        conn: &DbConn,\n    ) {\n        // Skip any processing if both WebSockets and Push are not active\n        if *NOTIFICATIONS_DISABLED {\n            return;\n        }\n        let data = create_update(\n            vec![(\"Id\".into(), auth_request_id.to_string().into()), (\"UserId\".into(), user_id.to_string().into())],\n            UpdateType::AuthRequestResponse,\n            Some(device.uuid.clone()),\n        );\n        if CONFIG.enable_websocket() {\n            self.send_update(user_id, &data).await;\n        }\n\n        if CONFIG.push_enabled() {\n            push_auth_response(user_id, auth_request_id, device, conn).await;\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct AnonymousWebSocketSubscriptions {\n    map: Arc<dashmap::DashMap<String, Sender<Message>>>,\n}\n\nimpl AnonymousWebSocketSubscriptions {\n    async fn send_update(&self, token: &str, data: &[u8]) {\n        if let Some(sender) = self.map.get(token).map(|v| v.clone()) {\n            if let Err(e) = sender.send(Message::binary(data)).await {\n                error!(\"Error sending WS update {e}\");\n            }\n        }\n    }\n\n    pub async fn send_auth_response(&self, user_id: &UserId, auth_request_id: &AuthRequestId) {\n        if !CONFIG.enable_websocket() {\n            return;\n        }\n        let data = create_anonymous_update(\n            vec![(\"Id\".into(), auth_request_id.to_string().into()), (\"UserId\".into(), user_id.to_string().into())],\n            UpdateType::AuthRequestResponse,\n            user_id,\n        );\n        self.send_update(auth_request_id, &data).await;\n    }\n}\n\n/* Message Structure\n[\n    1, // MessageType.Invocation\n    {}, // Headers (map)\n    null, // InvocationId\n    \"ReceiveMessage\", // Target\n    [ // Arguments\n        {\n            \"ContextId\": acting_device_id || Nil,\n            \"Type\": ut as i32,\n            \"Payload\": {}\n        }\n    ]\n]\n*/\nfn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id: Option<DeviceId>) -> Vec<u8> {\n    use rmpv::Value as V;\n\n    let value = V::Array(vec![\n        1.into(),\n        V::Map(vec![]),\n        V::Nil,\n        \"ReceiveMessage\".into(),\n        V::Array(vec![V::Map(vec![\n            (\"ContextId\".into(), acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)),\n            (\"Type\".into(), (ut as i32).into()),\n            (\"Payload\".into(), payload.into()),\n        ])]),\n    ]);\n\n    serialize(&value)\n}\n\nfn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: &UserId) -> Vec<u8> {\n    use rmpv::Value as V;\n\n    let value = V::Array(vec![\n        1.into(),\n        V::Map(vec![]),\n        V::Nil,\n        // This word is misspelled, but upstream has this too\n        // https://github.com/bitwarden/server/blob/dff9f1cf538198819911cf2c20f8cda3307701c5/src/Notifications/HubHelpers.cs#L86\n        // https://github.com/bitwarden/clients/blob/9612a4ac45063e372a6fbe87eb253c7cb3c588fb/libs/common/src/auth/services/anonymous-hub.service.ts#L45\n        \"AuthRequestResponseRecieved\".into(),\n        V::Array(vec![V::Map(vec![\n            (\"Type\".into(), (ut as i32).into()),\n            (\"Payload\".into(), payload.into()),\n            (\"UserId\".into(), user_id.to_string().into()),\n        ])]),\n    ]);\n\n    serialize(&value)\n}\n\nfn create_ping() -> Vec<u8> {\n    serialize(&Value::Array(vec![6.into()]))\n}\n\n// https://github.com/bitwarden/server/blob/375af7c43b10d9da03525d41452f95de3f921541/src/Core/Enums/PushType.cs\n#[derive(Copy, Clone, Eq, PartialEq)]\npub enum UpdateType {\n    SyncCipherUpdate = 0,\n    SyncCipherCreate = 1,\n    SyncLoginDelete = 2,\n    SyncFolderDelete = 3,\n    SyncCiphers = 4,\n\n    SyncVault = 5,\n    SyncOrgKeys = 6,\n    SyncFolderCreate = 7,\n    SyncFolderUpdate = 8,\n    // SyncCipherDelete = 9, // Redirects to `SyncLoginDelete` on upstream\n    SyncSettings = 10,\n\n    LogOut = 11,\n\n    SyncSendCreate = 12,\n    SyncSendUpdate = 13,\n    SyncSendDelete = 14,\n\n    AuthRequest = 15,\n    AuthRequestResponse = 16,\n\n    // SyncOrganizations = 17, // Not supported\n    // SyncOrganizationStatusChanged = 18, // Not supported\n    // SyncOrganizationCollectionSettingChanged = 19, // Not supported\n\n    // Notification = 20, // Not supported\n    // NotificationStatus = 21, // Not supported\n\n    // RefreshSecurityTasks = 22, // Not supported\n    None = 100,\n}\n\npub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>;\npub type AnonymousNotify<'a> = &'a rocket::State<Arc<AnonymousWebSocketSubscriptions>>;\n"
  },
  {
    "path": "src/api/push.rs",
    "content": "use std::{\n    sync::LazyLock,\n    time::{Duration, Instant},\n};\n\nuse reqwest::{\n    header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},\n    Method,\n};\nuse serde_json::Value;\nuse tokio::sync::RwLock;\n\nuse crate::{\n    api::{ApiResult, EmptyResult, UpdateType},\n    db::{\n        models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},\n        DbConn,\n    },\n    http_client::make_http_request,\n    util::{format_date, get_uuid},\n    CONFIG,\n};\n\n#[derive(Deserialize)]\nstruct AuthPushToken {\n    access_token: String,\n    expires_in: i32,\n}\n\n#[derive(Debug)]\nstruct LocalAuthPushToken {\n    access_token: String,\n    valid_until: Instant,\n}\n\nasync fn get_auth_api_token() -> ApiResult<String> {\n    static API_TOKEN: LazyLock<RwLock<LocalAuthPushToken>> = LazyLock::new(|| {\n        RwLock::new(LocalAuthPushToken {\n            access_token: String::new(),\n            valid_until: Instant::now(),\n        })\n    });\n    let api_token = API_TOKEN.read().await;\n\n    if api_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {\n        debug!(\"Auth Push token still valid, no need for a new one\");\n        return Ok(api_token.access_token.clone());\n    }\n    drop(api_token); // Drop the read lock now\n\n    let installation_id = CONFIG.push_installation_id();\n    let client_id = format!(\"installation.{installation_id}\");\n    let client_secret = CONFIG.push_installation_key();\n\n    let params = [\n        (\"grant_type\", \"client_credentials\"),\n        (\"scope\", \"api.push\"),\n        (\"client_id\", &client_id),\n        (\"client_secret\", &client_secret),\n    ];\n\n    let res = match make_http_request(Method::POST, &format!(\"{}/connect/token\", CONFIG.push_identity_uri()))?\n        .form(&params)\n        .send()\n        .await\n    {\n        Ok(r) => r,\n        Err(e) => err!(format!(\"Error getting push token from bitwarden server: {e}\")),\n    };\n\n    let json_pushtoken = match res.json::<AuthPushToken>().await {\n        Ok(r) => r,\n        Err(e) => err!(format!(\"Unexpected push token received from bitwarden server: {e}\")),\n    };\n\n    let mut api_token = API_TOKEN.write().await;\n    api_token.valid_until = Instant::now()\n        .checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time\n        .unwrap();\n\n    api_token.access_token = json_pushtoken.access_token;\n\n    debug!(\"Token still valid for {}\", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs());\n    Ok(api_token.access_token.clone())\n}\n\npub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyResult {\n    if !CONFIG.push_enabled() || !device.is_push_device() {\n        return Ok(());\n    }\n\n    if device.push_token.is_none() {\n        warn!(\"Skipping the registration of the device {:?} because the push_token field is empty.\", device.uuid);\n        warn!(\"To get rid of this message you need to logout, clear the app data and login again on the device.\");\n        return Ok(());\n    }\n\n    debug!(\"Registering Device {:?}\", device.push_uuid);\n\n    // Generate a random push_uuid so if it doesn't already have one\n    if device.push_uuid.is_none() {\n        device.push_uuid = Some(PushId(get_uuid()));\n    }\n\n    //Needed to register a device for push to bitwarden :\n    let data = json!({\n        \"deviceId\": device.push_uuid, // Unique UUID per user/device\n        \"pushToken\": device.push_token,\n        \"userId\": device.user_uuid,\n        \"type\": device.atype,\n        \"identifier\": device.uuid,    // Unique UUID of the device/app, determined by the device/app it self currently registering\n        // \"organizationIds:\" [] // TODO: This is not yet implemented by Vaultwarden!\n        \"installationId\": CONFIG.push_installation_id(),\n    });\n\n    let auth_api_token = get_auth_api_token().await?;\n    let auth_header = format!(\"Bearer {auth_api_token}\");\n\n    if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + \"/push/register\"))?\n        .header(CONTENT_TYPE, \"application/json\")\n        .header(ACCEPT, \"application/json\")\n        .header(AUTHORIZATION, auth_header)\n        .json(&data)\n        .send()\n        .await?\n        .error_for_status()\n    {\n        err!(format!(\"An error occurred while proceeding registration of a device: {e}\"));\n    }\n\n    if let Err(e) = device.save(true, conn).await {\n        err!(format!(\"An error occurred while trying to save the (registered) device push uuid: {e}\"));\n    }\n\n    Ok(())\n}\n\npub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {\n    if !CONFIG.push_enabled() || push_id.is_none() {\n        return Ok(());\n    }\n    let auth_api_token = get_auth_api_token().await?;\n\n    let auth_header = format!(\"Bearer {auth_api_token}\");\n\n    match make_http_request(\n        Method::POST,\n        &format!(\"{}/push/delete/{}\", CONFIG.push_relay_uri(), push_id.as_ref().unwrap()),\n    )?\n    .header(AUTHORIZATION, auth_header)\n    .send()\n    .await\n    {\n        Ok(r) => r,\n        Err(e) => err!(format!(\"An error occurred during device unregistration: {e}\")),\n    };\n    Ok(())\n}\n\npub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &DbConn) {\n    // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.\n    if cipher.organization_uuid.is_some() {\n        return;\n    };\n    let Some(user_id) = &cipher.user_uuid else {\n        debug!(\"Cipher has no uuid\");\n        return;\n    };\n\n    if Device::check_user_has_push_device(user_id, conn).await {\n        send_to_push_relay(json!({\n            \"userId\": user_id,\n            \"organizationId\": null,\n            \"deviceId\": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)\n            \"identifier\": device.uuid, // Should be the acting device id (aka uuid per device/app)\n            \"type\": ut as i32,\n            \"payload\": {\n                \"id\": cipher.uuid,\n                \"userId\": cipher.user_uuid,\n                \"organizationId\": null,\n                \"collectionIds\": null,\n                \"revisionDate\": format_date(&cipher.updated_at)\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        }))\n        .await;\n    }\n}\n\npub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &DbConn) {\n    let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);\n\n    if Device::check_user_has_push_device(&user.uuid, conn).await {\n        tokio::task::spawn(send_to_push_relay(json!({\n            \"userId\": user.uuid,\n            \"organizationId\": (),\n            \"deviceId\": acting_device_id,\n            \"identifier\": acting_device_id,\n            \"type\": UpdateType::LogOut as i32,\n            \"payload\": {\n                \"userId\": user.uuid,\n                \"date\": format_date(&user.updated_at)\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        })));\n    }\n}\n\npub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &DbConn) {\n    if Device::check_user_has_push_device(&user.uuid, conn).await {\n        tokio::task::spawn(send_to_push_relay(json!({\n            \"userId\": user.uuid,\n            \"organizationId\": null,\n            \"deviceId\": push_uuid,\n            \"identifier\": null,\n            \"type\": ut as i32,\n            \"payload\": {\n                \"userId\": user.uuid,\n                \"date\": format_date(&user.updated_at)\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        })));\n    }\n}\n\npub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &DbConn) {\n    if Device::check_user_has_push_device(&folder.user_uuid, conn).await {\n        tokio::task::spawn(send_to_push_relay(json!({\n            \"userId\": folder.user_uuid,\n            \"organizationId\": null,\n            \"deviceId\": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)\n            \"identifier\": device.uuid, // Should be the acting device id (aka uuid per device/app)\n            \"type\": ut as i32,\n            \"payload\": {\n                \"id\": folder.uuid,\n                \"userId\": folder.user_uuid,\n                \"revisionDate\": format_date(&folder.updated_at)\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        })));\n    }\n}\n\npub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &DbConn) {\n    if let Some(s) = &send.user_uuid {\n        if Device::check_user_has_push_device(s, conn).await {\n            tokio::task::spawn(send_to_push_relay(json!({\n                \"userId\": send.user_uuid,\n                \"organizationId\": null,\n                \"deviceId\": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)\n                \"identifier\": device.uuid, // Should be the acting device id (aka uuid per device/app)\n                \"type\": ut as i32,\n                \"payload\": {\n                    \"id\": send.uuid,\n                    \"userId\": send.user_uuid,\n                    \"revisionDate\": format_date(&send.revision_date)\n                },\n                \"clientType\": null,\n                \"installationId\": null\n            })));\n        }\n    }\n}\n\nasync fn send_to_push_relay(notification_data: Value) {\n    if !CONFIG.push_enabled() {\n        return;\n    }\n\n    let auth_api_token = match get_auth_api_token().await {\n        Ok(s) => s,\n        Err(e) => {\n            debug!(\"Could not get the auth push token: {e}\");\n            return;\n        }\n    };\n\n    let auth_header = format!(\"Bearer {auth_api_token}\");\n\n    let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + \"/push/send\")) {\n        Ok(r) => r,\n        Err(e) => {\n            error!(\"An error occurred while sending a send update to the push relay: {e}\");\n            return;\n        }\n    };\n\n    if let Err(e) = req\n        .header(ACCEPT, \"application/json\")\n        .header(CONTENT_TYPE, \"application/json\")\n        .header(AUTHORIZATION, &auth_header)\n        .json(&notification_data)\n        .send()\n        .await\n    {\n        error!(\"An error occurred while sending a send update to the push relay: {e}\");\n    };\n}\n\npub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &DbConn) {\n    if Device::check_user_has_push_device(user_id, conn).await {\n        tokio::task::spawn(send_to_push_relay(json!({\n            \"userId\": user_id,\n            \"organizationId\": null,\n            \"deviceId\": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)\n            \"identifier\": device.uuid, // Should be the acting device id (aka uuid per device/app)\n            \"type\": UpdateType::AuthRequest as i32,\n            \"payload\": {\n                \"userId\": user_id,\n                \"id\": auth_request_id,\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        })));\n    }\n}\n\npub async fn push_auth_response(user_id: &UserId, auth_request_id: &AuthRequestId, device: &Device, conn: &DbConn) {\n    if Device::check_user_has_push_device(user_id, conn).await {\n        tokio::task::spawn(send_to_push_relay(json!({\n            \"userId\": user_id,\n            \"organizationId\": null,\n            \"deviceId\": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)\n            \"identifier\": device.uuid, // Should be the acting device id (aka uuid per device/app)\n            \"type\": UpdateType::AuthRequestResponse as i32,\n            \"payload\": {\n                \"userId\": user_id,\n                \"id\": auth_request_id,\n            },\n            \"clientType\": null,\n            \"installationId\": null\n        })));\n    }\n}\n"
  },
  {
    "path": "src/api/web.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse rocket::{\n    fs::NamedFile,\n    http::ContentType,\n    response::{content::RawCss as Css, content::RawHtml as Html, Redirect},\n    serde::json::Json,\n    Catcher, Route,\n};\nuse serde_json::Value;\n\nuse crate::{\n    api::{core::now, ApiResult, EmptyResult},\n    auth::decode_file_download,\n    db::models::{AttachmentId, CipherId},\n    error::Error,\n    util::Cached,\n    CONFIG,\n};\n\npub fn routes() -> Vec<Route> {\n    // If adding more routes here, consider also adding them to\n    // crate::utils::LOGGED_ROUTES to make sure they appear in the log\n    let mut routes = routes![attachments, alive, alive_head, static_files];\n    if CONFIG.web_vault_enabled() {\n        routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);\n    }\n\n    #[cfg(debug_assertions)]\n    if CONFIG.reload_templates() {\n        routes.append(&mut routes![_static_files_dev]);\n    }\n\n    routes\n}\n\npub fn catchers() -> Vec<Catcher> {\n    if CONFIG.web_vault_enabled() {\n        catchers![not_found]\n    } else {\n        catchers![]\n    }\n}\n\n#[catch(404)]\nfn not_found() -> ApiResult<Html<String>> {\n    // Return the page\n    let json = json!({\n        \"urlpath\": CONFIG.domain_path()\n    });\n    let text = CONFIG.render_template(\"404\", &json)?;\n    Ok(Html(text))\n}\n\n#[get(\"/css/vaultwarden.css\")]\nfn vaultwarden_css() -> Cached<Css<String>> {\n    let css_options = json!({\n        \"emergency_access_allowed\": CONFIG.emergency_access_allowed(),\n        \"load_user_scss\": true,\n        \"mail_2fa_enabled\": CONFIG._enable_email_2fa(),\n        \"mail_enabled\": CONFIG.mail_enabled(),\n        \"sends_allowed\": CONFIG.sends_allowed(),\n        \"remember_2fa_disabled\": CONFIG.disable_2fa_remember(),\n        \"password_hints_allowed\": CONFIG.password_hints_allowed(),\n        \"signup_disabled\": CONFIG.is_signup_disabled(),\n        \"sso_enabled\": CONFIG.sso_enabled(),\n        \"sso_only\": CONFIG.sso_enabled() && CONFIG.sso_only(),\n        \"webauthn_2fa_supported\": CONFIG.is_webauthn_2fa_supported(),\n        \"yubico_enabled\": CONFIG._enable_yubico() && CONFIG.yubico_client_id().is_some() && CONFIG.yubico_secret_key().is_some(),\n    });\n\n    let scss = match CONFIG.render_template(\"scss/vaultwarden.scss\", &css_options) {\n        Ok(t) => t,\n        Err(e) => {\n            // Something went wrong loading the template. Use the fallback\n            warn!(\"Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}\");\n            CONFIG\n                .render_fallback_template(\"scss/vaultwarden.scss\", &css_options)\n                .expect(\"Fallback scss/vaultwarden.scss.hbs to render\")\n        }\n    };\n\n    let css = match grass_compiler::from_string(\n        scss,\n        &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),\n    ) {\n        Ok(css) => css,\n        Err(e) => {\n            // Something went wrong compiling the scss. Use the fallback\n            warn!(\"Compiling the Vaultwarden SCSS styles failed. {e}\");\n            let mut css_options = css_options;\n            css_options[\"load_user_scss\"] = json!(false);\n            let scss = CONFIG\n                .render_fallback_template(\"scss/vaultwarden.scss\", &css_options)\n                .expect(\"Fallback scss/vaultwarden.scss.hbs to render\");\n            grass_compiler::from_string(\n                scss,\n                &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),\n            )\n            .expect(\"SCSS to compile\")\n        }\n    };\n\n    // Cache for one day should be enough and not too much\n    Cached::ttl(Css(css), 86_400, false)\n}\n\n#[get(\"/\")]\nasync fn web_index() -> Cached<Option<NamedFile>> {\n    Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(\"index.html\")).await.ok(), false)\n}\n\n// Make sure that `/index.html` redirect to actual domain path.\n// If not, this might cause issues with the web-vault\n#[get(\"/index.html\")]\nfn web_index_direct() -> Redirect {\n    Redirect::to(format!(\"{}/\", CONFIG.domain_path()))\n}\n\n#[head(\"/\")]\nfn web_index_head() -> EmptyResult {\n    // Add an explicit HEAD route to prevent uptime monitoring services from\n    // generating \"No matching routes for HEAD /\" error messages.\n    //\n    // Rocket automatically implements a HEAD route when there's a matching GET\n    // route, but relying on this behavior also means a spurious error gets\n    // logged due to <https://github.com/SergioBenitez/Rocket/issues/1098>.\n    Ok(())\n}\n\n#[get(\"/app-id.json\")]\nfn app_id() -> Cached<(ContentType, Json<Value>)> {\n    let content_type = ContentType::new(\"application\", \"fido.trusted-apps+json\");\n\n    Cached::long(\n        (\n            content_type,\n            Json(json!({\n            \"trustedFacets\": [\n                {\n                \"version\": { \"major\": 1, \"minor\": 0 },\n                \"ids\": [\n                    // Per <https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-the-facetid-of-a-calling-application>:\n                    //\n                    // \"In the Web case, the FacetID MUST be the Web Origin [RFC6454]\n                    // of the web page triggering the FIDO operation, written as\n                    // a URI with an empty path. Default ports are omitted and any\n                    // path component is ignored.\"\n                    //\n                    // This leaves it unclear as to whether the path must be empty,\n                    // or whether it can be non-empty and will be ignored. To be on\n                    // the safe side, use a proper web origin (with empty path).\n                    &CONFIG.domain_origin(),\n                    \"ios:bundle-id:com.8bit.bitwarden\",\n                    \"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI\" ]\n                }]\n            })),\n        ),\n        true,\n    )\n}\n\n#[get(\"/<p..>\", rank = 10)] // Only match this if the other routes don't match\nasync fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {\n    Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)\n}\n\n#[get(\"/attachments/<cipher_id>/<file_id>?<token>\")]\nasync fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) -> Option<NamedFile> {\n    let Ok(claims) = decode_file_download(&token) else {\n        return None;\n    };\n    if claims.sub != cipher_id || claims.file_id != file_id {\n        return None;\n    }\n\n    NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(cipher_id.as_ref()).join(file_id.as_ref())).await.ok()\n}\n\n// We use DbConn here to let the alive healthcheck also verify the database connection.\nuse crate::db::DbConn;\n#[get(\"/alive\")]\nfn alive(_conn: DbConn) -> Json<String> {\n    now()\n}\n\n#[head(\"/alive\")]\nfn alive_head(_conn: DbConn) -> EmptyResult {\n    // Avoid logging spurious \"No matching routes for HEAD /alive\" errors\n    // due to <https://github.com/SergioBenitez/Rocket/issues/1098>.\n    Ok(())\n}\n\n// This endpoint/function is used during development and development only.\n// It allows to easily develop the admin interface by always loading the files from disk instead from a slice of bytes\n// This will only be active during a debug build and only when `RELOAD_TEMPLATES` is set to `true`\n// NOTE: Do not forget to add any new files added to the `static_files` function below!\n#[cfg(debug_assertions)]\n#[get(\"/vw_static/<filename>\", rank = 1)]\npub async fn _static_files_dev(filename: PathBuf) -> Option<NamedFile> {\n    warn!(\"LOADING STATIC FILES FROM DISK\");\n    let file = filename.to_str().unwrap_or_default();\n    let ext = filename.extension().unwrap_or_default();\n\n    let path = if ext == \"png\" || ext == \"svg\" {\n        tokio::fs::canonicalize(Path::new(file!()).parent().unwrap().join(\"../static/images/\").join(file)).await\n    } else {\n        tokio::fs::canonicalize(Path::new(file!()).parent().unwrap().join(\"../static/scripts/\").join(file)).await\n    };\n\n    if let Ok(path) = path {\n        return NamedFile::open(path).await.ok();\n    };\n    None\n}\n\n#[get(\"/vw_static/<filename>\", rank = 2)]\npub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {\n    match filename {\n        \"404.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/404.png\"))),\n        \"mail-github.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/mail-github.png\"))),\n        \"logo-gray.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/logo-gray.png\"))),\n        \"error-x.svg\" => Ok((ContentType::SVG, include_bytes!(\"../static/images/error-x.svg\"))),\n        \"hibp.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/hibp.png\"))),\n        \"vaultwarden-icon.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/vaultwarden-icon.png\"))),\n        \"vaultwarden-favicon.png\" => Ok((ContentType::PNG, include_bytes!(\"../static/images/vaultwarden-favicon.png\"))),\n        \"404.css\" => Ok((ContentType::CSS, include_bytes!(\"../static/scripts/404.css\"))),\n        \"admin.css\" => Ok((ContentType::CSS, include_bytes!(\"../static/scripts/admin.css\"))),\n        \"admin.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/admin.js\"))),\n        \"admin_settings.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/admin_settings.js\"))),\n        \"admin_users.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/admin_users.js\"))),\n        \"admin_organizations.js\" => {\n            Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/admin_organizations.js\")))\n        }\n        \"admin_diagnostics.js\" => {\n            Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/admin_diagnostics.js\")))\n        }\n        \"bootstrap.css\" => Ok((ContentType::CSS, include_bytes!(\"../static/scripts/bootstrap.css\"))),\n        \"bootstrap.bundle.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/bootstrap.bundle.js\"))),\n        \"jdenticon-3.3.0.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/jdenticon-3.3.0.js\"))),\n        \"datatables.js\" => Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/datatables.js\"))),\n        \"datatables.css\" => Ok((ContentType::CSS, include_bytes!(\"../static/scripts/datatables.css\"))),\n        \"jquery-4.0.0.slim.js\" => {\n            Ok((ContentType::JavaScript, include_bytes!(\"../static/scripts/jquery-4.0.0.slim.js\")))\n        }\n        _ => err!(format!(\"Static file not found: {filename}\")),\n    }\n}\n"
  },
  {
    "path": "src/auth.rs",
    "content": "use std::{\n    env,\n    net::IpAddr,\n    sync::{LazyLock, OnceLock},\n};\n\nuse chrono::{DateTime, TimeDelta, Utc};\nuse jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};\nuse num_traits::FromPrimitive;\nuse openssl::rsa::Rsa;\nuse serde::de::DeserializeOwned;\nuse serde::ser::Serialize;\n\nuse crate::{\n    api::ApiResult,\n    config::PathType,\n    db::models::{\n        AttachmentId, CipherId, CollectionId, DeviceId, DeviceType, EmergencyAccessId, MembershipId, OrgApiKeyId,\n        OrganizationId, SendFileId, SendId, UserId,\n    },\n    error::Error,\n    sso, CONFIG,\n};\n\nconst JWT_ALGORITHM: Algorithm = Algorithm::RS256;\n\n// Limit when BitWarden consider the token as expired\npub static BW_EXPIRATION: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_minutes(5).unwrap());\n\npub static DEFAULT_REFRESH_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_days(30).unwrap());\npub static MOBILE_REFRESH_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_days(90).unwrap());\npub static DEFAULT_ACCESS_VALIDITY: LazyLock<TimeDelta> = LazyLock::new(|| TimeDelta::try_hours(2).unwrap());\nstatic JWT_HEADER: LazyLock<Header> = LazyLock::new(|| Header::new(JWT_ALGORITHM));\n\npub static JWT_LOGIN_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|login\", CONFIG.domain_origin()));\nstatic JWT_INVITE_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|invite\", CONFIG.domain_origin()));\nstatic JWT_EMERGENCY_ACCESS_INVITE_ISSUER: LazyLock<String> =\n    LazyLock::new(|| format!(\"{}|emergencyaccessinvite\", CONFIG.domain_origin()));\nstatic JWT_DELETE_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|delete\", CONFIG.domain_origin()));\nstatic JWT_VERIFYEMAIL_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|verifyemail\", CONFIG.domain_origin()));\nstatic JWT_ADMIN_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|admin\", CONFIG.domain_origin()));\nstatic JWT_SEND_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|send\", CONFIG.domain_origin()));\nstatic JWT_ORG_API_KEY_ISSUER: LazyLock<String> =\n    LazyLock::new(|| format!(\"{}|api.organization\", CONFIG.domain_origin()));\nstatic JWT_FILE_DOWNLOAD_ISSUER: LazyLock<String> =\n    LazyLock::new(|| format!(\"{}|file_download\", CONFIG.domain_origin()));\nstatic JWT_REGISTER_VERIFY_ISSUER: LazyLock<String> =\n    LazyLock::new(|| format!(\"{}|register_verify\", CONFIG.domain_origin()));\n\nstatic PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new();\nstatic PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();\n\npub async fn initialize_keys() -> Result<(), Error> {\n    use std::io::Error;\n\n    let rsa_key_filename = std::path::PathBuf::from(CONFIG.private_rsa_key())\n        .file_name()\n        .ok_or_else(|| Error::other(\"Private RSA key path missing filename\"))?\n        .to_str()\n        .ok_or_else(|| Error::other(\"Private RSA key path filename is not valid UTF-8\"))?\n        .to_string();\n\n    let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?;\n\n    let priv_key_buffer = match operator.read(&rsa_key_filename).await {\n        Ok(buffer) => Some(buffer),\n        Err(e) if e.kind() == opendal::ErrorKind::NotFound => None,\n        Err(e) => return Err(e.into()),\n    };\n\n    let (priv_key, priv_key_buffer) = if let Some(priv_key_buffer) = priv_key_buffer {\n        (Rsa::private_key_from_pem(priv_key_buffer.to_vec().as_slice())?, priv_key_buffer.to_vec())\n    } else {\n        let rsa_key = Rsa::generate(2048)?;\n        let priv_key_buffer = rsa_key.private_key_to_pem()?;\n        operator.write(&rsa_key_filename, priv_key_buffer.clone()).await?;\n        info!(\"Private key '{}' created correctly\", CONFIG.private_rsa_key());\n        (rsa_key, priv_key_buffer)\n    };\n    let pub_key_buffer = priv_key.public_key_to_pem()?;\n\n    let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;\n    let dec: DecodingKey = DecodingKey::from_rsa_pem(&pub_key_buffer)?;\n    if PRIVATE_RSA_KEY.set(enc).is_err() {\n        err!(\"PRIVATE_RSA_KEY must only be initialized once\")\n    }\n    if PUBLIC_RSA_KEY.set(dec).is_err() {\n        err!(\"PUBLIC_RSA_KEY must only be initialized once\")\n    }\n    Ok(())\n}\n\npub fn encode_jwt<T: Serialize>(claims: &T) -> String {\n    match jsonwebtoken::encode(&JWT_HEADER, claims, PRIVATE_RSA_KEY.wait()) {\n        Ok(token) => token,\n        Err(e) => panic!(\"Error encoding jwt {e}\"),\n    }\n}\n\npub fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Error> {\n    let mut validation = jsonwebtoken::Validation::new(JWT_ALGORITHM);\n    validation.leeway = 30; // 30 seconds\n    validation.validate_exp = true;\n    validation.validate_nbf = true;\n    validation.set_issuer(&[issuer]);\n\n    let token = token.replace(char::is_whitespace, \"\");\n    match jsonwebtoken::decode(&token, PUBLIC_RSA_KEY.wait(), &validation) {\n        Ok(d) => Ok(d.claims),\n        Err(err) => match *err.kind() {\n            ErrorKind::InvalidToken => err!(\"Token is invalid\"),\n            ErrorKind::InvalidIssuer => err!(\"Issuer is invalid\"),\n            ErrorKind::ExpiredSignature => err!(\"Token has expired\"),\n            _ => err!(format!(\"Error decoding JWT: {:?}\", err)),\n        },\n    }\n}\n\npub fn decode_refresh(token: &str) -> Result<RefreshJwtClaims, Error> {\n    decode_jwt(token, JWT_LOGIN_ISSUER.to_string())\n}\n\npub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {\n    decode_jwt(token, JWT_LOGIN_ISSUER.to_string())\n}\n\npub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> {\n    decode_jwt(token, JWT_INVITE_ISSUER.to_string())\n}\n\npub fn decode_emergency_access_invite(token: &str) -> Result<EmergencyAccessInviteJwtClaims, Error> {\n    decode_jwt(token, JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string())\n}\n\npub fn decode_delete(token: &str) -> Result<BasicJwtClaims, Error> {\n    decode_jwt(token, JWT_DELETE_ISSUER.to_string())\n}\n\npub fn decode_verify_email(token: &str) -> Result<BasicJwtClaims, Error> {\n    decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())\n}\n\npub fn decode_admin(token: &str) -> Result<BasicJwtClaims, Error> {\n    decode_jwt(token, JWT_ADMIN_ISSUER.to_string())\n}\n\npub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {\n    decode_jwt(token, JWT_SEND_ISSUER.to_string())\n}\n\npub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> {\n    decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string())\n}\n\npub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> {\n    decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())\n}\n\npub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error> {\n    decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct LoginJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: UserId,\n\n    pub premium: bool,\n    pub name: String,\n    pub email: String,\n    pub email_verified: bool,\n\n    // ---\n    // Disabled these keys to be added to the JWT since they could cause the JWT to get too large\n    // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients\n    // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out\n    // See: https://github.com/dani-garcia/vaultwarden/issues/4156\n    // ---\n    // pub orgowner: Vec<String>,\n    // pub orgadmin: Vec<String>,\n    // pub orguser: Vec<String>,\n    // pub orgmanager: Vec<String>,\n\n    // user security_stamp\n    pub sstamp: String,\n    // device uuid\n    pub device: DeviceId,\n    // what kind of device, like FirefoxBrowser or Android derived from DeviceType\n    pub devicetype: String,\n    // the type of client_id, like web, cli, desktop, browser or mobile\n    pub client_id: String,\n\n    // [ \"api\", \"offline_access\" ]\n    pub scope: Vec<String>,\n    // [ \"Application\" ]\n    pub amr: Vec<String>,\n}\n\nimpl LoginJwtClaims {\n    pub fn new(\n        device: &Device,\n        user: &User,\n        nbf: i64,\n        exp: i64,\n        scope: Vec<String>,\n        client_id: Option<String>,\n        now: DateTime<Utc>,\n    ) -> Self {\n        // ---\n        // Disabled these keys to be added to the JWT since they could cause the JWT to get too large\n        // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients\n        // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out\n        // ---\n        // fn arg: orgs: Vec<super::UserOrganization>,\n        // ---\n        // let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();\n        // let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();\n        // let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();\n        // let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();\n\n        if exp <= (now + *BW_EXPIRATION).timestamp() {\n            warn!(\"Raise access_token lifetime to more than 5min.\")\n        }\n\n        // Create the JWT claims struct, to send to the client\n        Self {\n            nbf,\n            exp,\n            iss: JWT_LOGIN_ISSUER.to_string(),\n            sub: user.uuid.clone(),\n            premium: true,\n            name: user.name.clone(),\n            email: user.email.clone(),\n            email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),\n\n            // ---\n            // Disabled these keys to be added to the JWT since they could cause the JWT to get too large\n            // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients\n            // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out\n            // See: https://github.com/dani-garcia/vaultwarden/issues/4156\n            // ---\n            // orgowner,\n            // orgadmin,\n            // orguser,\n            // orgmanager,\n            sstamp: user.security_stamp.clone(),\n            device: device.uuid.clone(),\n            devicetype: DeviceType::from_i32(device.atype).to_string(),\n            client_id: client_id.unwrap_or(\"undefined\".to_string()),\n            scope,\n            amr: vec![\"Application\".into()],\n        }\n    }\n\n    pub fn default(device: &Device, user: &User, auth_method: &AuthMethod, client_id: Option<String>) -> Self {\n        let time_now = Utc::now();\n        Self::new(\n            device,\n            user,\n            time_now.timestamp(),\n            (time_now + *DEFAULT_ACCESS_VALIDITY).timestamp(),\n            auth_method.scope_vec(),\n            client_id,\n            time_now,\n        )\n    }\n\n    pub fn token(&self) -> String {\n        encode_jwt(&self)\n    }\n\n    pub fn expires_in(&self) -> i64 {\n        self.exp - Utc::now().timestamp()\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct InviteJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: UserId,\n\n    pub email: String,\n    pub org_id: OrganizationId,\n    pub member_id: MembershipId,\n    pub invited_by_email: Option<String>,\n}\n\npub fn generate_invite_claims(\n    user_id: UserId,\n    email: String,\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    invited_by_email: Option<String>,\n) -> InviteJwtClaims {\n    let time_now = Utc::now();\n    let expire_hours = i64::from(CONFIG.invitation_expiration_hours());\n    InviteJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),\n        iss: JWT_INVITE_ISSUER.to_string(),\n        sub: user_id,\n        email,\n        org_id,\n        member_id,\n        invited_by_email,\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct EmergencyAccessInviteJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: UserId,\n\n    pub email: String,\n    pub emer_id: EmergencyAccessId,\n    pub grantor_name: String,\n    pub grantor_email: String,\n}\n\npub fn generate_emergency_access_invite_claims(\n    user_id: UserId,\n    email: String,\n    emer_id: EmergencyAccessId,\n    grantor_name: String,\n    grantor_email: String,\n) -> EmergencyAccessInviteJwtClaims {\n    let time_now = Utc::now();\n    let expire_hours = i64::from(CONFIG.invitation_expiration_hours());\n    EmergencyAccessInviteJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),\n        iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),\n        sub: user_id,\n        email,\n        emer_id,\n        grantor_name,\n        grantor_email,\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct OrgApiKeyLoginJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: OrgApiKeyId,\n\n    pub client_id: String,\n    pub client_sub: OrganizationId,\n    pub scope: Vec<String>,\n}\n\npub fn generate_organization_api_key_login_claims(\n    org_api_key_uuid: OrgApiKeyId,\n    org_id: OrganizationId,\n) -> OrgApiKeyLoginJwtClaims {\n    let time_now = Utc::now();\n    OrgApiKeyLoginJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(),\n        iss: JWT_ORG_API_KEY_ISSUER.to_string(),\n        sub: org_api_key_uuid,\n        client_id: format!(\"organization.{org_id}\"),\n        client_sub: org_id,\n        scope: vec![\"api.organization\".into()],\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct FileDownloadClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: CipherId,\n\n    pub file_id: AttachmentId,\n}\n\npub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId) -> FileDownloadClaims {\n    let time_now = Utc::now();\n    FileDownloadClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(),\n        iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(),\n        sub: cipher_id,\n        file_id,\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct RegisterVerifyClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: String,\n\n    pub name: Option<String>,\n    pub verified: bool,\n}\n\npub fn generate_register_verify_claims(email: String, name: Option<String>, verified: bool) -> RegisterVerifyClaims {\n    let time_now = Utc::now();\n    RegisterVerifyClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_minutes(30).unwrap()).timestamp(),\n        iss: JWT_REGISTER_VERIFY_ISSUER.to_string(),\n        sub: email,\n        name,\n        verified,\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct BasicJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: String,\n}\n\npub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {\n    let time_now = Utc::now();\n    let expire_hours = i64::from(CONFIG.invitation_expiration_hours());\n    BasicJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),\n        iss: JWT_DELETE_ISSUER.to_string(),\n        sub: uuid,\n    }\n}\n\npub fn generate_verify_email_claims(user_id: &UserId) -> BasicJwtClaims {\n    let time_now = Utc::now();\n    let expire_hours = i64::from(CONFIG.invitation_expiration_hours());\n    BasicJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),\n        iss: JWT_VERIFYEMAIL_ISSUER.to_string(),\n        sub: user_id.to_string(),\n    }\n}\n\npub fn generate_admin_claims() -> BasicJwtClaims {\n    let time_now = Utc::now();\n    BasicJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_minutes(CONFIG.admin_session_lifetime()).unwrap()).timestamp(),\n        iss: JWT_ADMIN_ISSUER.to_string(),\n        sub: \"admin_panel\".to_string(),\n    }\n}\n\npub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims {\n    let time_now = Utc::now();\n    BasicJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + TimeDelta::try_minutes(2).unwrap()).timestamp(),\n        iss: JWT_SEND_ISSUER.to_string(),\n        sub: format!(\"{send_id}/{file_id}\"),\n    }\n}\n\n//\n// Bearer token authentication\n//\nuse rocket::{\n    outcome::try_outcome,\n    request::{FromRequest, Outcome, Request},\n};\n\nuse crate::db::{\n    models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException},\n    DbConn,\n};\n\npub struct Host {\n    pub host: String,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for Host {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n\n        // Get host\n        let host = if CONFIG.domain_set() {\n            CONFIG.domain()\n        } else if let Some(referer) = headers.get_one(\"Referer\") {\n            referer.to_string()\n        } else {\n            // Try to guess from the headers\n            let protocol = if let Some(proto) = headers.get_one(\"X-Forwarded-Proto\") {\n                proto\n            } else if env::var(\"ROCKET_TLS\").is_ok() {\n                \"https\"\n            } else {\n                \"http\"\n            };\n\n            let host = if let Some(host) = headers.get_one(\"X-Forwarded-Host\") {\n                host\n            } else {\n                headers.get_one(\"Host\").unwrap_or_default()\n            };\n\n            format!(\"{protocol}://{host}\")\n        };\n\n        Outcome::Success(Host {\n            host,\n        })\n    }\n}\n\npub struct ClientHeaders {\n    pub device_type: i32,\n    pub ip: ClientIp,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for ClientHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let ip = match ClientIp::from_request(request).await {\n            Outcome::Success(ip) => ip,\n            _ => err_handler!(\"Error getting Client IP\"),\n        };\n        // When unknown or unable to parse, return 14, which is 'Unknown Browser'\n        let device_type: i32 =\n            request.headers().get_one(\"device-type\").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14);\n\n        Outcome::Success(ClientHeaders {\n            device_type,\n            ip,\n        })\n    }\n}\n\npub struct Headers {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub ip: ClientIp,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for Headers {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n\n        let host = try_outcome!(Host::from_request(request).await).host;\n        let ip = match ClientIp::from_request(request).await {\n            Outcome::Success(ip) => ip,\n            _ => err_handler!(\"Error getting Client IP\"),\n        };\n\n        // Get access_token\n        let access_token: &str = match headers.get_one(\"Authorization\") {\n            Some(a) => match a.rsplit(\"Bearer \").next() {\n                Some(split) => split,\n                None => err_handler!(\"No access token provided\"),\n            },\n            None => err_handler!(\"No access token provided\"),\n        };\n\n        // Check JWT token is valid and get device and user from it\n        let Ok(claims) = decode_login(access_token) else {\n            err_handler!(\"Invalid claim\")\n        };\n\n        let device_id = claims.device;\n        let user_id = claims.sub;\n\n        let conn = match DbConn::from_request(request).await {\n            Outcome::Success(conn) => conn,\n            _ => err_handler!(\"Error getting DB\"),\n        };\n\n        let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &conn).await else {\n            err_handler!(\"Invalid device id\")\n        };\n\n        let Some(user) = User::find_by_uuid(&user_id, &conn).await else {\n            err_handler!(\"Device has no user associated\")\n        };\n\n        if user.security_stamp != claims.sstamp {\n            if let Some(stamp_exception) =\n                user.stamp_exception.as_deref().and_then(|s| serde_json::from_str::<UserStampException>(s).ok())\n            {\n                let Some(current_route) = request.route().and_then(|r| r.name.as_deref()) else {\n                    err_handler!(\"Error getting current route for stamp exception\")\n                };\n\n                // Check if the stamp exception has expired first.\n                // Then, check if the current route matches any of the allowed routes.\n                // After that check the stamp in exception matches the one in the claims.\n                if Utc::now().timestamp() > stamp_exception.expire {\n                    // If the stamp exception has been expired remove it from the database.\n                    // This prevents checking this stamp exception for new requests.\n                    let mut user = user;\n                    user.reset_stamp_exception();\n                    if let Err(e) = user.save(&conn).await {\n                        error!(\"Error updating user: {e:#?}\");\n                    }\n                    err_handler!(\"Stamp exception is expired\")\n                } else if !stamp_exception.routes.contains(&current_route.to_string()) {\n                    err_handler!(\"Invalid security stamp: Current route and exception route do not match\")\n                } else if stamp_exception.security_stamp != claims.sstamp {\n                    err_handler!(\"Invalid security stamp for matched stamp exception\")\n                }\n            } else {\n                err_handler!(\"Invalid security stamp\")\n            }\n        }\n\n        Outcome::Success(Headers {\n            host,\n            device,\n            user,\n            ip,\n        })\n    }\n}\n\npub struct OrgHeaders {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub membership_type: MembershipType,\n    pub membership_status: MembershipStatus,\n    pub membership: Membership,\n    pub ip: ClientIp,\n}\n\nimpl OrgHeaders {\n    fn is_member(&self) -> bool {\n        // NOTE: we don't care about MembershipStatus at the moment because this is only used\n        // where an invited, accepted or confirmed user is expected if this ever changes or\n        // if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly\n        self.membership_type >= MembershipType::User\n    }\n    fn is_confirmed_and_admin(&self) -> bool {\n        self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin\n    }\n    fn is_confirmed_and_manager(&self) -> bool {\n        self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Manager\n    }\n    fn is_confirmed_and_owner(&self) -> bool {\n        self.membership_status == MembershipStatus::Confirmed && self.membership_type == MembershipType::Owner\n    }\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for OrgHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(Headers::from_request(request).await);\n\n        // org_id is usually the second path param (\"/organizations/<org_id>\"),\n        // but there are cases where it is a query value.\n        // First check the path, if this is not a valid uuid, try the query values.\n        let url_org_id: Option<OrganizationId> = {\n            if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {\n                Some(org_id)\n            } else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>(\"organizationId\") {\n                Some(org_id)\n            } else {\n                None\n            }\n        };\n\n        match url_org_id {\n            Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {\n                let conn = match DbConn::from_request(request).await {\n                    Outcome::Success(conn) => conn,\n                    _ => err_handler!(\"Error getting DB\"),\n                };\n\n                let user = headers.user;\n                let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org_id, &conn).await else {\n                    err_handler!(\"The current user isn't member of the organization\");\n                };\n\n                Outcome::Success(Self {\n                    host: headers.host,\n                    device: headers.device,\n                    user,\n                    membership_type: {\n                        if let Some(member_type) = MembershipType::from_i32(membership.atype) {\n                            member_type\n                        } else {\n                            // This should only happen if the DB is corrupted\n                            err_handler!(\"Unknown user type in the database\")\n                        }\n                    },\n                    membership_status: {\n                        if let Some(member_status) = MembershipStatus::from_i32(membership.status) {\n                            // NOTE: add additional check for revoked if from_i32 is ever changed\n                            // to return Revoked status.\n                            member_status\n                        } else {\n                            err_handler!(\"User status is either revoked or invalid.\")\n                        }\n                    },\n                    membership,\n                    ip: headers.ip,\n                })\n            }\n            _ => err_handler!(\"Error getting the organization id\"),\n        }\n    }\n}\n\npub struct AdminHeaders {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub membership_type: MembershipType,\n    pub ip: ClientIp,\n    pub org_id: OrganizationId,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for AdminHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(OrgHeaders::from_request(request).await);\n        if headers.is_confirmed_and_admin() {\n            Outcome::Success(Self {\n                host: headers.host,\n                device: headers.device,\n                user: headers.user,\n                membership_type: headers.membership_type,\n                ip: headers.ip,\n                org_id: headers.membership.org_uuid,\n            })\n        } else {\n            err_handler!(\"You need to be Admin or Owner to call this endpoint\")\n        }\n    }\n}\n\n// col_id is usually the fourth path param (\"/organizations/<org_id>/collections/<col_id>\"),\n// but there could be cases where it is a query value.\n// First check the path, if this is not a valid uuid, try the query values.\nfn get_col_id(request: &Request<'_>) -> Option<CollectionId> {\n    if let Some(Ok(col_id)) = request.param::<String>(3) {\n        if uuid::Uuid::parse_str(&col_id).is_ok() {\n            return Some(col_id.into());\n        }\n    }\n\n    if let Some(Ok(col_id)) = request.query_value::<String>(\"collectionId\") {\n        if uuid::Uuid::parse_str(&col_id).is_ok() {\n            return Some(col_id.into());\n        }\n    }\n\n    None\n}\n\n/// The ManagerHeaders are used to check if you are at least a Manager\n/// and have access to the specific collection provided via the <col_id>/collections/collectionId.\n/// This does strict checking on the collection_id, ManagerHeadersLoose does not.\npub struct ManagerHeaders {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub ip: ClientIp,\n    pub org_id: OrganizationId,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for ManagerHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(OrgHeaders::from_request(request).await);\n        if headers.is_confirmed_and_manager() {\n            match get_col_id(request) {\n                Some(col_id) => {\n                    let conn = match DbConn::from_request(request).await {\n                        Outcome::Success(conn) => conn,\n                        _ => err_handler!(\"Error getting DB\"),\n                    };\n\n                    if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await {\n                        err_handler!(\"The current user isn't a manager for this collection\")\n                    }\n                }\n                _ => err_handler!(\"Error getting the collection id\"),\n            }\n\n            Outcome::Success(Self {\n                host: headers.host,\n                device: headers.device,\n                user: headers.user,\n                ip: headers.ip,\n                org_id: headers.membership.org_uuid,\n            })\n        } else {\n            err_handler!(\"You need to be a Manager, Admin or Owner to call this endpoint\")\n        }\n    }\n}\n\nimpl From<ManagerHeaders> for Headers {\n    fn from(h: ManagerHeaders) -> Headers {\n        Headers {\n            host: h.host,\n            device: h.device,\n            user: h.user,\n            ip: h.ip,\n        }\n    }\n}\n\n/// The ManagerHeadersLoose is used when you at least need to be a Manager,\n/// but there is no collection_id sent with the request (either in the path or as form data).\npub struct ManagerHeadersLoose {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub membership: Membership,\n    pub ip: ClientIp,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for ManagerHeadersLoose {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(OrgHeaders::from_request(request).await);\n        if headers.is_confirmed_and_manager() {\n            Outcome::Success(Self {\n                host: headers.host,\n                device: headers.device,\n                user: headers.user,\n                membership: headers.membership,\n                ip: headers.ip,\n            })\n        } else {\n            err_handler!(\"You need to be a Manager, Admin or Owner to call this endpoint\")\n        }\n    }\n}\n\nimpl From<ManagerHeadersLoose> for Headers {\n    fn from(h: ManagerHeadersLoose) -> Headers {\n        Headers {\n            host: h.host,\n            device: h.device,\n            user: h.user,\n            ip: h.ip,\n        }\n    }\n}\n\nimpl ManagerHeaders {\n    pub async fn from_loose(\n        h: ManagerHeadersLoose,\n        collections: &Vec<CollectionId>,\n        conn: &DbConn,\n    ) -> Result<ManagerHeaders, Error> {\n        for col_id in collections {\n            if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {\n                err!(\"Collection Id is malformed!\");\n            }\n            if !Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await {\n                err!(\"Collection not found\", \"The current user isn't a manager for this collection\")\n            }\n        }\n\n        Ok(ManagerHeaders {\n            host: h.host,\n            device: h.device,\n            user: h.user,\n            ip: h.ip,\n            org_id: h.membership.org_uuid,\n        })\n    }\n}\n\npub struct OwnerHeaders {\n    pub device: Device,\n    pub user: User,\n    pub ip: ClientIp,\n    pub org_id: OrganizationId,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for OwnerHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(OrgHeaders::from_request(request).await);\n        if headers.is_confirmed_and_owner() {\n            Outcome::Success(Self {\n                device: headers.device,\n                user: headers.user,\n                ip: headers.ip,\n                org_id: headers.membership.org_uuid,\n            })\n        } else {\n            err_handler!(\"You need to be Owner to call this endpoint\")\n        }\n    }\n}\n\npub struct OrgMemberHeaders {\n    pub host: String,\n    pub device: Device,\n    pub user: User,\n    pub membership: Membership,\n    pub ip: ClientIp,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for OrgMemberHeaders {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = try_outcome!(OrgHeaders::from_request(request).await);\n        if headers.is_member() {\n            Outcome::Success(Self {\n                host: headers.host,\n                device: headers.device,\n                user: headers.user,\n                membership: headers.membership,\n                ip: headers.ip,\n            })\n        } else {\n            err_handler!(\"You need to be a Member of the Organization to call this endpoint\")\n        }\n    }\n}\n\nimpl From<OrgMemberHeaders> for Headers {\n    fn from(h: OrgMemberHeaders) -> Headers {\n        Headers {\n            host: h.host,\n            device: h.device,\n            user: h.user,\n            ip: h.ip,\n        }\n    }\n}\n\n//\n// Client IP address detection\n//\n\npub struct ClientIp {\n    pub ip: IpAddr,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for ClientIp {\n    type Error = ();\n\n    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let ip = if CONFIG._ip_header_enabled() {\n            req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {\n                match ip.find(',') {\n                    Some(idx) => &ip[..idx],\n                    None => ip,\n                }\n                .parse()\n                .map_err(|_| warn!(\"'{}' header is malformed: {ip}\", CONFIG.ip_header()))\n                .ok()\n            })\n        } else {\n            None\n        };\n\n        let ip = ip.or_else(|| req.remote().map(|r| r.ip())).unwrap_or_else(|| \"0.0.0.0\".parse().unwrap());\n\n        Outcome::Success(ClientIp {\n            ip,\n        })\n    }\n}\n\npub struct Secure {\n    pub https: bool,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for Secure {\n    type Error = ();\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n\n        // Try to guess from the headers\n        let protocol = match headers.get_one(\"X-Forwarded-Proto\") {\n            Some(proto) => proto,\n            None => {\n                if env::var(\"ROCKET_TLS\").is_ok() {\n                    \"https\"\n                } else {\n                    \"http\"\n                }\n            }\n        };\n\n        Outcome::Success(Secure {\n            https: protocol == \"https\",\n        })\n    }\n}\n\npub struct WsAccessTokenHeader {\n    pub access_token: Option<String>,\n}\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for WsAccessTokenHeader {\n    type Error = ();\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n\n        // Get access_token\n        let access_token = match headers.get_one(\"Authorization\") {\n            Some(a) => a.rsplit(\"Bearer \").next().map(String::from),\n            None => None,\n        };\n\n        Outcome::Success(Self {\n            access_token,\n        })\n    }\n}\n\npub struct ClientVersion(pub semver::Version);\n\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for ClientVersion {\n    type Error = &'static str;\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        let headers = request.headers();\n\n        let Some(version) = headers.get_one(\"Bitwarden-Client-Version\") else {\n            err_handler!(\"No Bitwarden-Client-Version header provided\")\n        };\n\n        let Ok(version) = semver::Version::parse(version) else {\n            err_handler!(\"Invalid Bitwarden-Client-Version header provided\")\n        };\n\n        Outcome::Success(ClientVersion(version))\n    }\n}\n\n#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum AuthMethod {\n    OrgApiKey,\n    Password,\n    Sso,\n    UserApiKey,\n}\n\nimpl AuthMethod {\n    pub fn scope(&self) -> String {\n        match self {\n            AuthMethod::OrgApiKey => \"api.organization\".to_string(),\n            AuthMethod::Password => \"api offline_access\".to_string(),\n            AuthMethod::Sso => \"api offline_access\".to_string(),\n            AuthMethod::UserApiKey => \"api\".to_string(),\n        }\n    }\n\n    pub fn scope_vec(&self) -> Vec<String> {\n        self.scope().split_whitespace().map(str::to_string).collect()\n    }\n\n    pub fn check_scope(&self, scope: Option<&String>) -> ApiResult<String> {\n        let method_scope = self.scope();\n        match scope {\n            None => err!(\"Missing scope\"),\n            Some(scope) if scope == &method_scope => Ok(method_scope),\n            Some(scope) => err!(format!(\"Scope ({scope}) not supported\")),\n        }\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub enum TokenWrapper {\n    Access(String),\n    Refresh(String),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct RefreshJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: AuthMethod,\n\n    pub device_token: String,\n\n    pub token: Option<TokenWrapper>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct AuthTokens {\n    pub refresh_claims: RefreshJwtClaims,\n    pub access_claims: LoginJwtClaims,\n}\n\nimpl AuthTokens {\n    pub fn refresh_token(&self) -> String {\n        encode_jwt(&self.refresh_claims)\n    }\n\n    pub fn access_token(&self) -> String {\n        self.access_claims.token()\n    }\n\n    pub fn expires_in(&self) -> i64 {\n        self.access_claims.expires_in()\n    }\n\n    pub fn scope(&self) -> String {\n        self.refresh_claims.sub.scope()\n    }\n\n    // Create refresh_token and access_token with default validity\n    pub fn new(device: &Device, user: &User, sub: AuthMethod, client_id: Option<String>) -> Self {\n        let time_now = Utc::now();\n\n        let access_claims = LoginJwtClaims::default(device, user, &sub, client_id);\n\n        let validity = if device.is_mobile() {\n            *MOBILE_REFRESH_VALIDITY\n        } else {\n            *DEFAULT_REFRESH_VALIDITY\n        };\n\n        let refresh_claims = RefreshJwtClaims {\n            nbf: time_now.timestamp(),\n            exp: (time_now + validity).timestamp(),\n            iss: JWT_LOGIN_ISSUER.to_string(),\n            sub,\n            device_token: device.refresh_token.clone(),\n            token: None,\n        };\n\n        Self {\n            refresh_claims,\n            access_claims,\n        }\n    }\n}\n\npub async fn refresh_tokens(\n    ip: &ClientIp,\n    refresh_token: &str,\n    client_id: Option<String>,\n    conn: &DbConn,\n) -> ApiResult<(Device, AuthTokens)> {\n    let refresh_claims = match decode_refresh(refresh_token) {\n        Err(err) => {\n            error!(\"Failed to decode {} refresh_token: {refresh_token}: {err:?}\", ip.ip);\n            //err_silent!(format!(\"Impossible to read refresh_token: {}\", err.message()))\n\n            // If the token failed to decode, it was probably one of the old style tokens that was just a Base64 string.\n            // We can generate a claim for them for backwards compatibility. Note that the password refresh claims don't\n            // check expiration or issuer, so they're not included here.\n            RefreshJwtClaims {\n                nbf: 0,\n                exp: 0,\n                iss: String::new(),\n                sub: AuthMethod::Password,\n                device_token: refresh_token.into(),\n                token: None,\n            }\n        }\n        Ok(claims) => claims,\n    };\n\n    // Get device by refresh token\n    let mut device = match Device::find_by_refresh_token(&refresh_claims.device_token, conn).await {\n        None => err!(\"Invalid refresh token\"),\n        Some(device) => device,\n    };\n\n    // Save to update `updated_at`.\n    device.save(true, conn).await?;\n\n    let user = match User::find_by_uuid(&device.user_uuid, conn).await {\n        None => err!(\"Impossible to find user\"),\n        Some(user) => user,\n    };\n\n    let auth_tokens = match refresh_claims.sub {\n        AuthMethod::Sso if CONFIG.sso_enabled() && CONFIG.sso_auth_only_not_session() => {\n            AuthTokens::new(&device, &user, refresh_claims.sub, client_id)\n        }\n        AuthMethod::Sso if CONFIG.sso_enabled() => {\n            sso::exchange_refresh_token(&device, &user, client_id, refresh_claims).await?\n        }\n        AuthMethod::Sso => err!(\"SSO is now disabled, Login again using email and master password\"),\n        AuthMethod::Password if CONFIG.sso_enabled() && CONFIG.sso_only() => err!(\"SSO is now required, Login again\"),\n        AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id),\n        _ => err!(\"Invalid auth method, cannot refresh token\"),\n    };\n\n    Ok((device, auth_tokens))\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use std::{\n    env::consts::EXE_SUFFIX,\n    fmt,\n    process::exit,\n    sync::{\n        atomic::{AtomicBool, Ordering},\n        LazyLock, RwLock,\n    },\n};\n\nuse job_scheduler_ng::Schedule;\nuse reqwest::Url;\nuse serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};\n\nuse crate::{\n    error::Error,\n    util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags},\n};\n\nstatic CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {\n    let data_folder = get_env(\"DATA_FOLDER\").unwrap_or_else(|| String::from(\"data\"));\n    get_env(\"CONFIG_FILE\").unwrap_or_else(|| format!(\"{data_folder}/config.json\"))\n});\n\nstatic CONFIG_FILE_PARENT_DIR: LazyLock<String> = LazyLock::new(|| {\n    let path = std::path::PathBuf::from(&*CONFIG_FILE);\n    path.parent().unwrap_or(std::path::Path::new(\"data\")).to_str().unwrap_or(\"data\").to_string()\n});\n\nstatic CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {\n    let path = std::path::PathBuf::from(&*CONFIG_FILE);\n    path.file_name().unwrap_or(std::ffi::OsStr::new(\"config.json\")).to_str().unwrap_or(\"config.json\").to_string()\n});\n\npub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);\n\npub static CONFIG: LazyLock<Config> = LazyLock::new(|| {\n    std::thread::spawn(|| {\n        let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap_or_else(|e| {\n            println!(\"Error loading config:\\n  {e:?}\\n\");\n            exit(12)\n        });\n\n        rt.block_on(Config::load()).unwrap_or_else(|e| {\n            println!(\"Error loading config:\\n  {e:?}\\n\");\n            exit(12)\n        })\n    })\n    .join()\n    .unwrap_or_else(|e| {\n        println!(\"Error loading config:\\n  {e:?}\\n\");\n        exit(12)\n    })\n});\n\npub type Pass = String;\n\nmacro_rules! make_config {\n    // Support string print\n    ( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value(&$value.as_ref().map(|_| String::from(\"***\"))).unwrap() }; // Optional pass, we map to an Option<String> with \"***\"\n    ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { \"***\".into() }; // Required pass, we return \"***\"\n    ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value(&$value).unwrap() }; // Optional other or string, we convert to json\n    ( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { $value.as_str().into() }; // Required string value, we convert to json\n    ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to json\n\n    // Group or empty string\n    ( @show ) => { \"\" };\n    ( @show $lit:literal ) => { $lit };\n\n    // Wrap the optionals in an Option type\n    ( @type $ty:ty, option) => { Option<$ty> };\n    ( @type $ty:ty, $id:ident) => { $ty };\n\n    // Generate the values depending on none_action\n    ( @build $value:expr, $config:expr, option, ) => { $value };\n    ( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };\n    ( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{\n        match $value {\n            Some(v) => v,\n            None => {\n                let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;\n                f($config)\n            }\n        }\n    }};\n    ( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{\n        let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;\n        f($config)\n    }};\n\n    ( @getenv $name:expr, bool ) => { get_env_bool($name) };\n    ( @getenv $name:expr, $ty:ident ) => { get_env($name) };\n\n    ($(\n        $(#[doc = $groupdoc:literal])?\n        $group:ident $(: $group_enabled:ident)? {\n        $(\n            $(#[doc = $doc:literal])+\n            $name:ident : $ty:ident, $editable:literal, $none_action:ident $(, $default:expr)?;\n        )+},\n    )+) => {\n        pub struct Config { inner: RwLock<Inner> }\n\n        struct Inner {\n            rocket_shutdown_handle: Option<rocket::Shutdown>,\n\n            templates: Handlebars<'static>,\n            config: ConfigItems,\n\n            _env: ConfigBuilder,\n            _usr: ConfigBuilder,\n\n            _overrides: Vec<&'static str>,\n        }\n\n        // Custom Deserialize for ConfigBuilder, mainly based upon https://serde.rs/deserialize-struct.html\n        // This deserialize doesn't care if there are keys missing, or if there are duplicate keys\n        // In case of duplicate keys (which should never be possible unless manually edited), the last value is used!\n        // Main reason for this is removing the `visit_seq` function, which causes a lot of code generation not needed or used for this struct.\n        impl<'de> Deserialize<'de> for ConfigBuilder {\n            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n            where\n                D: Deserializer<'de>,\n            {\n                const FIELDS: &[&str] = &[\n                $($(\n                    stringify!($name),\n                )+)+\n                ];\n\n                #[allow(non_camel_case_types)]\n                enum Field {\n                $($(\n                    $name,\n                )+)+\n                    __ignore,\n                }\n\n                impl<'de> Deserialize<'de> for Field {\n                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n                    where\n                        D: Deserializer<'de>,\n                    {\n                        struct FieldVisitor;\n\n                        impl Visitor<'_> for FieldVisitor {\n                            type Value = Field;\n\n                            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n                                formatter.write_str(\"ConfigBuilder field identifier\")\n                            }\n\n                            #[inline]\n                            fn visit_str<E>(self, value: &str) -> Result<Field, E>\n                            where\n                                E: de::Error,\n                            {\n                                match value {\n                                $($(\n                                    stringify!($name) => Ok(Field::$name),\n                                )+)+\n                                    _ => Ok(Field::__ignore),\n                                }\n                            }\n                        }\n\n                        deserializer.deserialize_identifier(FieldVisitor)\n                    }\n                }\n\n                struct ConfigBuilderVisitor;\n\n                impl<'de> Visitor<'de> for ConfigBuilderVisitor {\n                    type Value = ConfigBuilder;\n\n                    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n                        formatter.write_str(\"struct ConfigBuilder\")\n                    }\n\n                    #[inline]\n                    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>\n                    where\n                        A: MapAccess<'de>,\n                    {\n                        let mut builder = ConfigBuilder::default();\n                        while let Some(key) = map.next_key()? {\n                            match key {\n                            $($(\n                                Field::$name => {\n                                    if builder.$name.is_some() {\n                                        return Err(de::Error::duplicate_field(stringify!($name)));\n                                    }\n                                    builder.$name = map.next_value()?;\n                                }\n                            )+)+\n                                Field::__ignore => {\n                                    let _ = map.next_value::<de::IgnoredAny>()?;\n                                }\n                            }\n                        }\n                        Ok(builder)\n                    }\n                }\n\n                deserializer.deserialize_struct(\"ConfigBuilder\", FIELDS, ConfigBuilderVisitor)\n            }\n        }\n\n        #[derive(Clone, Default, Serialize)]\n        pub struct ConfigBuilder {\n            $($(\n                #[serde(skip_serializing_if = \"Option::is_none\")]\n                $name: Option<$ty>,\n            )+)+\n        }\n\n        impl ConfigBuilder {\n            fn from_env() -> Self {\n                let env_file = get_env(\"ENV_FILE\").unwrap_or_else(|| String::from(\".env\"));\n                match dotenvy::from_path(&env_file) {\n                    Ok(_) => {\n                        println!(\"[INFO] Using environment file `{env_file}` for configuration.\\n\");\n                    },\n                    Err(e) => match e {\n                        dotenvy::Error::LineParse(msg, pos) => {\n                            println!(\"[ERROR] Failed parsing environment file: `{env_file}`\\nNear {msg:?} on position {pos}\\nPlease fix and restart!\\n\");\n                            exit(255);\n                        },\n                        dotenvy::Error::Io(ioerr) => match ioerr.kind() {\n                            std::io::ErrorKind::NotFound => {\n                                // Only exit if this environment variable is set, but the file was not found.\n                                // This prevents incorrectly configured environments.\n                                if let Some(env_file) = get_env::<String>(\"ENV_FILE\") {\n                                    println!(\"[ERROR] The configured ENV_FILE `{env_file}` was not found!\\n\");\n                                    exit(255);\n                                }\n                            },\n                            std::io::ErrorKind::PermissionDenied => {\n                                println!(\"[ERROR] Permission denied while trying to read environment file `{env_file}`!\\n\");\n                                exit(255);\n                            },\n                            _ => {\n                                println!(\"[ERROR] Reading environment file `{env_file}` failed:\\n{ioerr:?}\\n\");\n                                exit(255);\n                            }\n                        },\n                        _ => {\n                            println!(\"[ERROR] Reading environment file `{env_file}` failed:\\n{e:?}\\n\");\n                            exit(255);\n                        }\n                    }\n                };\n\n                let mut builder = ConfigBuilder::default();\n                $($(\n                    builder.$name = make_config! { @getenv pastey::paste!(stringify!([<$name:upper>])), $ty };\n                )+)+\n\n                builder\n            }\n\n            async fn from_file() -> Result<Self, Error> {\n                let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;\n                let config_bytes = operator.read(&CONFIG_FILENAME).await?;\n                println!(\"[INFO] Using saved config from `{}` for configuration.\\n\", *CONFIG_FILE);\n                serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)\n            }\n\n            fn clear_non_editable(&mut self) {\n                $($(\n                    if !$editable {\n                        self.$name = None;\n                    }\n                )+)+\n            }\n\n            /// Merges the values of both builders into a new builder.\n            /// If both have the same element, `other` wins.\n            fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<&str>) -> Self {\n                let mut builder = self.clone();\n                $($(\n                    if let v @Some(_) = &other.$name {\n                        builder.$name = v.clone();\n\n                        if self.$name.is_some() {\n                            overrides.push(pastey::paste!(stringify!([<$name:upper>])));\n                        }\n                    }\n                )+)+\n\n                if show_overrides && !overrides.is_empty() {\n                    // We can't use warn! here because logging isn't setup yet.\n                    println!(\"[WARNING] The following environment variables are being overridden by the config.json file.\");\n                    println!(\"[WARNING] Please use the admin panel to make changes to them:\");\n                    println!(\"[WARNING] {}\\n\", overrides.join(\", \"));\n                }\n\n                builder\n            }\n\n            fn build(&self) -> ConfigItems {\n                let mut config = ConfigItems::default();\n                let _domain_set = self.domain.is_some();\n                $($(\n                    config.$name = make_config! { @build self.$name.clone(), &config, $none_action, $($default)? };\n                )+)+\n                config.domain_set = _domain_set;\n\n                config.domain = config.domain.trim_end_matches('/').to_string();\n\n                config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();\n                config.org_creation_users = config.org_creation_users.trim().to_lowercase();\n\n\n                // Copy the values from the deprecated flags to the new ones\n                if config.http_request_block_regex.is_none() {\n                    config.http_request_block_regex = config.icon_blacklist_regex.clone();\n                }\n\n                config\n            }\n        }\n\n        #[derive(Clone, Default)]\n        struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ }\n\n        #[derive(Serialize)]\n        struct ElementDoc {\n            name: &'static str,\n            description: &'static str,\n        }\n\n        #[derive(Serialize)]\n        struct ElementData {\n            editable: bool,\n            name: &'static str,\n            value: serde_json::Value,\n            default: serde_json::Value,\n            #[serde(rename = \"type\")]\n            r#type: &'static str,\n            doc: ElementDoc,\n            overridden: bool,\n        }\n\n        #[derive(Serialize)]\n        pub struct GroupData {\n            group: &'static str,\n            grouptoggle: &'static str,\n            groupdoc: &'static str,\n            elements: Vec<ElementData>,\n        }\n\n        #[allow(unused)]\n        impl Config {\n            $($(\n                $(#[doc = $doc])+\n                pub fn $name(&self) -> make_config! {@type $ty, $none_action} {\n                    self.inner.read().unwrap().config.$name.clone()\n                }\n            )+)+\n\n            pub fn prepare_json(&self) -> serde_json::Value {\n                let (def, cfg, overridden) = {\n                    // Lock the inner as short as possible and clone what is needed to prevent deadlocks\n                    let inner = &self.inner.read().unwrap();\n                    (inner._env.build(), inner.config.clone(), inner._overrides.clone())\n                };\n\n                fn _get_form_type(rust_type: &'static str) -> &'static str {\n                    match rust_type {\n                        \"Pass\" => \"password\",\n                        \"String\" => \"text\",\n                        \"bool\" => \"checkbox\",\n                        _ => \"number\"\n                    }\n                }\n\n                fn _get_doc(doc_str: &'static str) -> ElementDoc {\n                    let mut split = doc_str.split(\"|>\").map(str::trim);\n                    ElementDoc {\n                        name: split.next().unwrap_or_default(),\n                        description: split.next().unwrap_or_default(),\n                    }\n                }\n\n                let data: Vec<GroupData> = vec![\n                $( // This repetition is for each group\n                    GroupData {\n                        group: stringify!($group),\n                        grouptoggle: stringify!($($group_enabled)?),\n                        groupdoc: (make_config! { @show $($groupdoc)? }),\n\n                        elements: vec![\n                        $( // This repetition is for each element within a group\n                            ElementData {\n                                editable: $editable,\n                                name: stringify!($name),\n                                value: serde_json::to_value(&cfg.$name).unwrap_or_default(),\n                                default: serde_json::to_value(&def.$name).unwrap_or_default(),\n                                r#type: _get_form_type(stringify!($ty)),\n                                doc: _get_doc(concat!($($doc),+)),\n                                overridden: overridden.contains(&pastey::paste!(stringify!([<$name:upper>]))),\n                            },\n                        )+], // End of elements repetition\n                    },\n                )+]; // End of groups repetition\n                serde_json::to_value(data).unwrap()\n            }\n\n            pub fn get_support_json(&self) -> serde_json::Value {\n                // Define which config keys need to be masked.\n                // Pass types will always be masked and no need to put them in the list.\n                // Besides Pass, only String types will be masked via _privacy_mask.\n                const PRIVACY_CONFIG: &[&str] = &[\n                    \"allowed_connect_src\",\n                    \"allowed_iframe_ancestors\",\n                    \"database_url\",\n                    \"domain_origin\",\n                    \"domain_path\",\n                    \"domain\",\n                    \"helo_name\",\n                    \"org_creation_users\",\n                    \"signups_domains_whitelist\",\n                    \"_smtp_img_src\",\n                    \"smtp_from_name\",\n                    \"smtp_from\",\n                    \"smtp_host\",\n                    \"smtp_username\",\n                    \"sso_authority\",\n                    \"sso_callback_path\",\n                    \"sso_client_id\",\n                ];\n\n                let cfg = {\n                    // Lock the inner as short as possible and clone what is needed to prevent deadlocks\n                    let inner = &self.inner.read().unwrap();\n                    inner.config.clone()\n                };\n\n                /// We map over the string and remove all alphanumeric, _ and - characters.\n                /// This is the fastest way (within micro-seconds) instead of using a regex (which takes mili-seconds)\n                fn _privacy_mask(value: &str) -> String {\n                    let mut n: u16 = 0;\n                    let mut colon_match = false;\n                    value\n                        .chars()\n                        .map(|c| {\n                            n += 1;\n                            match c {\n                                ':' if n <= 11 => {\n                                    colon_match = true;\n                                    c\n                                }\n                                '/' if n <= 13 && colon_match => c,\n                                ',' => c,\n                                _ => '*',\n                            }\n                        })\n                        .collect::<String>()\n                }\n\n                serde_json::Value::Object({\n                    let mut json = serde_json::Map::new();\n                    $($(\n                        json.insert(String::from(stringify!($name)), make_config! { @supportstr $name, cfg.$name, $ty, $none_action });\n                    )+)+;\n                    // Loop through all privacy sensitive keys and mask them\n                    for mask_key in PRIVACY_CONFIG {\n                        if let Some(value) = json.get_mut(*mask_key) {\n                            if let Some(s) = value.as_str() {\n                                *value = _privacy_mask(s).into();\n                            }\n                        }\n                    }\n                    json\n                })\n            }\n\n            pub fn get_overrides(&self) -> Vec<&'static str> {\n                let overrides = {\n                    let inner = &self.inner.read().unwrap();\n                    inner._overrides.clone()\n                };\n                overrides\n            }\n        }\n    };\n}\n\n//STRUCTURE:\n// /// Short description (without this they won't appear on the list)\n// group {\n//   /// Friendly Name |> Description (Optional)\n//   name: type, is_editable, action, <default_value (Optional)>\n// }\n//\n// Where action applied when the value wasn't provided and can be:\n//  def:       Use a default value\n//  auto:      Value is auto generated based on other values\n//  option:    Value is optional\n//  generated: Value is always autogenerated and it's original value ignored\nmake_config! {\n    folders {\n        ///  Data folder |> Main data folder\n        data_folder:            String, false,  def,    \"data\".to_string();\n        /// Database URL\n        database_url:           String, false,  auto,   |c| format!(\"{}/db.sqlite3\", c.data_folder);\n        /// Icon cache folder\n        icon_cache_folder:      String, false,  auto,   |c| format!(\"{}/icon_cache\", c.data_folder);\n        /// Attachments folder\n        attachments_folder:     String, false,  auto,   |c| format!(\"{}/attachments\", c.data_folder);\n        /// Sends folder\n        sends_folder:           String, false,  auto,   |c| format!(\"{}/sends\", c.data_folder);\n        /// Temp folder |> Used for storing temporary file uploads\n        tmp_folder:             String, false,  auto,   |c| format!(\"{}/tmp\", c.data_folder);\n        /// Templates folder\n        templates_folder:       String, false,  auto,   |c| format!(\"{}/templates\", c.data_folder);\n        /// Session JWT key\n        rsa_key_filename:       String, false,  auto,   |c| format!(\"{}/rsa_key\", c.data_folder);\n        /// Web vault folder\n        web_vault_folder:       String, false,  def,    \"web-vault/\".to_string();\n    },\n    ws {\n        /// Enable websocket notifications\n        enable_websocket:       bool,   false,  def,    true;\n    },\n    push {\n        /// Enable push notifications\n        push_enabled:           bool,   false,  def,    false;\n        /// Push relay uri\n        push_relay_uri:         String, false,  def,    \"https://push.bitwarden.com\".to_string();\n        /// Push identity uri\n        push_identity_uri:      String, false,  def,    \"https://identity.bitwarden.com\".to_string();\n        /// Installation id |> The installation id from https://bitwarden.com/host\n        push_installation_id:   Pass,   false,  def,    String::new();\n        /// Installation key |> The installation key from https://bitwarden.com/host\n        push_installation_key:  Pass,   false,  def,    String::new();\n    },\n    jobs {\n        /// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.\n        /// Set to 0 to globally disable scheduled jobs.\n        job_poll_interval_ms:   u64,    false,  def,    30_000;\n        /// Send purge schedule |> Cron schedule of the job that checks for Sends past their deletion date.\n        /// Defaults to hourly. Set blank to disable this job.\n        send_purge_schedule:    String, false,  def,    \"0 5 * * * *\".to_string();\n        /// Trash purge schedule |> Cron schedule of the job that checks for trashed items to delete permanently.\n        /// Defaults to daily. Set blank to disable this job.\n        trash_purge_schedule:   String, false,  def,    \"0 5 0 * * *\".to_string();\n        /// Incomplete 2FA login schedule |> Cron schedule of the job that checks for incomplete 2FA logins.\n        /// Defaults to once every minute. Set blank to disable this job.\n        incomplete_2fa_schedule: String, false,  def,   \"30 * * * * *\".to_string();\n        /// Emergency notification reminder schedule |> Cron schedule of the job that sends expiration reminders to emergency access grantors.\n        /// Defaults to hourly. (3 minutes after the hour) Set blank to disable this job.\n        emergency_notification_reminder_schedule:   String, false,  def,    \"0 3 * * * *\".to_string();\n        /// Emergency request timeout schedule |> Cron schedule of the job that grants emergency access requests that have met the required wait time.\n        /// Defaults to hourly. (7 minutes after the hour) Set blank to disable this job.\n        emergency_request_timeout_schedule:   String, false,  def,    \"0 7 * * * *\".to_string();\n        /// Event cleanup schedule |> Cron schedule of the job that cleans old events from the event table.\n        /// Defaults to daily. Set blank to disable this job.\n        event_cleanup_schedule:   String, false,  def,    \"0 10 0 * * *\".to_string();\n        /// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request.\n        /// Defaults to every minute. Set blank to disable this job.\n        auth_request_purge_schedule:   String, false,  def,    \"30 * * * * *\".to_string();\n        /// Duo Auth context cleanup schedule |> Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt.\n        /// Defaults to once every minute. Set blank to disable this job.\n        duo_context_purge_schedule:   String, false,  def,    \"30 * * * * *\".to_string();\n        /// Purge incomplete SSO auth. |> Cron schedule of the job that cleans leftover auth in db due to incomplete SSO login.\n        /// Defaults to daily. Set blank to disable this job.\n        purge_incomplete_sso_auth: String, false,  def,   \"0 20 0 * * *\".to_string();\n    },\n\n    /// General settings\n    settings {\n        /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'\n        /// and port, if it's different than the default. Some server functions don't work correctly without this value\n        domain:                 String, true,   def,    \"http://localhost\".to_string();\n        /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.\n        domain_set:             bool,   false,  def,    false;\n        /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin)\n        domain_origin:          String, false,  auto,   |c| extract_url_origin(&c.domain);\n        /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path)\n        domain_path:            String, false,  auto,   |c| extract_url_path(&c.domain);\n        /// Enable web vault\n        web_vault_enabled:      bool,   false,  def,    true;\n\n        /// Allow Sends |> Controls whether users are allowed to create Bitwarden Sends.\n        /// This setting applies globally to all users. To control this on a per-org basis instead, use the \"Disable Send\" org policy.\n        sends_allowed:          bool,   true,   def,    true;\n\n        /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key\n        hibp_api_key:           Pass,   true,   option;\n\n        /// Per-user attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per user. When this limit is reached, the user will not be allowed to upload further attachments.\n        user_attachment_limit:  i64,    true,   option;\n        /// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org.\n        org_attachment_limit:   i64,    true,   option;\n        /// Per-user send storage limit (KB) |> Max kilobytes of sends storage allowed per user. When this limit is reached, the user will not be allowed to upload further sends.\n        user_send_limit:   i64,    true,   option;\n\n        /// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item.\n        /// If unset, trashed items are not auto-deleted. This setting applies globally, so make\n        /// sure to inform all users of any changes to this setting.\n        trash_auto_delete_days: i64,    true,   option;\n\n        /// Incomplete 2FA time limit |> Number of minutes to wait before a 2FA-enabled login is\n        /// considered incomplete, resulting in an email notification. An incomplete 2FA login is one\n        /// where the correct master password was provided but the required 2FA step was not completed,\n        /// which potentially indicates a master password compromise. Set to 0 to disable this check.\n        /// This setting applies globally to all users.\n        incomplete_2fa_time_limit: i64, true,   def,    3;\n\n        /// Disable icon downloads |> Set to true to disable icon downloading in the internal icon service.\n        /// This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external\n        /// network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons\n        /// will be deleted eventually, but won't be downloaded again.\n        disable_icon_download:  bool,   true,   def,    false;\n        /// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled\n        signups_allowed:        bool,   true,   def,    true;\n        /// Require email verification on signups. On new client versions, this will require verification at signup time. On older clients,\n        /// this will prevent logins from succeeding until the address has been verified\n        signups_verify:         bool,   true,   def,    false;\n        /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)\n        signups_verify_resend_time: u64, true,  def,    3_600;\n        /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit)\n        signups_verify_resend_limit: u32, true, def,    6;\n        /// Email domain whitelist |> Allow signups only from this list of comma-separated domains, even when signups are otherwise disabled\n        signups_domains_whitelist: String, true, def,   String::new();\n        /// Enable event logging |> Enables event logging for organizations.\n        org_events_enabled:     bool,   false,  def,    false;\n        /// Org creation users |> Allow org creation only by this list of comma-separated user emails.\n        /// Blank or 'all' means all users can create orgs; 'none' means no users can create orgs.\n        org_creation_users:     String, true,   def,    String::new();\n        /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled\n        invitations_allowed:    bool,   true,   def,    true;\n        /// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token,\n        /// email verification token and deletion request token will expire (must be at least 1)\n        invitation_expiration_hours: u32, false, def, 120;\n        /// Enable emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users.\n        emergency_access_allowed:    bool,   true,   def,    true;\n        /// Allow email change |> Controls whether users can change their email. This setting applies globally to all users.\n        email_change_allowed:    bool,   true,   def,    true;\n        /// Password iterations |> Number of server-side passwords hashing iterations for the password hash.\n        /// The default for new users. If changed, it will be updated during login for existing users.\n        password_iterations:    i32,    true,   def,    600_000;\n        /// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users.\n        password_hints_allowed: bool,   true,   def,    true;\n        /// Show password hint (Know the risks!) |> Controls whether a password hint should be shown directly in the web page\n        /// if SMTP service is not configured and password hints are allowed. Not recommended for publicly-accessible instances\n        /// because this provides unauthenticated access to potentially sensitive data.\n        show_password_hint:     bool,   true,   def,    false;\n\n        /// Admin token/Argon2 PHC |> The plain text token or Argon2 PHC string used to authenticate in this very same page. Changing it here will not deauthorize the current session!\n        admin_token:            Pass,   true,   option;\n\n        /// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization\n        invitation_org_name:    String, true,   def,    \"Vaultwarden\".to_string();\n\n        /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely.\n        events_days_retain:     i64,    false,   option;\n    },\n\n    /// Advanced settings\n    advanced {\n        /// Client IP header |> If not present, the remote IP is used.\n        /// Set to the string \"none\" (without quotes), to disable any headers and just use the remote IP\n        ip_header:              String, true,   def,    \"X-Real-IP\".to_string();\n        /// Internal IP header property, used to avoid recomputing each time\n        _ip_header_enabled:     bool,   false,  generated,    |c| &c.ip_header.trim().to_lowercase() != \"none\";\n        /// Icon service |> The predefined icon services are: internal, bitwarden, duckduckgo, google.\n        /// To specify a custom icon service, set a URL template with exactly one instance of `{}`,\n        /// which is replaced with the domain. For example: `https://icon.example.com/domain/{}`.\n        /// `internal` refers to Vaultwarden's built-in icon fetching implementation. If an external\n        /// service is set, an icon request to Vaultwarden will return an HTTP redirect to the\n        /// corresponding icon at the external service.\n        icon_service:           String, false,  def,    \"internal\".to_string();\n        /// _icon_service_url\n        _icon_service_url:      String, false,  generated,    |c| generate_icon_service_url(&c.icon_service);\n        /// _icon_service_csp\n        _icon_service_csp:      String, false,  generated,    |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url);\n        /// Icon redirect code |> The HTTP status code to use for redirects to an external icon service.\n        /// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent).\n        /// Temporary redirects are useful while testing different icon services, but once a service\n        /// has been decided on, consider using permanent redirects for cacheability. The legacy codes\n        /// are currently better supported by the Bitwarden clients.\n        icon_redirect_code:     u32,    true,   def,    302;\n        /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be refreshed\n        icon_cache_ttl:         u64,    true,   def,    2_592_000;\n        /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.\n        icon_cache_negttl:      u64,    true,   def,    259_200;\n        /// Icon download timeout |> Number of seconds when to stop attempting to download an icon.\n        icon_download_timeout:  u64,    true,   def,    10;\n\n        /// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead\n        icon_blacklist_regex:   String, false,   option;\n        /// [Deprecated] Icon blacklist non global IPs |> Use `http_request_block_non_global_ips` instead\n        icon_blacklist_non_global_ips:  bool,   false,   def, true;\n\n        /// Block HTTP domains/IPs by Regex |> Any domains or IPs that match this regex won't be fetched by the internal HTTP client.\n        /// Useful to hide other servers in the local network. Check the WIKI for more details\n        http_request_block_regex:   String, true,   option;\n        /// Block non global IPs |> Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address.\n        /// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block\n        http_request_block_non_global_ips:  bool,   true,   auto, |c| c.icon_blacklist_non_global_ips;\n\n        /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.\n        /// Note that the checkbox would still be present, but ignored.\n        disable_2fa_remember:   bool,   true,   def,    false;\n\n        /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid\n        /// TOTP codes of the previous and next 30 seconds will be invalid.\n        authenticator_disable_time_drift: bool, true, def, false;\n\n        /// Customize the enabled feature flags on the clients |> This is a comma separated list of feature flags to enable.\n        experimental_client_feature_flags: String, false, def, String::new();\n\n        /// Require new device emails |> When a user logs in an email is required to be sent.\n        /// If sending the email fails the login attempt will fail.\n        require_device_email:   bool,   true,   def,     false;\n\n        /// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.\n        /// ONLY use this during development, as it can slow down the server\n        reload_templates:       bool,   true,   def,    false;\n        /// Enable extended logging\n        extended_logging:       bool,   false,  def,    true;\n        /// Log timestamp format\n        log_timestamp_format:   String, true,   def,    \"%Y-%m-%d %H:%M:%S.%3f\".to_string();\n        /// Enable the log to output to Syslog\n        use_syslog:             bool,   false,  def,    false;\n        /// Log file path\n        log_file:               String, false,  option;\n        /// Log level |> Valid values are \"trace\", \"debug\", \"info\", \"warn\", \"error\" and \"off\"\n        /// For a specific module append it as a comma separated value \"info,path::to::module=debug\"\n        log_level:              String, false,  def,    \"info\".to_string();\n\n        /// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems,\n        /// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.\n        enable_db_wal:          bool,   false,  def,    true;\n\n        /// Max database connection retries |> Number of times to retry the database connection during startup, with 1 second between each retry, set to 0 to retry indefinitely\n        db_connection_retries:  u32,    false,  def,    15;\n\n        /// Timeout when acquiring database connection\n        database_timeout:       u64,    false,  def,    30;\n\n        /// Timeout in seconds before idle connections to the database are closed\n        database_idle_timeout:  u64,    false, def,     600;\n\n        /// Database connection max pool size\n        database_max_conns:     u32,    false,  def,    10;\n\n        /// Database connection min pool size\n        database_min_conns:     u32,    false,  def,    2;\n\n        /// Database connection init |> SQL statements to run when creating a new database connection, mainly useful for connection-scoped pragmas. If empty, a database-specific default is used.\n        database_conn_init:     String, false,  def,    String::new();\n\n        /// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front\n        disable_admin_token:    bool,   false,  def,    false;\n\n        /// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets\n        allowed_iframe_ancestors: String, true, def,    String::new();\n\n        /// Allowed connect-src (Know the risks!) |> Allows other domains to URLs which can be loaded using script interfaces like the Forwarded email alias feature\n        allowed_connect_src:      String, true, def,    String::new();\n\n        /// Seconds between login requests |> Number of seconds, on average, between login and 2FA requests from the same IP address before rate limiting kicks in\n        login_ratelimit_seconds:       u64, false, def, 60;\n        /// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `login_ratelimit_seconds`. Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2\n        login_ratelimit_max_burst:     u32, false, def, 10;\n\n        /// Seconds between admin login requests |> Number of seconds, on average, between admin requests from the same IP address before rate limiting kicks in\n        admin_ratelimit_seconds:       u64, false, def, 300;\n        /// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds`\n        admin_ratelimit_max_burst:     u32, false, def, 3;\n\n        /// Admin session lifetime |> Set the lifetime of admin sessions to this value (in minutes).\n        admin_session_lifetime:        i64, true,  def, 20;\n\n        /// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).\n        org_groups_enabled:            bool, false, def, false;\n\n        /// Increase note size limit (Know the risks!) |> Sets the secure note size limit to 100_000 instead of the default 10_000.\n        /// WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers!\n        increase_note_size_limit:      bool,  true,  def, false;\n        /// Generated max_note_size value to prevent if..else matching during every check\n        _max_note_size:                usize, false, generated, |c| if c.increase_note_size_limit {100_000} else {10_000};\n\n        /// Enforce Single Org with Reset Password Policy |> Enforce that the Single Org policy is enabled before setting the Reset Password policy\n        /// Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available.\n        /// Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy.\n        enforce_single_org_with_reset_pw_policy: bool, false, def, false;\n\n        /// Prefer IPv6 (AAAA) resolving |> This settings configures the DNS resolver to resolve IPv6 first, and if not available try IPv4\n        /// This could be useful in IPv6 only environments.\n        dns_prefer_ipv6: bool, true, def, false;\n    },\n\n    /// OpenID Connect SSO settings\n    sso {\n        /// Enabled\n        sso_enabled:                    bool,   true,   def,    false;\n        /// Only SSO login |> Disable Email+Master Password login\n        sso_only:                       bool,   true,   def,    false;\n        /// Allow email association |> Associate existing non-SSO user based on email\n        sso_signups_match_email:        bool,   true,   def,    true;\n        /// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.\n        sso_allow_unknown_email_verification: bool, true, def, false;\n        /// Client ID\n        sso_client_id:                  String, true,   def,    String::new();\n        /// Client Key\n        sso_client_secret:              Pass,   true,   def,    String::new();\n        /// Authority Server |> Base url of the OIDC provider discovery endpoint (without `/.well-known/openid-configuration`)\n        sso_authority:                  String, true,   def,    String::new();\n        /// Authorization request scopes |> List the of the needed scope (`openid` is implicit)\n        sso_scopes:                     String, true,  def,   \"email profile\".to_string();\n        /// Authorization request extra parameters\n        sso_authorize_extra_params:     String, true,  def,    String::new();\n        /// Use PKCE during Authorization flow\n        sso_pkce:                       bool,   true,   def,    true;\n        /// Regex for additional trusted Id token audience |> By default only the client_id is trusted.\n        sso_audience_trusted:           String, true,  option;\n        /// CallBack Path |> Generated from Domain.\n        sso_callback_path:              String, true,  generated, |c| generate_sso_callback_path(&c.domain);\n        /// Optional SSO master password policy |> Ex format: '{\"enforceOnLogin\":false,\"minComplexity\":3,\"minLength\":12,\"requireLower\":false,\"requireNumbers\":false,\"requireSpecial\":false,\"requireUpper\":false}'\n        sso_master_password_policy:     String, true,  option;\n        /// Use SSO only for auth not the session lifecycle |> Use default Vaultwarden session lifecycle (Idle refresh token valid for 30days)\n        sso_auth_only_not_session:      bool,   true,   def,    false;\n        /// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache\n        sso_client_cache_expiration:    u64,    true,   def,    0;\n        /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required\n        sso_debug_tokens:               bool,   true,   def,    false;\n    },\n\n    /// Yubikey settings\n    yubico: _enable_yubico {\n        /// Enabled\n        _enable_yubico:         bool,   true,   def,     true;\n        /// Client ID\n        yubico_client_id:       String, true,   option;\n        /// Secret Key\n        yubico_secret_key:      Pass,   true,   option;\n        /// Server\n        yubico_server:          String, true,   option;\n    },\n\n    /// Global Duo settings (Note that users can override them)\n    duo: _enable_duo {\n        /// Enabled\n        _enable_duo:            bool,   true,   def,     true;\n        /// Attempt to use deprecated iframe-based Traditional Prompt (Duo WebSDK 2)\n        duo_use_iframe:         bool,   false,  def,     false;\n        /// Client Id\n        duo_ikey:               String, true,   option;\n        /// Client Secret\n        duo_skey:               Pass,   true,   option;\n        /// Host\n        duo_host:               String, true,   option;\n        /// Application Key (generated automatically)\n        _duo_akey:              Pass,   false,  option;\n    },\n\n    /// SMTP Email Settings\n    smtp: _enable_smtp {\n        /// Enabled\n        _enable_smtp:                  bool,   true,   def,     true;\n        /// Use Sendmail |> Whether to send mail via the `sendmail` command\n        use_sendmail:                  bool,   true,   def,     false;\n        /// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified.\n        sendmail_command:              String, false,  option;\n        /// Host\n        smtp_host:                     String, true,   option;\n        /// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY\n        smtp_ssl:                      bool,   false,  option;\n        /// DEPRECATED smtp_explicit_tls |> DEPRECATED - Please use SMTP_SECURITY\n        smtp_explicit_tls:             bool,   false,  option;\n        /// Secure SMTP |> (\"starttls\", \"force_tls\", \"off\") Enable a secure connection. Default is \"starttls\" (Explicit - ports 587 or 25), \"force_tls\" (Implicit - port 465) or \"off\", no encryption\n        smtp_security:                 String, true,   auto,    |c| smtp_convert_deprecated_ssl_options(c.smtp_ssl, c.smtp_explicit_tls); // TODO: After deprecation make it `def, \"starttls\".to_string()`\n        /// Port\n        smtp_port:                     u16,    true,   auto,    |c| if c.smtp_security == *\"force_tls\" {465} else if c.smtp_security == *\"starttls\" {587} else {25};\n        /// From Address\n        smtp_from:                     String, true,   def,     String::new();\n        /// From Name\n        smtp_from_name:                String, true,   def,     \"Vaultwarden\".to_string();\n        /// Username\n        smtp_username:                 String, true,   option;\n        /// Password\n        smtp_password:                 Pass,   true,   option;\n        /// SMTP Auth mechanism |> Defaults for SSL is \"Plain\" and \"Login\" and nothing for Non-SSL connections. Possible values: [\"Plain\", \"Login\", \"Xoauth2\"]. Multiple options need to be separated by a comma ','.\n        smtp_auth_mechanism:           String, true,   option;\n        /// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server\n        smtp_timeout:                  u64,    true,   def,     15;\n        /// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters\n        helo_name:                     String, true,   option;\n        /// Embed images as email attachments.\n        smtp_embed_images:             bool, true, def, true;\n        /// _smtp_img_src\n        _smtp_img_src:                 String, false, generated, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain);\n        /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!\n        smtp_debug:                    bool,   false,  def,     false;\n        /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!\n        smtp_accept_invalid_certs:     bool,   true,   def,     false;\n        /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!\n        smtp_accept_invalid_hostnames: bool,   true,   def,     false;\n    },\n\n    /// Email 2FA Settings\n    email_2fa: _enable_email_2fa {\n        /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured\n        _enable_email_2fa:      bool,   true,   auto,    |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail);\n        /// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.\n        email_token_size:       u8,     true,   def,      6;\n        /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.\n        email_expiration_time:  u64,    true,   def,      600;\n        /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent\n        email_attempts_limit:   u64,    true,   def,      3;\n        /// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy\n        email_2fa_enforce_on_verified_invite: bool,   true,   def,      false;\n        /// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed\n        email_2fa_auto_fallback: bool,  true,   def,      false;\n    },\n}\n\nfn validate_config(cfg: &ConfigItems) -> Result<(), Error> {\n    // Validate connection URL is valid and DB feature is enabled\n    #[cfg(sqlite)]\n    {\n        use crate::db::DbConnType;\n        let url = &cfg.database_url;\n        if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') {\n            let path = std::path::Path::new(&url);\n            if let Some(parent) = path.parent() {\n                if !parent.is_dir() {\n                    err!(format!(\n                        \"SQLite database directory `{}` does not exist or is not a directory\",\n                        parent.display()\n                    ));\n                }\n            }\n        }\n    }\n\n    if cfg.password_iterations < 100_000 {\n        err!(\"PASSWORD_ITERATIONS should be at least 100000 or higher. The default is 600000!\");\n    }\n\n    let limit = 256;\n    if cfg.database_max_conns < 1 || cfg.database_max_conns > limit {\n        err!(format!(\"`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.\",));\n    }\n\n    if cfg.database_min_conns < 1 || cfg.database_min_conns > limit {\n        err!(format!(\"`DATABASE_MIN_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.\",));\n    }\n\n    if cfg.database_min_conns > cfg.database_max_conns {\n        err!(format!(\"`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.\",));\n    }\n\n    if let Some(log_file) = &cfg.log_file {\n        if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {\n            err!(\"Unable to write to log file\", log_file);\n        }\n    }\n\n    let dom = cfg.domain.to_lowercase();\n    if !dom.starts_with(\"http://\") && !dom.starts_with(\"https://\") {\n        err!(\n            \"DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'\"\n        );\n    }\n\n    let connect_src = cfg.allowed_connect_src.to_lowercase();\n    for url in connect_src.split_whitespace() {\n        if !url.starts_with(\"https://\") || Url::parse(url).is_err() {\n            err!(\"ALLOWED_CONNECT_SRC variable contains one or more invalid URLs. Only FQDN's starting with https are allowed\");\n        }\n    }\n\n    let whitelist = &cfg.signups_domains_whitelist;\n    if !whitelist.is_empty() && whitelist.split(',').any(|d| d.trim().is_empty()) {\n        err!(\"`SIGNUPS_DOMAINS_WHITELIST` contains empty tokens\");\n    }\n\n    let org_creation_users = cfg.org_creation_users.trim().to_lowercase();\n    if !(org_creation_users.is_empty() || org_creation_users == \"all\" || org_creation_users == \"none\")\n        && org_creation_users.split(',').any(|u| !u.contains('@'))\n    {\n        err!(\"`ORG_CREATION_USERS` contains invalid email addresses\");\n    }\n\n    if let Some(ref token) = cfg.admin_token {\n        if token.trim().is_empty() && !cfg.disable_admin_token {\n            println!(\"[WARNING] `ADMIN_TOKEN` is enabled but has an empty value, so the admin page will be disabled.\");\n            println!(\"[WARNING] To enable the admin page without a token, use `DISABLE_ADMIN_TOKEN`.\");\n        }\n    }\n\n    if cfg.push_enabled && (cfg.push_installation_id == String::new() || cfg.push_installation_key == String::new()) {\n        err!(\n            \"Misconfigured Push Notification service\\n\\\n            ########################################################################################\\n\\\n            # It looks like you enabled Push Notification feature, but didn't configure it         #\\n\\\n            # properly. Make sure the installation id and key from https://bitwarden.com/host are  #\\n\\\n            # added to your configuration.                                                         #\\n\\\n            ########################################################################################\\n\"\n        )\n    }\n\n    if cfg.push_enabled {\n        let push_relay_uri = cfg.push_relay_uri.to_lowercase();\n        if !push_relay_uri.starts_with(\"https://\") {\n            err!(\"`PUSH_RELAY_URI` must start with 'https://'.\")\n        }\n\n        if Url::parse(&push_relay_uri).is_err() {\n            err!(\"Invalid URL format for `PUSH_RELAY_URI`.\");\n        }\n\n        let push_identity_uri = cfg.push_identity_uri.to_lowercase();\n        if !push_identity_uri.starts_with(\"https://\") {\n            err!(\"`PUSH_IDENTITY_URI` must start with 'https://'.\")\n        }\n\n        if Url::parse(&push_identity_uri).is_err() {\n            err!(\"Invalid URL format for `PUSH_IDENTITY_URI`.\");\n        }\n    }\n\n    // Server (v2025.6.2): https://github.com/bitwarden/server/blob/d094be3267f2030bd0dc62106bc6871cf82682f5/src/Core/Constants.cs#L103\n    // Client (web-v2026.2.0): https://github.com/bitwarden/clients/blob/a2fefe804d8c9b4a56c42f9904512c5c5821e2f6/libs/common/src/enums/feature-flag.enum.ts#L12\n    // Android (v2025.6.0): https://github.com/bitwarden/android/blob/b5b022caaad33390c31b3021b2c1205925b0e1a2/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L22\n    // iOS (v2025.6.0): https://github.com/bitwarden/ios/blob/ff06d9c6cc8da89f78f37f376495800201d7261a/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7\n    //\n    // NOTE: Move deprecated flags to the utils::parse_experimental_client_feature_flags() DEPRECATED_FLAGS const!\n    const KNOWN_FLAGS: &[&str] = &[\n        // Auth Team\n        \"pm-5594-safari-account-switching\",\n        // Autofill Team\n        \"inline-menu-positioning-improvements\",\n        \"inline-menu-totp\",\n        \"ssh-agent\",\n        // Key Management Team\n        \"ssh-key-vault-item\",\n        \"pm-25373-windows-biometrics-v2\",\n        // Tools\n        \"export-attachments\",\n        // Mobile Team\n        \"anon-addy-self-host-alias\",\n        \"simple-login-self-host-alias\",\n        \"mutual-tls\",\n        \"cxp-import-mobile\",\n        \"cxp-export-mobile\",\n        // Webauthn Related Origins\n        \"pm-30529-webauthn-related-origins\",\n    ];\n    let configured_flags = parse_experimental_client_feature_flags(&cfg.experimental_client_feature_flags);\n    let invalid_flags: Vec<_> = configured_flags.keys().filter(|flag| !KNOWN_FLAGS.contains(&flag.as_str())).collect();\n    if !invalid_flags.is_empty() {\n        err!(format!(\"Unrecognized experimental client feature flags: {invalid_flags:?}.\\n\\n\\\n                     Please ensure all feature flags are spelled correctly and that they are supported in this version.\\n\\\n                     Supported flags: {KNOWN_FLAGS:?}\"));\n    }\n\n    const MAX_FILESIZE_KB: i64 = i64::MAX >> 10;\n\n    if let Some(limit) = cfg.user_attachment_limit {\n        if !(0i64..=MAX_FILESIZE_KB).contains(&limit) {\n            err!(\"`USER_ATTACHMENT_LIMIT` is out of bounds\");\n        }\n    }\n\n    if let Some(limit) = cfg.org_attachment_limit {\n        if !(0i64..=MAX_FILESIZE_KB).contains(&limit) {\n            err!(\"`ORG_ATTACHMENT_LIMIT` is out of bounds\");\n        }\n    }\n\n    if let Some(limit) = cfg.user_send_limit {\n        if !(0i64..=MAX_FILESIZE_KB).contains(&limit) {\n            err!(\"`USER_SEND_LIMIT` is out of bounds\");\n        }\n    }\n\n    if cfg._enable_duo\n        && (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())\n        && !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())\n    {\n        err!(\"All Duo options need to be set for global Duo support\")\n    }\n\n    if cfg.sso_enabled {\n        if cfg.sso_client_id.is_empty() || cfg.sso_client_secret.is_empty() || cfg.sso_authority.is_empty() {\n            err!(\"`SSO_CLIENT_ID`, `SSO_CLIENT_SECRET` and `SSO_AUTHORITY` must be set for SSO support\")\n        }\n\n        validate_internal_sso_issuer_url(&cfg.sso_authority)?;\n        validate_internal_sso_redirect_url(&cfg.sso_callback_path)?;\n        validate_sso_master_password_policy(&cfg.sso_master_password_policy)?;\n    }\n\n    if cfg._enable_yubico {\n        if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {\n            err!(\"Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support\")\n        }\n\n        if let Some(yubico_server) = &cfg.yubico_server {\n            let yubico_server = yubico_server.to_lowercase();\n            if !yubico_server.starts_with(\"https://\") {\n                err!(\"`YUBICO_SERVER` must be a valid URL and start with 'https://'. Either unset this variable or provide a valid URL.\")\n            }\n        }\n    }\n\n    if cfg._enable_smtp {\n        match cfg.smtp_security.as_str() {\n            \"off\" | \"starttls\" | \"force_tls\" => (),\n            _ => err!(\n                \"`SMTP_SECURITY` is invalid. It needs to be one of the following options: starttls, force_tls or off\"\n            ),\n        }\n\n        if cfg.use_sendmail {\n            let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!(\"sendmail{EXE_SUFFIX}\"));\n\n            let mut path = std::path::PathBuf::from(&command);\n            // Check if we can find the sendmail command to execute when no absolute path is given\n            if !path.is_absolute() {\n                let Ok(which_path) = which::which(&command) else {\n                    err!(format!(\"sendmail command {command} not found in $PATH\"))\n                };\n                path = which_path;\n            }\n\n            match path.metadata() {\n                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n                    err!(format!(\"sendmail command not found at `{path:?}`\"))\n                }\n                Err(err) => {\n                    err!(format!(\"failed to access sendmail command at `{path:?}`: {err}\"))\n                }\n                Ok(metadata) => {\n                    if metadata.is_dir() {\n                        err!(format!(\"sendmail command at `{path:?}` isn't a directory\"));\n                    }\n\n                    #[cfg(unix)]\n                    {\n                        use std::os::unix::fs::PermissionsExt;\n                        if !metadata.permissions().mode() & 0o111 != 0 {\n                            err!(format!(\"sendmail command at `{path:?}` isn't executable\"));\n                        }\n                    }\n                }\n            }\n        } else {\n            if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {\n                err!(\"Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`\")\n            }\n\n            if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {\n                err!(\"Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`\")\n            }\n        }\n\n        if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) {\n            err!(format!(\"SMTP_FROM '{}' is not a valid email address\", cfg.smtp_from))\n        }\n\n        if cfg._enable_email_2fa && cfg.email_token_size < 6 {\n            err!(\"`EMAIL_TOKEN_SIZE` has a minimum size of 6\")\n        }\n    }\n\n    if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) {\n        err!(\"To enable email 2FA, a mail transport must be configured\")\n    }\n\n    if !cfg._enable_email_2fa && cfg.email_2fa_enforce_on_verified_invite {\n        err!(\"To enforce email 2FA on verified invitations, email 2fa has to be enabled!\");\n    }\n    if !cfg._enable_email_2fa && cfg.email_2fa_auto_fallback {\n        err!(\"To use email 2FA as automatic fallback, email 2fa has to be enabled!\");\n    }\n\n    // Check if the HTTP request block regex is valid\n    if let Some(ref r) = cfg.http_request_block_regex {\n        let validate_regex = regex::Regex::new(r);\n        match validate_regex {\n            Ok(_) => (),\n            Err(e) => err!(format!(\"`HTTP_REQUEST_BLOCK_REGEX` is invalid: {e:#?}\")),\n        }\n    }\n\n    // Check if the icon service is valid\n    let icon_service = cfg.icon_service.as_str();\n    match icon_service {\n        \"internal\" | \"bitwarden\" | \"duckduckgo\" | \"google\" => (),\n        _ => {\n            if !icon_service.starts_with(\"http\") {\n                err!(format!(\"Icon service URL `{icon_service}` must start with \\\"http\\\"\"))\n            }\n            match icon_service.matches(\"{}\").count() {\n                1 => (), // nominal\n                0 => err!(format!(\"Icon service URL `{icon_service}` has no placeholder \\\"{{}}\\\"\")),\n                _ => err!(format!(\"Icon service URL `{icon_service}` has more than one placeholder \\\"{{}}\\\"\")),\n            }\n        }\n    }\n\n    // Check if the icon redirect code is valid\n    match cfg.icon_redirect_code {\n        301 | 302 | 307 | 308 => (),\n        _ => err!(\"Only HTTP 301/302 and 307/308 redirects are supported\"),\n    }\n\n    if cfg.invitation_expiration_hours < 1 {\n        err!(\"`INVITATION_EXPIRATION_HOURS` has a minimum duration of 1 hour\")\n    }\n\n    // Validate schedule crontab format\n    if !cfg.send_purge_schedule.is_empty() && cfg.send_purge_schedule.parse::<Schedule>().is_err() {\n        err!(\"`SEND_PURGE_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.trash_purge_schedule.is_empty() && cfg.trash_purge_schedule.parse::<Schedule>().is_err() {\n        err!(\"`TRASH_PURGE_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.incomplete_2fa_schedule.is_empty() && cfg.incomplete_2fa_schedule.parse::<Schedule>().is_err() {\n        err!(\"`INCOMPLETE_2FA_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.emergency_notification_reminder_schedule.is_empty()\n        && cfg.emergency_notification_reminder_schedule.parse::<Schedule>().is_err()\n    {\n        err!(\"`EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.emergency_request_timeout_schedule.is_empty()\n        && cfg.emergency_request_timeout_schedule.parse::<Schedule>().is_err()\n    {\n        err!(\"`EMERGENCY_REQUEST_TIMEOUT_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.event_cleanup_schedule.is_empty() && cfg.event_cleanup_schedule.parse::<Schedule>().is_err() {\n        err!(\"`EVENT_CLEANUP_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.auth_request_purge_schedule.is_empty() && cfg.auth_request_purge_schedule.parse::<Schedule>().is_err() {\n        err!(\"`AUTH_REQUEST_PURGE_SCHEDULE` is not a valid cron expression\")\n    }\n\n    if !cfg.disable_admin_token {\n        match cfg.admin_token.as_ref() {\n            Some(t) if t.starts_with(\"$argon2\") => {\n                if let Err(e) = argon2::password_hash::PasswordHash::new(t) {\n                    err!(format!(\"The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: '{e}'\"))\n                }\n            }\n            Some(_) => {\n                println!(\n                    \"[NOTICE] You are using a plain text `ADMIN_TOKEN` which is insecure.\\n\\\n                Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.\\n\\\n                See: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token\\n\"\n                );\n            }\n            _ => {}\n        }\n    }\n\n    if cfg.increase_note_size_limit {\n        println!(\"[WARNING] Secure Note size limit is increased to 100_000!\");\n        println!(\"[WARNING] This could cause issues with clients. Also exports will not work on Bitwarden servers!.\");\n    }\n    Ok(())\n}\n\nfn validate_internal_sso_issuer_url(sso_authority: &String) -> Result<openidconnect::IssuerUrl, Error> {\n    match openidconnect::IssuerUrl::new(sso_authority.clone()) {\n        Err(err) => err!(format!(\"Invalid sso_authority URL ({sso_authority}): {err}\")),\n        Ok(issuer_url) => Ok(issuer_url),\n    }\n}\n\nfn validate_internal_sso_redirect_url(sso_callback_path: &String) -> Result<openidconnect::RedirectUrl, Error> {\n    match openidconnect::RedirectUrl::new(sso_callback_path.clone()) {\n        Err(err) => err!(format!(\"Invalid sso_callback_path ({sso_callback_path} built using `domain`) URL: {err}\")),\n        Ok(redirect_url) => Ok(redirect_url),\n    }\n}\n\nfn validate_sso_master_password_policy(\n    sso_master_password_policy: &Option<String>,\n) -> Result<Option<serde_json::Value>, Error> {\n    let policy = sso_master_password_policy.as_ref().map(|mpp| serde_json::from_str::<serde_json::Value>(mpp));\n\n    match policy {\n        None => Ok(None),\n        Some(Ok(jsobject @ serde_json::Value::Object(_))) => Ok(Some(jsobject)),\n        Some(Ok(_)) => err!(\"Invalid sso_master_password_policy: parsed value is not a JSON object\"),\n        Some(Err(error)) => {\n            err!(format!(\"Invalid sso_master_password_policy ({error}), Ensure that it's correctly escaped with ''\"))\n        }\n    }\n}\n\n/// Extracts an RFC 6454 web origin from a URL.\nfn extract_url_origin(url: &str) -> String {\n    match Url::parse(url) {\n        Ok(u) => u.origin().ascii_serialization(),\n        Err(e) => {\n            println!(\"Error validating domain: {e}\");\n            String::new()\n        }\n    }\n}\n\n/// Extracts the path from a URL.\n/// All trailing '/' chars are trimmed, even if the path is a lone '/'.\nfn extract_url_path(url: &str) -> String {\n    match Url::parse(url) {\n        Ok(u) => u.path().trim_end_matches('/').to_string(),\n        Err(_) => {\n            // We already print it in the method above, no need to do it again\n            String::new()\n        }\n    }\n}\n\nfn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {\n    if embed_images {\n        \"cid:\".to_string()\n    } else {\n        // normalize base_url\n        let base_url = domain.trim_end_matches('/');\n        format!(\"{base_url}/vw_static/\")\n    }\n}\n\nfn generate_sso_callback_path(domain: &str) -> String {\n    // normalize base_url\n    let base_url = domain.trim_end_matches('/');\n    format!(\"{base_url}/identity/connect/oidc-signin\")\n}\n\n/// Generate the correct URL for the icon service.\n/// This will be used within icons.rs to call the external icon service.\nfn generate_icon_service_url(icon_service: &str) -> String {\n    match icon_service {\n        \"internal\" => String::new(),\n        \"bitwarden\" => \"https://icons.bitwarden.net/{}/icon.png\".to_string(),\n        \"duckduckgo\" => \"https://icons.duckduckgo.com/ip3/{}.ico\".to_string(),\n        \"google\" => \"https://www.google.com/s2/favicons?domain={}&sz=32\".to_string(),\n        _ => icon_service.to_string(),\n    }\n}\n\n/// Generate the CSP string needed to allow redirected icon fetching\nfn generate_icon_service_csp(icon_service: &str, icon_service_url: &str) -> String {\n    // We split on the first '{', since that is the variable delimiter for an icon service URL.\n    // Everything up until the first '{' should be fixed and can be used as an CSP string.\n    let csp_string = match icon_service_url.split_once('{') {\n        Some((c, _)) => c.to_string(),\n        None => String::new(),\n    };\n\n    // Because Google does a second redirect to there gstatic.com domain, we need to add an extra csp string.\n    match icon_service {\n        \"google\" => csp_string + \" https://*.gstatic.com/favicon\",\n        _ => csp_string,\n    }\n}\n\n/// Convert the old SMTP_SSL and SMTP_EXPLICIT_TLS options\nfn smtp_convert_deprecated_ssl_options(smtp_ssl: Option<bool>, smtp_explicit_tls: Option<bool>) -> String {\n    if smtp_explicit_tls.is_some() || smtp_ssl.is_some() {\n        println!(\"[DEPRECATED]: `SMTP_SSL` or `SMTP_EXPLICIT_TLS` is set. Please use `SMTP_SECURITY` instead.\");\n    }\n    if smtp_explicit_tls.is_some() && smtp_explicit_tls.unwrap() {\n        return \"force_tls\".to_string();\n    } else if smtp_ssl.is_some() && !smtp_ssl.unwrap() {\n        return \"off\".to_string();\n    }\n    // Return the default `starttls` in all other cases\n    \"starttls\".to_string()\n}\n\nfn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {\n    // Cache of previously built operators by path\n    static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =\n        LazyLock::new(dashmap::DashMap::new);\n\n    if let Some(operator) = OPERATORS_BY_PATH.get(path) {\n        return Ok(operator.clone());\n    }\n\n    let operator = if path.starts_with(\"s3://\") {\n        #[cfg(not(s3))]\n        return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, \"S3 support is not enabled\").into());\n\n        #[cfg(s3)]\n        opendal_s3_operator_for_path(path)?\n    } else {\n        let builder = opendal::services::Fs::default().root(path);\n        opendal::Operator::new(builder)?.finish()\n    };\n\n    OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());\n\n    Ok(operator)\n}\n\n#[cfg(s3)]\nfn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {\n    use crate::http_client::aws::AwsReqwestConnector;\n    use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};\n\n    // This is a custom AWS credential loader that uses the official AWS Rust\n    // SDK config crate to load credentials. This ensures maximum compatibility\n    // with AWS credential configurations. For example, OpenDAL doesn't support\n    // AWS SSO temporary credentials yet.\n    struct OpenDALS3CredentialLoader {}\n\n    #[async_trait]\n    impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader {\n        async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result<Option<reqsign::AwsCredential>> {\n            use aws_credential_types::provider::ProvideCredentials as _;\n            use tokio::sync::OnceCell;\n\n            static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();\n\n            let chain = DEFAULT_CREDENTIAL_CHAIN\n                .get_or_init(|| {\n                    let reqwest_client = reqwest::Client::builder().build().unwrap();\n                    let connector = AwsReqwestConnector {\n                        client: reqwest_client,\n                    };\n\n                    let conf = ProviderConfig::default().with_http_client(connector);\n\n                    DefaultCredentialsChain::builder().configure(conf).build()\n                })\n                .await;\n\n            let creds = chain.provide_credentials().await?;\n\n            Ok(Some(reqsign::AwsCredential {\n                access_key_id: creds.access_key_id().to_string(),\n                secret_access_key: creds.secret_access_key().to_string(),\n                session_token: creds.session_token().map(|s| s.to_string()),\n                expires_in: creds.expiry().map(|expiration| expiration.into()),\n            }))\n        }\n    }\n\n    const OPEN_DAL_S3_CREDENTIAL_LOADER: OpenDALS3CredentialLoader = OpenDALS3CredentialLoader {};\n\n    let url = Url::parse(path).map_err(|e| format!(\"Invalid path S3 URL path {path:?}: {e}\"))?;\n\n    let bucket = url.host_str().ok_or_else(|| format!(\"Missing Bucket name in data folder S3 URL {path:?}\"))?;\n\n    let builder = opendal::services::S3::default()\n        .customized_credential_load(Box::new(OPEN_DAL_S3_CREDENTIAL_LOADER))\n        .enable_virtual_host_style()\n        .bucket(bucket)\n        .root(url.path())\n        .default_storage_class(\"INTELLIGENT_TIERING\");\n\n    Ok(opendal::Operator::new(builder)?.finish())\n}\n\npub enum PathType {\n    Data,\n    IconCache,\n    Attachments,\n    Sends,\n    RsaKey,\n}\n\nimpl Config {\n    pub async fn load() -> Result<Self, Error> {\n        // Loading from env and file\n        let _env = ConfigBuilder::from_env();\n        let _usr = ConfigBuilder::from_file().await.unwrap_or_default();\n\n        // Create merged config, config file overwrites env\n        let mut _overrides = Vec::new();\n        let builder = _env.merge(&_usr, true, &mut _overrides);\n\n        // Fill any missing with defaults\n        let config = builder.build();\n        if !SKIP_CONFIG_VALIDATION.load(Ordering::Relaxed) {\n            validate_config(&config)?;\n        }\n\n        Ok(Config {\n            inner: RwLock::new(Inner {\n                rocket_shutdown_handle: None,\n                templates: load_templates(&config.templates_folder),\n                config,\n                _env,\n                _usr,\n                _overrides,\n            }),\n        })\n    }\n\n    pub async fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> {\n        // Remove default values\n        //let builder = other.remove(&self.inner.read().unwrap()._env);\n\n        // TODO: Remove values that are defaults, above only checks those set by env and not the defaults\n        let mut builder = other;\n\n        // Remove values that are not editable\n        if ignore_non_editable {\n            builder.clear_non_editable();\n        }\n\n        // Serialize now before we consume the builder\n        let config_str = serde_json::to_string_pretty(&builder)?;\n\n        // Prepare the combined config\n        let mut overrides = Vec::new();\n        let config = {\n            let env = &self.inner.read().unwrap()._env;\n            env.merge(&builder, false, &mut overrides).build()\n        };\n        validate_config(&config)?;\n\n        // Save both the user and the combined config\n        {\n            let mut writer = self.inner.write().unwrap();\n            writer.config = config;\n            writer._usr = builder;\n            writer._overrides = overrides;\n        }\n\n        //Save to file\n        let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;\n        operator.write(&CONFIG_FILENAME, config_str).await?;\n\n        Ok(())\n    }\n\n    async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {\n        let builder = {\n            let usr = &self.inner.read().unwrap()._usr;\n            let mut _overrides = Vec::new();\n            usr.merge(&other, false, &mut _overrides)\n        };\n        self.update_config(builder, false).await\n    }\n\n    /// Tests whether an email's domain is allowed. A domain is allowed if it\n    /// is in signups_domains_whitelist, or if no whitelist is set (so there\n    /// are no domain restrictions in effect).\n    pub fn is_email_domain_allowed(&self, email: &str) -> bool {\n        let e: Vec<&str> = email.rsplitn(2, '@').collect();\n        if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {\n            warn!(\"Failed to parse email address '{email}'\");\n            return false;\n        }\n        let email_domain = e[0].to_lowercase();\n        let whitelist = self.signups_domains_whitelist();\n\n        whitelist.is_empty() || whitelist.split(',').any(|d| d.trim() == email_domain)\n    }\n\n    /// Tests whether signup is allowed for an email address, taking into\n    /// account the signups_allowed and signups_domains_whitelist settings.\n    pub fn is_signup_allowed(&self, email: &str) -> bool {\n        if !self.signups_domains_whitelist().is_empty() {\n            // The whitelist setting overrides the signups_allowed setting.\n            self.is_email_domain_allowed(email)\n        } else {\n            self.signups_allowed()\n        }\n    }\n\n    // The registration link should be hidden if\n    //  - Signup is not allowed and email whitelist is empty unless mail is disabled and invitations are allowed\n    //  - The SSO is activated and password login is disabled.\n    pub fn is_signup_disabled(&self) -> bool {\n        (!self.signups_allowed()\n            && self.signups_domains_whitelist().is_empty()\n            && (self.mail_enabled() || !self.invitations_allowed()))\n            || (self.sso_enabled() && self.sso_only())\n    }\n\n    /// Tests whether the specified user is allowed to create an organization.\n    pub fn is_org_creation_allowed(&self, email: &str) -> bool {\n        let users = self.org_creation_users();\n        if users.is_empty() || users == \"all\" {\n            true\n        } else if users == \"none\" {\n            false\n        } else {\n            let email = email.to_lowercase();\n            users.split(',').any(|u| u.trim() == email)\n        }\n    }\n\n    pub async fn delete_user_config(&self) -> Result<(), Error> {\n        let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;\n        operator.delete(&CONFIG_FILENAME).await?;\n\n        // Empty user config\n        let usr = ConfigBuilder::default();\n\n        // Config now is env + defaults\n        let config = {\n            let env = &self.inner.read().unwrap()._env;\n            env.build()\n        };\n\n        // Save configs\n        {\n            let mut writer = self.inner.write().unwrap();\n            writer.config = config;\n            writer._usr = usr;\n            writer._overrides = Vec::new();\n        }\n\n        Ok(())\n    }\n\n    pub fn private_rsa_key(&self) -> String {\n        format!(\"{}.pem\", self.rsa_key_filename())\n    }\n    pub fn mail_enabled(&self) -> bool {\n        let inner = &self.inner.read().unwrap().config;\n        inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)\n    }\n\n    pub async fn get_duo_akey(&self) -> String {\n        if let Some(akey) = self._duo_akey() {\n            akey\n        } else {\n            let akey_s = crate::crypto::encode_random_bytes::<64>(&data_encoding::BASE64);\n\n            // Save the new value\n            let builder = ConfigBuilder {\n                _duo_akey: Some(akey_s.clone()),\n                ..Default::default()\n            };\n            self.update_config_partial(builder).await.ok();\n\n            akey_s\n        }\n    }\n\n    pub fn is_webauthn_2fa_supported(&self) -> bool {\n        Url::parse(&self.domain()).expect(\"DOMAIN not a valid URL\").domain().is_some()\n    }\n\n    /// Tests whether the admin token is set to a non-empty value.\n    pub fn is_admin_token_set(&self) -> bool {\n        let token = self.admin_token();\n\n        token.is_some() && !token.unwrap().trim().is_empty()\n    }\n\n    pub fn opendal_operator_for_path_type(&self, path_type: &PathType) -> Result<opendal::Operator, Error> {\n        let path = match path_type {\n            PathType::Data => self.data_folder(),\n            PathType::IconCache => self.icon_cache_folder(),\n            PathType::Attachments => self.attachments_folder(),\n            PathType::Sends => self.sends_folder(),\n            PathType::RsaKey => std::path::Path::new(&self.rsa_key_filename())\n                .parent()\n                .ok_or_else(|| std::io::Error::other(\"Failed to get directory of RSA key file\"))?\n                .to_str()\n                .ok_or_else(|| std::io::Error::other(\"Failed to convert RSA key file directory to UTF-8 string\"))?\n                .to_string(),\n        };\n\n        opendal_operator_for_path(&path)\n    }\n\n    pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {\n        if self.reload_templates() {\n            warn!(\"RELOADING TEMPLATES\");\n            let hb = load_templates(CONFIG.templates_folder());\n            hb.render(name, data).map_err(Into::into)\n        } else {\n            let hb = &self.inner.read().unwrap().templates;\n            hb.render(name, data).map_err(Into::into)\n        }\n    }\n\n    pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {\n        let hb = &self.inner.read().unwrap().templates;\n        hb.render(&format!(\"fallback_{name}\"), data).map_err(Into::into)\n    }\n\n    pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {\n        self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);\n    }\n\n    pub fn shutdown(&self) {\n        if let Ok(mut c) = self.inner.write() {\n            if let Some(handle) = c.rocket_shutdown_handle.take() {\n                handle.notify();\n            }\n        }\n    }\n\n    pub fn sso_issuer_url(&self) -> Result<openidconnect::IssuerUrl, Error> {\n        validate_internal_sso_issuer_url(&self.sso_authority())\n    }\n\n    pub fn sso_redirect_url(&self) -> Result<openidconnect::RedirectUrl, Error> {\n        validate_internal_sso_redirect_url(&self.sso_callback_path())\n    }\n\n    pub fn sso_master_password_policy_value(&self) -> Option<serde_json::Value> {\n        validate_sso_master_password_policy(&self.sso_master_password_policy()).ok().flatten()\n    }\n\n    pub fn sso_scopes_vec(&self) -> Vec<String> {\n        self.sso_scopes().split_whitespace().map(str::to_string).collect()\n    }\n\n    pub fn sso_authorize_extra_params_vec(&self) -> Vec<(String, String)> {\n        url::form_urlencoded::parse(self.sso_authorize_extra_params().as_bytes()).into_owned().collect()\n    }\n}\n\nuse handlebars::{\n    Context, DirectorySourceOptions, Handlebars, Helper, HelperResult, Output, RenderContext, RenderErrorReason,\n    Renderable,\n};\n\nfn load_templates<P>(path: P) -> Handlebars<'static>\nwhere\n    P: AsRef<std::path::Path>,\n{\n    let mut hb = Handlebars::new();\n    // Error on missing params\n    hb.set_strict_mode(true);\n    // Register helpers\n    hb.register_helper(\"case\", Box::new(case_helper));\n    hb.register_helper(\"to_json\", Box::new(to_json));\n    hb.register_helper(\"webver\", Box::new(webver));\n    hb.register_helper(\"vwver\", Box::new(vwver));\n\n    macro_rules! reg {\n        ($name:expr) => {{\n            let template = include_str!(concat!(\"static/templates/\", $name, \".hbs\"));\n            hb.register_template_string($name, template).unwrap();\n        }};\n        ($name:expr, $ext:expr) => {{\n            reg!($name);\n            reg!(concat!($name, $ext));\n        }};\n        (@withfallback $name:expr) => {{\n            let template = include_str!(concat!(\"static/templates/\", $name, \".hbs\"));\n            hb.register_template_string($name, template).unwrap();\n            hb.register_template_string(concat!(\"fallback_\", $name), template).unwrap();\n        }};\n    }\n\n    // First register default templates here\n    reg!(\"email/email_header\");\n    reg!(\"email/email_footer\");\n    reg!(\"email/email_footer_text\");\n\n    reg!(\"email/admin_reset_password\", \".html\");\n    reg!(\"email/change_email_existing\", \".html\");\n    reg!(\"email/change_email_invited\", \".html\");\n    reg!(\"email/change_email\", \".html\");\n    reg!(\"email/delete_account\", \".html\");\n    reg!(\"email/emergency_access_invite_accepted\", \".html\");\n    reg!(\"email/emergency_access_invite_confirmed\", \".html\");\n    reg!(\"email/emergency_access_recovery_approved\", \".html\");\n    reg!(\"email/emergency_access_recovery_initiated\", \".html\");\n    reg!(\"email/emergency_access_recovery_rejected\", \".html\");\n    reg!(\"email/emergency_access_recovery_reminder\", \".html\");\n    reg!(\"email/emergency_access_recovery_timed_out\", \".html\");\n    reg!(\"email/incomplete_2fa_login\", \".html\");\n    reg!(\"email/invite_accepted\", \".html\");\n    reg!(\"email/invite_confirmed\", \".html\");\n    reg!(\"email/new_device_logged_in\", \".html\");\n    reg!(\"email/protected_action\", \".html\");\n    reg!(\"email/pw_hint_none\", \".html\");\n    reg!(\"email/pw_hint_some\", \".html\");\n    reg!(\"email/register_verify_email\", \".html\");\n    reg!(\"email/send_2fa_removed_from_org\", \".html\");\n    reg!(\"email/send_emergency_access_invite\", \".html\");\n    reg!(\"email/send_org_invite\", \".html\");\n    reg!(\"email/send_single_org_removed_from_org\", \".html\");\n    reg!(\"email/smtp_test\", \".html\");\n    reg!(\"email/sso_change_email\", \".html\");\n    reg!(\"email/twofactor_email\", \".html\");\n    reg!(\"email/verify_email\", \".html\");\n    reg!(\"email/welcome_must_verify\", \".html\");\n    reg!(\"email/welcome\", \".html\");\n\n    reg!(\"admin/base\");\n    reg!(\"admin/login\");\n    reg!(\"admin/settings\");\n    reg!(\"admin/users\");\n    reg!(\"admin/organizations\");\n    reg!(\"admin/diagnostics\");\n\n    reg!(\"404\");\n\n    reg!(@withfallback \"scss/vaultwarden.scss\");\n    reg!(\"scss/user.vaultwarden.scss\");\n\n    // And then load user templates to overwrite the defaults\n    // Use .hbs extension for the files\n    // Templates get registered with their relative name\n    hb.register_templates_directory(path, DirectorySourceOptions::default()).unwrap();\n\n    hb\n}\n\nfn case_helper<'reg, 'rc>(\n    h: &Helper<'rc>,\n    r: &'reg Handlebars<'_>,\n    ctx: &'rc Context,\n    rc: &mut RenderContext<'reg, 'rc>,\n    out: &mut dyn Output,\n) -> HelperResult {\n    let param =\n        h.param(0).ok_or_else(|| RenderErrorReason::Other(String::from(\"Param not found for helper \\\"case\\\"\")))?;\n    let value = param.value().clone();\n\n    if h.params().iter().skip(1).any(|x| x.value() == &value) {\n        h.template().map(|t| t.render(r, ctx, rc, out)).unwrap_or_else(|| Ok(()))\n    } else {\n        Ok(())\n    }\n}\n\nfn to_json<'reg, 'rc>(\n    h: &Helper<'rc>,\n    _r: &'reg Handlebars<'_>,\n    _ctx: &'rc Context,\n    _rc: &mut RenderContext<'reg, 'rc>,\n    out: &mut dyn Output,\n) -> HelperResult {\n    let param = h\n        .param(0)\n        .ok_or_else(|| RenderErrorReason::Other(String::from(\"Expected 1 parameter for \\\"to_json\\\"\")))?\n        .value();\n    let json = serde_json::to_string(param)\n        .map_err(|e| RenderErrorReason::Other(format!(\"Can't serialize parameter to JSON: {e}\")))?;\n    out.write(&json)?;\n    Ok(())\n}\n\n// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.\n// The default is based upon the version since this feature is added.\nstatic WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {\n    let vault_version = get_active_web_release();\n    // Use a single regex capture to extract version components\n    let re = regex::Regex::new(r\"(\\d{4})\\.(\\d{1,2})\\.(\\d{1,2})\").unwrap();\n    re.captures(&vault_version)\n        .and_then(|c| {\n            (c.len() == 4).then(|| {\n                format!(\"{}.{}.{}\", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())\n            })\n        })\n        .and_then(|v| semver::Version::parse(&v).ok())\n        .unwrap_or_else(|| semver::Version::parse(\"2024.6.2\").unwrap())\n});\n\n// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.\n// The default is based upon the version since this feature is added.\nstatic VW_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {\n    let vw_version = crate::VERSION.unwrap_or(\"1.32.5\");\n    // Use a single regex capture to extract version components\n    let re = regex::Regex::new(r\"(\\d{1})\\.(\\d{1,2})\\.(\\d{1,2})\").unwrap();\n    re.captures(vw_version)\n        .and_then(|c| {\n            (c.len() == 4).then(|| {\n                format!(\"{}.{}.{}\", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str())\n            })\n        })\n        .and_then(|v| semver::Version::parse(&v).ok())\n        .unwrap_or_else(|| semver::Version::parse(\"1.32.5\").unwrap())\n});\n\nhandlebars::handlebars_helper!(webver: | web_vault_version: String |\n    semver::VersionReq::parse(&web_vault_version).expect(\"Invalid web-vault version compare string\").matches(&WEB_VAULT_VERSION)\n);\nhandlebars::handlebars_helper!(vwver: | vw_version: String |\n    semver::VersionReq::parse(&vw_version).expect(\"Invalid Vaultwarden version compare string\").matches(&VW_VERSION)\n);\n"
  },
  {
    "path": "src/crypto.rs",
    "content": "//\n// PBKDF2 derivation\n//\nuse std::num::NonZeroU32;\n\nuse data_encoding::{Encoding, HEXLOWER};\nuse ring::{digest, hmac, pbkdf2};\n\nconst DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;\nconst OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;\n\npub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {\n    let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros\n\n    let iterations = NonZeroU32::new(iterations).expect(\"Iterations can't be zero\");\n    pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);\n\n    out\n}\n\npub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {\n    let iterations = NonZeroU32::new(iterations).expect(\"Iterations can't be zero\");\n    pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()\n}\n\n//\n// HMAC\n//\npub fn hmac_sign(key: &str, data: &str) -> String {\n    let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key.as_bytes());\n    let signature = hmac::sign(&key, data.as_bytes());\n\n    HEXLOWER.encode(signature.as_ref())\n}\n\n//\n// Random values\n//\n\n/// Return an array holding `N` random bytes.\npub fn get_random_bytes<const N: usize>() -> [u8; N] {\n    use ring::rand::{SecureRandom, SystemRandom};\n\n    let mut array = [0; N];\n    SystemRandom::new().fill(&mut array).expect(\"Error generating random values\");\n\n    array\n}\n\n/// Encode random bytes using the provided function.\npub fn encode_random_bytes<const N: usize>(e: &Encoding) -> String {\n    e.encode(&get_random_bytes::<N>())\n}\n\n/// Generates a random string over a specified alphabet.\npub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String {\n    // Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html\n    use rand::RngExt;\n    let mut rng = rand::rng();\n\n    (0..num_chars)\n        .map(|_| {\n            let i = rng.random_range(0..alphabet.len());\n            char::from(alphabet[i])\n        })\n        .collect()\n}\n\n/// Generates a random numeric string.\npub fn get_random_string_numeric(num_chars: usize) -> String {\n    const ALPHABET: &[u8] = b\"0123456789\";\n    get_random_string(ALPHABET, num_chars)\n}\n\n/// Generates a random alphanumeric string.\npub fn get_random_string_alphanum(num_chars: usize) -> String {\n    const ALPHABET: &[u8] = b\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\\\n                              abcdefghijklmnopqrstuvwxyz\\\n                              0123456789\";\n    get_random_string(ALPHABET, num_chars)\n}\n\npub fn generate_id<const N: usize>() -> String {\n    encode_random_bytes::<N>(&HEXLOWER)\n}\n\npub fn generate_send_file_id() -> String {\n    // Send File IDs are globally scoped, so make them longer to avoid collisions.\n    generate_id::<32>() // 256 bits\n}\n\nuse crate::db::models::AttachmentId;\npub fn generate_attachment_id() -> AttachmentId {\n    // Attachment IDs are scoped to a cipher, so they can be smaller.\n    AttachmentId(generate_id::<10>()) // 80 bits\n}\n\n/// Generates a numeric token for email-based verifications.\npub fn generate_email_token(token_size: u8) -> String {\n    get_random_string_numeric(token_size as usize)\n}\n\n/// Generates a personal API key.\n/// Upstream uses 30 chars, which is ~178 bits of entropy.\npub fn generate_api_key() -> String {\n    get_random_string_alphanum(30)\n}\n\n//\n// Constant time compare\n//\npub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {\n    use subtle::ConstantTimeEq;\n    a.as_ref().ct_eq(b.as_ref()).into()\n}\n"
  },
  {
    "path": "src/db/mod.rs",
    "content": "mod query_logger;\n\nuse std::{\n    sync::{Arc, OnceLock},\n    time::Duration,\n};\n\nuse diesel::{\n    connection::SimpleConnection,\n    r2d2::{CustomizeConnection, Pool, PooledConnection},\n    Connection, RunQueryDsl,\n};\n\nuse rocket::{\n    http::Status,\n    request::{FromRequest, Outcome},\n    Request,\n};\n\nuse tokio::{\n    sync::{Mutex, OwnedSemaphorePermit, Semaphore},\n    time::timeout,\n};\n\nuse crate::{\n    error::{Error, MapResult},\n    CONFIG,\n};\n\n// These changes are based on Rocket 0.5-rc wrapper of Diesel: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/contrib/sync_db_pools\n// A wrapper around spawn_blocking that propagates panics to the calling code.\npub async fn run_blocking<F, R>(job: F) -> R\nwhere\n    F: FnOnce() -> R + Send + 'static,\n    R: Send + 'static,\n{\n    match tokio::task::spawn_blocking(job).await {\n        Ok(ret) => ret,\n        Err(e) => match e.try_into_panic() {\n            Ok(panic) => std::panic::resume_unwind(panic),\n            Err(_) => unreachable!(\"spawn_blocking tasks are never cancelled\"),\n        },\n    }\n}\n\n// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported\n#[derive(diesel::MultiConnection)]\npub enum DbConnInner {\n    #[cfg(mysql)]\n    Mysql(diesel::mysql::MysqlConnection),\n    #[cfg(postgresql)]\n    Postgresql(diesel::pg::PgConnection),\n    #[cfg(sqlite)]\n    Sqlite(diesel::sqlite::SqliteConnection),\n}\n\n/// Custom connection manager that implements manual connection establishment\npub struct DbConnManager {\n    database_url: String,\n}\n\nimpl DbConnManager {\n    pub fn new(database_url: &str) -> Self {\n        Self {\n            database_url: database_url.to_string(),\n        }\n    }\n\n    fn establish_connection(&self) -> Result<DbConnInner, diesel::r2d2::Error> {\n        match DbConnType::from_url(&self.database_url) {\n            #[cfg(mysql)]\n            Ok(DbConnType::Mysql) => {\n                let conn = diesel::mysql::MysqlConnection::establish(&self.database_url)?;\n                Ok(DbConnInner::Mysql(conn))\n            }\n            #[cfg(postgresql)]\n            Ok(DbConnType::Postgresql) => {\n                let conn = diesel::pg::PgConnection::establish(&self.database_url)?;\n                Ok(DbConnInner::Postgresql(conn))\n            }\n            #[cfg(sqlite)]\n            Ok(DbConnType::Sqlite) => {\n                let conn = diesel::sqlite::SqliteConnection::establish(&self.database_url)?;\n                Ok(DbConnInner::Sqlite(conn))\n            }\n\n            Err(e) => Err(diesel::r2d2::Error::ConnectionError(diesel::ConnectionError::InvalidConnectionUrl(\n                format!(\"Unable to estabilsh a connection: {e:?}\"),\n            ))),\n        }\n    }\n}\n\nimpl diesel::r2d2::ManageConnection for DbConnManager {\n    type Connection = DbConnInner;\n    type Error = diesel::r2d2::Error;\n\n    fn connect(&self) -> Result<Self::Connection, Self::Error> {\n        self.establish_connection()\n    }\n\n    fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {\n        use diesel::r2d2::R2D2Connection;\n        conn.ping().map_err(diesel::r2d2::Error::QueryError)\n    }\n\n    fn has_broken(&self, conn: &mut Self::Connection) -> bool {\n        use diesel::r2d2::R2D2Connection;\n        conn.is_broken()\n    }\n}\n\n#[derive(Eq, PartialEq)]\npub enum DbConnType {\n    #[cfg(mysql)]\n    Mysql,\n    #[cfg(postgresql)]\n    Postgresql,\n    #[cfg(sqlite)]\n    Sqlite,\n}\n\npub static ACTIVE_DB_TYPE: OnceLock<DbConnType> = OnceLock::new();\n\npub struct DbConn {\n    conn: Arc<Mutex<Option<PooledConnection<DbConnManager>>>>,\n    permit: Option<OwnedSemaphorePermit>,\n}\n\n#[derive(Debug)]\npub struct DbConnOptions {\n    pub init_stmts: String,\n}\n\nimpl CustomizeConnection<DbConnInner, diesel::r2d2::Error> for DbConnOptions {\n    fn on_acquire(&self, conn: &mut DbConnInner) -> Result<(), diesel::r2d2::Error> {\n        if !self.init_stmts.is_empty() {\n            conn.batch_execute(&self.init_stmts).map_err(diesel::r2d2::Error::QueryError)?;\n        }\n        Ok(())\n    }\n}\n\n#[derive(Clone)]\npub struct DbPool {\n    // This is an 'Option' so that we can drop the pool in a 'spawn_blocking'.\n    pool: Option<Pool<DbConnManager>>,\n    semaphore: Arc<Semaphore>,\n}\n\nimpl Drop for DbConn {\n    fn drop(&mut self) {\n        let conn = Arc::clone(&self.conn);\n        let permit = self.permit.take();\n\n        // Since connection can't be on the stack in an async fn during an\n        // await, we have to spawn a new blocking-safe thread...\n        tokio::task::spawn_blocking(move || {\n            // And then re-enter the runtime to wait on the async mutex, but in a blocking fashion.\n            let mut conn = tokio::runtime::Handle::current().block_on(conn.lock_owned());\n\n            if let Some(conn) = conn.take() {\n                drop(conn);\n            }\n\n            // Drop permit after the connection is dropped\n            drop(permit);\n        });\n    }\n}\n\nimpl Drop for DbPool {\n    fn drop(&mut self) {\n        let pool = self.pool.take();\n        // Only use spawn_blocking if the Tokio runtime is still available\n        // Otherwise the pool will be dropped on the current thread\n        if let Ok(handle) = tokio::runtime::Handle::try_current() {\n            handle.spawn_blocking(move || drop(pool));\n        }\n    }\n}\n\nimpl DbPool {\n    // For the given database URL, guess its type, run migrations, create pool, and return it\n    pub fn from_config() -> Result<Self, Error> {\n        let db_url = CONFIG.database_url();\n        let conn_type = DbConnType::from_url(&db_url)?;\n\n        // Only set the default instrumentation if the log level is specifically set to either warn, info or debug\n        if log_enabled!(target: \"vaultwarden::db::query_logger\", log::Level::Warn)\n            || log_enabled!(target: \"vaultwarden::db::query_logger\", log::Level::Info)\n            || log_enabled!(target: \"vaultwarden::db::query_logger\", log::Level::Debug)\n        {\n            drop(diesel::connection::set_default_instrumentation(query_logger::simple_logger));\n        }\n\n        match conn_type {\n            #[cfg(mysql)]\n            DbConnType::Mysql => {\n                mysql_migrations::run_migrations(&db_url)?;\n            }\n            #[cfg(postgresql)]\n            DbConnType::Postgresql => {\n                postgresql_migrations::run_migrations(&db_url)?;\n            }\n            #[cfg(sqlite)]\n            DbConnType::Sqlite => {\n                sqlite_migrations::run_migrations(&db_url)?;\n            }\n        }\n\n        let max_conns = CONFIG.database_max_conns();\n        let manager = DbConnManager::new(&db_url);\n        let pool = Pool::builder()\n            .max_size(max_conns)\n            .min_idle(Some(CONFIG.database_min_conns()))\n            .idle_timeout(Some(Duration::from_secs(CONFIG.database_idle_timeout())))\n            .connection_timeout(Duration::from_secs(CONFIG.database_timeout()))\n            .connection_customizer(Box::new(DbConnOptions {\n                init_stmts: conn_type.get_init_stmts(),\n            }))\n            .build(manager)\n            .map_res(\"Failed to create pool\")?;\n\n        // Set a global to determine the database more easily throughout the rest of the code\n        if ACTIVE_DB_TYPE.set(conn_type).is_err() {\n            error!(\"Tried to set the active database connection type more than once.\")\n        }\n\n        Ok(DbPool {\n            pool: Some(pool),\n            semaphore: Arc::new(Semaphore::new(max_conns as usize)),\n        })\n    }\n\n    // Get a connection from the pool\n    pub async fn get(&self) -> Result<DbConn, Error> {\n        let duration = Duration::from_secs(CONFIG.database_timeout());\n        let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await {\n            Ok(p) => p.expect(\"Semaphore should be open\"),\n            Err(_) => {\n                err!(\"Timeout waiting for database connection\");\n            }\n        };\n\n        let p = self.pool.as_ref().expect(\"DbPool.pool should always be Some()\");\n        let pool = p.clone();\n        let c =\n            run_blocking(move || pool.get_timeout(duration)).await.map_res(\"Error retrieving connection from pool\")?;\n        Ok(DbConn {\n            conn: Arc::new(Mutex::new(Some(c))),\n            permit: Some(permit),\n        })\n    }\n}\n\nimpl DbConnType {\n    pub fn from_url(url: &str) -> Result<Self, Error> {\n        // Mysql\n        if url.len() > 6 && &url[..6] == \"mysql:\" {\n            #[cfg(mysql)]\n            return Ok(DbConnType::Mysql);\n\n            #[cfg(not(mysql))]\n            err!(\"`DATABASE_URL` is a MySQL URL, but the 'mysql' feature is not enabled\")\n\n        // Postgresql\n        } else if url.len() > 11 && (&url[..11] == \"postgresql:\" || &url[..9] == \"postgres:\") {\n            #[cfg(postgresql)]\n            return Ok(DbConnType::Postgresql);\n\n            #[cfg(not(postgresql))]\n            err!(\"`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled\")\n\n        //Sqlite\n        } else {\n            #[cfg(sqlite)]\n            return Ok(DbConnType::Sqlite);\n\n            #[cfg(not(sqlite))]\n            err!(\"`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled\")\n        }\n    }\n\n    pub fn get_init_stmts(&self) -> String {\n        let init_stmts = CONFIG.database_conn_init();\n        if !init_stmts.is_empty() {\n            init_stmts\n        } else {\n            self.default_init_stmts()\n        }\n    }\n\n    pub fn default_init_stmts(&self) -> String {\n        match self {\n            #[cfg(mysql)]\n            Self::Mysql => String::new(),\n            #[cfg(postgresql)]\n            Self::Postgresql => String::new(),\n            #[cfg(sqlite)]\n            Self::Sqlite => \"PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;\".to_string(),\n        }\n    }\n}\n\nimpl DbConn {\n    pub async fn run<F, R>(&self, f: F) -> R\n    where\n        F: FnOnce(&mut DbConnInner) -> R + Send,\n        R: Send + 'static,\n    {\n        let conn = Arc::clone(&self.conn);\n        let mut conn = conn.lock_owned().await;\n        let conn = conn.as_mut().expect(\"Internal invariant broken: self.conn is Some\");\n\n        // Run blocking can't be used due to the 'static limitation, use block_in_place instead\n        tokio::task::block_in_place(move || f(conn))\n    }\n}\n\n#[macro_export]\nmacro_rules! db_run {\n    ( $conn:ident: $body:block ) => {\n        $conn.run(move |$conn| $body).await\n    };\n\n    ( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {\n        $conn.run(move |$conn| {\n            match $conn {\n                $($(\n                #[cfg($db)]\n                pastey::paste!(&mut $crate::db::DbConnInner::[<$db:camel>](ref mut $conn)) => {\n                    $body\n                },\n            )+)+}\n        }).await\n    };\n}\n\n// Write all ToSql<Text, DB> and FromSql<Text, DB> given a serializable/deserializable type.\n#[macro_export]\nmacro_rules! impl_FromToSqlText {\n    ($name:ty) => {\n        #[cfg(mysql)]\n        impl ToSql<Text, diesel::mysql::Mysql> for $name {\n            fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::mysql::Mysql>) -> diesel::serialize::Result {\n                serde_json::to_writer(out, self).map(|_| diesel::serialize::IsNull::No).map_err(Into::into)\n            }\n        }\n\n        #[cfg(postgresql)]\n        impl ToSql<Text, diesel::pg::Pg> for $name {\n            fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result {\n                serde_json::to_writer(out, self).map(|_| diesel::serialize::IsNull::No).map_err(Into::into)\n            }\n        }\n\n        #[cfg(sqlite)]\n        impl ToSql<Text, diesel::sqlite::Sqlite> for $name {\n            fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::sqlite::Sqlite>) -> diesel::serialize::Result {\n                serde_json::to_string(self).map_err(Into::into).map(|str| {\n                    out.set_value(str);\n                    diesel::serialize::IsNull::No\n                })\n            }\n        }\n\n        impl<DB: diesel::backend::Backend> FromSql<Text, DB> for $name\n        where\n            String: FromSql<Text, DB>,\n        {\n            fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {\n                <String as FromSql<Text, DB>>::from_sql(bytes)\n                    .and_then(|str| serde_json::from_str(&str).map_err(Into::into))\n            }\n        }\n    };\n}\n\npub mod schema;\n\n// Reexport the models, needs to be after the macros are defined so it can access them\npub mod models;\n\n/// Creates a back-up of the sqlite database\n/// MySQL/MariaDB and PostgreSQL are not supported.\n#[cfg(sqlite)]\npub fn backup_sqlite() -> Result<String, Error> {\n    use diesel::Connection;\n    use std::{fs::File, io::Write};\n\n    let db_url = CONFIG.database_url();\n    if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) {\n        // Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't\n        // This way we can set a readonly flag on the opening mode without issues.\n        let mut conn = diesel::sqlite::SqliteConnection::establish(&format!(\"sqlite://{db_url}?mode=ro\"))?;\n\n        let db_path = std::path::Path::new(&db_url).parent().unwrap();\n        let backup_file = db_path\n            .join(format!(\"db_{}.sqlite3\", chrono::Utc::now().format(\"%Y%m%d_%H%M%S\")))\n            .to_string_lossy()\n            .into_owned();\n\n        match File::create(backup_file.clone()) {\n            Ok(mut f) => {\n                let serialized_db = conn.serialize_database_to_buffer();\n                f.write_all(serialized_db.as_slice()).expect(\"Error writing SQLite backup\");\n                Ok(backup_file)\n            }\n            Err(e) => {\n                err_silent!(format!(\"Unable to save SQLite backup: {e:?}\"))\n            }\n        }\n    } else {\n        err_silent!(\"The database type is not SQLite. Backups only works for SQLite databases\")\n    }\n}\n\n#[cfg(not(sqlite))]\npub fn backup_sqlite() -> Result<String, Error> {\n    err_silent!(\"The database type is not SQLite. Backups only works for SQLite databases\")\n}\n\n/// Get the SQL Server version\npub async fn get_sql_server_version(conn: &DbConn) -> String {\n    db_run! { conn:\n        postgresql,mysql {\n            diesel::select(diesel::dsl::sql::<diesel::sql_types::Text>(\"version();\"))\n            .get_result::<String>(conn)\n            .unwrap_or_else(|_| \"Unknown\".to_string())\n        }\n        sqlite {\n            diesel::select(diesel::dsl::sql::<diesel::sql_types::Text>(\"sqlite_version();\"))\n            .get_result::<String>(conn)\n            .unwrap_or_else(|_| \"Unknown\".to_string())\n        }\n    }\n}\n\n/// Attempts to retrieve a single connection from the managed database pool. If\n/// no pool is currently managed, fails with an `InternalServerError` status. If\n/// no connections are available, fails with a `ServiceUnavailable` status.\n#[rocket::async_trait]\nimpl<'r> FromRequest<'r> for DbConn {\n    type Error = ();\n\n    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {\n        match request.rocket().state::<DbPool>() {\n            Some(p) => match p.get().await {\n                Ok(dbconn) => Outcome::Success(dbconn),\n                _ => Outcome::Error((Status::ServiceUnavailable, ())),\n            },\n            None => Outcome::Error((Status::InternalServerError, ())),\n        }\n    }\n}\n\n// Embed the migrations from the migrations folder into the application\n// This way, the program automatically migrates the database to the latest version\n// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html\n#[cfg(sqlite)]\nmod sqlite_migrations {\n    use diesel::{Connection, RunQueryDsl};\n    use diesel_migrations::{EmbeddedMigrations, MigrationHarness};\n    pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(\"migrations/sqlite\");\n\n    pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {\n        // Establish a connection to the sqlite database (this will create a new one, if it does\n        // not exist, and exit if there is an error).\n        let mut connection = diesel::sqlite::SqliteConnection::establish(db_url)?;\n\n        // Run the migrations after successfully establishing a connection\n        // Disable Foreign Key Checks during migration\n        // Scoped to a connection.\n        diesel::sql_query(\"PRAGMA foreign_keys = OFF\")\n            .execute(&mut connection)\n            .expect(\"Failed to disable Foreign Key Checks during migrations\");\n\n        // Turn on WAL in SQLite\n        if crate::CONFIG.enable_db_wal() {\n            diesel::sql_query(\"PRAGMA journal_mode=wal\").execute(&mut connection).expect(\"Failed to turn on WAL\");\n        }\n\n        connection.run_pending_migrations(MIGRATIONS).expect(\"Error running migrations\");\n        Ok(())\n    }\n}\n\n#[cfg(mysql)]\nmod mysql_migrations {\n    use diesel::{Connection, RunQueryDsl};\n    use diesel_migrations::{EmbeddedMigrations, MigrationHarness};\n    pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(\"migrations/mysql\");\n\n    pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {\n        // Make sure the database is up to date (create if it doesn't exist, or run the migrations)\n        let mut connection = diesel::mysql::MysqlConnection::establish(db_url)?;\n\n        // Disable Foreign Key Checks during migration\n        // Scoped to a connection/session.\n        diesel::sql_query(\"SET FOREIGN_KEY_CHECKS = 0\")\n            .execute(&mut connection)\n            .expect(\"Failed to disable Foreign Key Checks during migrations\");\n\n        connection.run_pending_migrations(MIGRATIONS).expect(\"Error running migrations\");\n        Ok(())\n    }\n}\n\n#[cfg(postgresql)]\nmod postgresql_migrations {\n    use diesel::Connection;\n    use diesel_migrations::{EmbeddedMigrations, MigrationHarness};\n    pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(\"migrations/postgresql\");\n\n    pub fn run_migrations(db_url: &str) -> Result<(), super::Error> {\n        // Make sure the database is up to date (create if it doesn't exist, or run the migrations)\n        let mut connection = diesel::pg::PgConnection::establish(db_url)?;\n\n        connection.run_pending_migrations(MIGRATIONS).expect(\"Error running migrations\");\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/db/models/attachment.rs",
    "content": "use bigdecimal::{BigDecimal, ToPrimitive};\nuse derive_more::{AsRef, Deref, Display};\nuse diesel::prelude::*;\nuse serde_json::Value;\nuse std::time::Duration;\n\nuse super::{CipherId, OrganizationId, UserId};\nuse crate::db::schema::{attachments, ciphers};\nuse crate::{config::PathType, CONFIG};\nuse macros::IdFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = attachments)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(id))]\npub struct Attachment {\n    pub id: AttachmentId,\n    pub cipher_uuid: CipherId,\n    pub file_name: String, // encrypted\n    pub file_size: i64,\n    pub akey: Option<String>,\n}\n\n/// Local methods\nimpl Attachment {\n    pub const fn new(\n        id: AttachmentId,\n        cipher_uuid: CipherId,\n        file_name: String,\n        file_size: i64,\n        akey: Option<String>,\n    ) -> Self {\n        Self {\n            id,\n            cipher_uuid,\n            file_name,\n            file_size,\n            akey,\n        }\n    }\n\n    pub fn get_file_path(&self) -> String {\n        format!(\"{}/{}\", self.cipher_uuid, self.id)\n    }\n\n    pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {\n        let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;\n\n        if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {\n            let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));\n            Ok(format!(\"{host}/attachments/{}/{}?token={token}\", self.cipher_uuid, self.id))\n        } else {\n            Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string())\n        }\n    }\n\n    pub async fn to_json(&self, host: &str) -> Result<Value, crate::Error> {\n        Ok(json!({\n            \"id\": self.id,\n            \"url\": self.get_url(host).await?,\n            \"fileName\": self.file_name,\n            \"size\": self.file_size.to_string(),\n            \"sizeName\": crate::util::get_display_size(self.file_size),\n            \"key\": self.akey,\n            \"object\": \"attachment\"\n        }))\n    }\n}\n\nuse crate::auth::{encode_jwt, generate_file_download_claims};\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Attachment {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(attachments::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(attachments::table)\n                            .filter(attachments::id.eq(&self.id))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving attachment\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving attachment\")\n            }\n            postgresql {\n                diesel::insert_into(attachments::table)\n                    .values(self)\n                    .on_conflict(attachments::id)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving attachment\")\n            }\n        }\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            crate::util::retry(||\n                diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))\n                .execute(conn),\n                10,\n            )\n            .map(|_| ())\n            .map_res(\"Error deleting attachment\")\n        }}?;\n\n        let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;\n        let file_path = self.get_file_path();\n\n        if let Err(e) = operator.delete(&file_path).await {\n            if e.kind() == opendal::ErrorKind::NotFound {\n                debug!(\"File '{file_path}' already deleted.\");\n            } else {\n                return Err(e.into());\n            }\n        }\n\n        Ok(())\n    }\n\n    pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {\n        for attachment in Attachment::find_by_cipher(cipher_uuid, conn).await {\n            attachment.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn find_by_id(id: &AttachmentId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            attachments::table\n                .filter(attachments::id.eq(id.to_lowercase()))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            attachments::table\n                .filter(attachments::cipher_uuid.eq(cipher_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading attachments\")\n        }}\n    }\n\n    pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            let result: Option<BigDecimal> = attachments::table\n                .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))\n                .filter(ciphers::user_uuid.eq(user_uuid))\n                .select(diesel::dsl::sum(attachments::file_size))\n                .first(conn)\n                .expect(\"Error loading user attachment total size\");\n\n            match result.map(|r| r.to_i64()) {\n                Some(Some(r)) => r,\n                Some(None) => i64::MAX,\n                None => 0\n            }\n        }}\n    }\n\n    pub async fn count_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            attachments::table\n                .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))\n                .filter(ciphers::user_uuid.eq(user_uuid))\n                .count()\n                .first(conn)\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn size_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            let result: Option<BigDecimal> = attachments::table\n                .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))\n                .filter(ciphers::organization_uuid.eq(org_uuid))\n                .select(diesel::dsl::sum(attachments::file_size))\n                .first(conn)\n                .expect(\"Error loading user attachment total size\");\n\n            match result.map(|r| r.to_i64()) {\n                Some(Some(r)) => r,\n                Some(None) => i64::MAX,\n                None => 0\n            }\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            attachments::table\n                .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))\n                .filter(ciphers::organization_uuid.eq(org_uuid))\n                .count()\n                .first(conn)\n                .unwrap_or(0)\n        }}\n    }\n\n    // This will return all attachments linked to the user or org\n    // There is no filtering done here if the user actually has access!\n    // It is used to speed up the sync process, and the matching is done in a different part.\n    pub async fn find_all_by_user_and_orgs(\n        user_uuid: &UserId,\n        org_uuids: &Vec<OrganizationId>,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            attachments::table\n                .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))\n                .filter(ciphers::user_uuid.eq(user_uuid))\n                .or_filter(ciphers::organization_uuid.eq_any(org_uuids))\n                .select(attachments::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading attachments\")\n        }}\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    IdFromParam,\n)]\npub struct AttachmentId(pub String);\n"
  },
  {
    "path": "src/db/models/auth_request.rs",
    "content": "use super::{DeviceId, OrganizationId, UserId};\nuse crate::db::schema::auth_requests;\nuse crate::{crypto::ct_eq, util::format_date};\nuse chrono::{NaiveDateTime, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse diesel::prelude::*;\nuse macros::UuidFromParam;\nuse serde_json::Value;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]\n#[diesel(table_name = auth_requests)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct AuthRequest {\n    pub uuid: AuthRequestId,\n    pub user_uuid: UserId,\n    pub organization_uuid: Option<OrganizationId>,\n\n    pub request_device_identifier: DeviceId,\n    pub device_type: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs\n\n    pub request_ip: String,\n    pub response_device_id: Option<DeviceId>,\n\n    pub access_code: String,\n    pub public_key: String,\n\n    pub enc_key: Option<String>,\n\n    pub master_password_hash: Option<String>,\n    pub approved: Option<bool>,\n    pub creation_date: NaiveDateTime,\n    pub response_date: Option<NaiveDateTime>,\n\n    pub authentication_date: Option<NaiveDateTime>,\n}\n\nimpl AuthRequest {\n    pub fn new(\n        user_uuid: UserId,\n        request_device_identifier: DeviceId,\n        device_type: i32,\n        request_ip: String,\n        access_code: String,\n        public_key: String,\n    ) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid: AuthRequestId(crate::util::get_uuid()),\n            user_uuid,\n            organization_uuid: None,\n\n            request_device_identifier,\n            device_type,\n            request_ip,\n            response_device_id: None,\n            access_code,\n            public_key,\n            enc_key: None,\n            master_password_hash: None,\n            approved: None,\n            creation_date: now,\n            response_date: None,\n            authentication_date: None,\n        }\n    }\n\n    pub fn to_json_for_pending_device(&self) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"creationDate\": format_date(&self.creation_date),\n        })\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\nimpl AuthRequest {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(auth_requests::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(auth_requests::table)\n                            .filter(auth_requests::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error auth_request\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error auth_request\")\n            }\n            postgresql {\n                diesel::insert_into(auth_requests::table)\n                    .values(&*self)\n                    .on_conflict(auth_requests::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving auth_request\")\n            }\n        }\n    }\n\n    pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            auth_requests::table\n                .filter(auth_requests::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            auth_requests::table\n                .filter(auth_requests::uuid.eq(uuid))\n                .filter(auth_requests::user_uuid.eq(user_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            auth_requests::table\n                .filter(auth_requests::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading auth_requests\")\n        }}\n    }\n\n    pub async fn find_by_user_and_requested_device(\n        user_uuid: &UserId,\n        device_uuid: &DeviceId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            auth_requests::table\n                .filter(auth_requests::user_uuid.eq(user_uuid))\n                .filter(auth_requests::request_device_identifier.eq(device_uuid))\n                .filter(auth_requests::approved.is_null())\n                .order_by(auth_requests::creation_date.desc())\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_created_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            auth_requests::table\n                .filter(auth_requests::creation_date.lt(dt))\n                .load::<Self>(conn)\n                .expect(\"Error loading auth_requests\")\n        }}\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(auth_requests::table.filter(auth_requests::uuid.eq(&self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting auth request\")\n        }}\n    }\n\n    pub fn check_access_code(&self, access_code: &str) -> bool {\n        ct_eq(&self.access_code, access_code)\n    }\n\n    pub async fn purge_expired_auth_requests(conn: &DbConn) {\n        // delete auth requests older than 15 minutes which is functionally equivalent to upstream:\n        // https://github.com/bitwarden/server/blob/f8ee2270409f7a13125cd414c450740af605a175/src/Sql/dbo/Auth/Stored%20Procedures/AuthRequest_DeleteIfExpired.sql\n        let expiry_time = Utc::now().naive_utc() - chrono::TimeDelta::try_minutes(15).unwrap();\n        for auth_request in Self::find_created_before(&expiry_time, conn).await {\n            auth_request.delete(conn).await.ok();\n        }\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct AuthRequestId(String);\n"
  },
  {
    "path": "src/db/models/cipher.rs",
    "content": "use crate::db::schema::{\n    ciphers, ciphers_collections, collections, collections_groups, folders, folders_ciphers, groups, groups_users,\n    users_collections, users_organizations,\n};\nuse crate::util::LowerCase;\nuse crate::CONFIG;\nuse chrono::{NaiveDateTime, TimeDelta, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse diesel::prelude::*;\nuse serde_json::Value;\n\nuse super::{\n    Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,\n    MembershipType, OrganizationId, User, UserId,\n};\nuse crate::api::core::{CipherData, CipherSyncData, CipherSyncType};\nuse macros::UuidFromParam;\n\nuse std::borrow::Cow;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = ciphers)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Cipher {\n    pub uuid: CipherId,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n\n    pub user_uuid: Option<UserId>,\n    pub organization_uuid: Option<OrganizationId>,\n\n    pub key: Option<String>,\n\n    /*\n    Login = 1,\n    SecureNote = 2,\n    Card = 3,\n    Identity = 4,\n    SshKey = 5\n    */\n    pub atype: i32,\n    pub name: String,\n    pub notes: Option<String>,\n    pub fields: Option<String>,\n\n    pub data: String,\n\n    pub password_history: Option<String>,\n    pub deleted_at: Option<NaiveDateTime>,\n    pub reprompt: Option<i32>,\n}\n\npub enum RepromptType {\n    None = 0,\n    Password = 1,\n}\n\n/// Local methods\nimpl Cipher {\n    pub fn new(atype: i32, name: String) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid: CipherId(crate::util::get_uuid()),\n            created_at: now,\n            updated_at: now,\n\n            user_uuid: None,\n            organization_uuid: None,\n\n            key: None,\n\n            atype,\n            name,\n\n            notes: None,\n            fields: None,\n\n            data: String::new(),\n            password_history: None,\n            deleted_at: None,\n            reprompt: None,\n        }\n    }\n\n    pub fn validate_cipher_data(cipher_data: &[CipherData]) -> EmptyResult {\n        let mut validation_errors = serde_json::Map::new();\n        let max_note_size = CONFIG._max_note_size();\n        let max_note_size_msg =\n            format!(\"The field Notes exceeds the maximum encrypted value length of {max_note_size} characters.\");\n        for (index, cipher) in cipher_data.iter().enumerate() {\n            // Validate the note size and if it is exceeded return a warning\n            if let Some(note) = &cipher.notes {\n                if note.len() > max_note_size {\n                    validation_errors\n                        .insert(format!(\"Ciphers[{index}].Notes\"), serde_json::to_value([&max_note_size_msg]).unwrap());\n                }\n            }\n\n            // Validate the password history if it contains `null` values and if so, return a warning\n            if let Some(Value::Array(password_history)) = &cipher.password_history {\n                for pwh in password_history {\n                    if let Value::Object(pwo) = pwh {\n                        if pwo.get(\"password\").is_some_and(|p| !p.is_string()) {\n                            validation_errors.insert(\n                                format!(\"Ciphers[{index}].Notes\"),\n                                serde_json::to_value([\n                                    \"The password history contains a `null` value. Only strings are allowed.\",\n                                ])\n                                .unwrap(),\n                            );\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n\n        if !validation_errors.is_empty() {\n            let err_json = json!({\n                \"message\": \"The model state is invalid.\",\n                \"validationErrors\" : validation_errors,\n                \"object\": \"error\"\n            });\n            err_json!(err_json, \"Import validation errors\")\n        } else {\n            Ok(())\n        }\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Cipher {\n    pub async fn to_json(\n        &self,\n        host: &str,\n        user_uuid: &UserId,\n        cipher_sync_data: Option<&CipherSyncData>,\n        sync_type: CipherSyncType,\n        conn: &DbConn,\n    ) -> Result<Value, crate::Error> {\n        use crate::util::{format_date, validate_and_format_date};\n\n        let mut attachments_json: Value = Value::Null;\n        if let Some(cipher_sync_data) = cipher_sync_data {\n            if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) {\n                if !attachments.is_empty() {\n                    let mut attachments_json_vec = vec![];\n                    for attachment in attachments {\n                        attachments_json_vec.push(attachment.to_json(host).await?);\n                    }\n                    attachments_json = Value::Array(attachments_json_vec);\n                }\n            }\n        } else {\n            let attachments = Attachment::find_by_cipher(&self.uuid, conn).await;\n            if !attachments.is_empty() {\n                let mut attachments_json_vec = vec![];\n                for attachment in attachments {\n                    attachments_json_vec.push(attachment.to_json(host).await?);\n                }\n                attachments_json = Value::Array(attachments_json_vec);\n            }\n        }\n\n        // We don't need these values at all for Organizational syncs\n        // Skip any other database calls if this is the case and just return false.\n        let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User {\n            match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {\n                Some((ro, hp, mn)) => (ro, hp, mn),\n                None => {\n                    error!(\"Cipher ownership assertion failure\");\n                    (true, true, false)\n                }\n            }\n        } else {\n            (false, false, false)\n        };\n\n        let fields_json: Vec<_> = self\n            .fields\n            .as_ref()\n            .and_then(|s| {\n                serde_json::from_str::<Vec<LowerCase<Value>>>(s)\n                    .inspect_err(|e| warn!(\"Error parsing fields {e:?} for {}\", self.uuid))\n                    .ok()\n            })\n            .map(|d| {\n                d.into_iter()\n                    .map(|mut f| {\n                        // Check if the `type` key is a number, strings break some clients\n                        // The fallback type is the hidden type `1`. this should prevent accidental data disclosure\n                        // If not try to convert the string value to a number and fallback to `1`\n                        // If it is both not a number and not a string, fallback to `1`\n                        match f.data.get(\"type\") {\n                            Some(t) if t.is_number() => {}\n                            Some(t) if t.is_string() => {\n                                let type_num = &t.as_str().unwrap_or(\"1\").parse::<u8>().unwrap_or(1);\n                                f.data[\"type\"] = json!(type_num);\n                            }\n                            _ => {\n                                f.data[\"type\"] = json!(1);\n                            }\n                        }\n                        f.data\n                    })\n                    .collect()\n            })\n            .unwrap_or_default();\n\n        let password_history_json: Vec<_> = self\n            .password_history\n            .as_ref()\n            .and_then(|s| {\n                serde_json::from_str::<Vec<LowerCase<Value>>>(s)\n                    .inspect_err(|e| warn!(\"Error parsing password history {e:?} for {}\", self.uuid))\n                    .ok()\n            })\n            .map(|d| {\n                // Check every password history item if they are valid and return it.\n                // If a password field has the type `null` skip it, it breaks newer Bitwarden clients\n                // A second check is done to verify the lastUsedDate exists and is a valid DateTime string, if not the epoch start time will be used\n                d.into_iter()\n                    .filter_map(|d| match d.data.get(\"password\") {\n                        Some(p) if p.is_string() => Some(d.data),\n                        _ => None,\n                    })\n                    .map(|mut d| match d.get(\"lastUsedDate\").and_then(|l| l.as_str()) {\n                        Some(l) => {\n                            d[\"lastUsedDate\"] = json!(validate_and_format_date(l));\n                            d\n                        }\n                        _ => {\n                            d[\"lastUsedDate\"] = json!(\"1970-01-01T00:00:00.000000Z\");\n                            d\n                        }\n                    })\n                    .collect()\n            })\n            .unwrap_or_default();\n\n        // Get the type_data or a default to an empty json object '{}'.\n        // If not passing an empty object, mobile clients will crash.\n        let mut type_data_json =\n            serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_else(|_| {\n                warn!(\"Error parsing data field for {}\", self.uuid);\n                Value::Object(serde_json::Map::new())\n            });\n\n        // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream\n        // Set the first element of the Uris array as Uri, this is needed several (mobile) clients.\n        if self.atype == 1 {\n            // Upstream always has an `uri` key/value\n            type_data_json[\"uri\"] = Value::Null;\n            if let Some(uris) = type_data_json[\"uris\"].as_array_mut() {\n                if !uris.is_empty() {\n                    // Fix uri match values first, they are only allowed to be a number or null\n                    // If it is a string, convert it to an int or null if that fails\n                    for uri in &mut *uris {\n                        if uri[\"match\"].is_string() {\n                            let match_value = match uri[\"match\"].as_str().unwrap_or_default().parse::<u8>() {\n                                Ok(n) => json!(n),\n                                _ => Value::Null,\n                            };\n                            uri[\"match\"] = match_value;\n                        }\n                    }\n                    type_data_json[\"uri\"] = uris[0][\"uri\"].clone();\n                }\n            }\n\n            // Check if `passwordRevisionDate` is a valid date, else convert it\n            if let Some(pw_revision) = type_data_json[\"passwordRevisionDate\"].as_str() {\n                type_data_json[\"passwordRevisionDate\"] = json!(validate_and_format_date(pw_revision));\n            }\n        }\n\n        // Fix secure note issues when data is invalid\n        // This breaks at least the native mobile clients\n        if self.atype == 2 {\n            match type_data_json {\n                Value::Object(ref t) if t.get(\"type\").is_some_and(|t| t.is_number()) => {}\n                _ => {\n                    type_data_json = json!({\"type\": 0});\n                }\n            }\n        }\n\n        // Fix invalid SSH Entries\n        // This breaks at least the native mobile client if invalid\n        // The only way to fix this is by setting type_data_json to `null`\n        // Opening this ssh-key in the mobile client will probably crash the client, but you can edit, save and afterwards delete it\n        if self.atype == 5\n            && (type_data_json[\"keyFingerprint\"].as_str().is_none_or(|v| v.is_empty())\n                || type_data_json[\"privateKey\"].as_str().is_none_or(|v| v.is_empty())\n                || type_data_json[\"publicKey\"].as_str().is_none_or(|v| v.is_empty()))\n        {\n            warn!(\"Error parsing ssh-key, mandatory fields are invalid for {}\", self.uuid);\n            type_data_json = Value::Null;\n        }\n\n        // Clone the type_data and add some default value.\n        let mut data_json = type_data_json.clone();\n\n        // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream\n        // data_json should always contain the following keys with every atype\n        data_json[\"fields\"] = json!(fields_json);\n        data_json[\"name\"] = json!(self.name);\n        data_json[\"notes\"] = json!(self.notes);\n        data_json[\"passwordHistory\"] = Value::Array(password_history_json.clone());\n\n        let collection_ids = if let Some(cipher_sync_data) = cipher_sync_data {\n            if let Some(cipher_collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {\n                Cow::from(cipher_collections)\n            } else {\n                Cow::from(Vec::with_capacity(0))\n            }\n        } else {\n            Cow::from(self.get_admin_collections(user_uuid.clone(), conn).await)\n        };\n\n        // There are three types of cipher response models in upstream\n        // Bitwarden: \"cipherMini\", \"cipher\", and \"cipherDetails\" (in order\n        // of increasing level of detail). vaultwarden currently only\n        // supports the \"cipherDetails\" type, though it seems like the\n        // Bitwarden clients will ignore extra fields.\n        //\n        // Ref: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Vault/Models/Response/CipherResponseModel.cs#L14\n        let mut json_object = json!({\n            \"object\": \"cipherDetails\",\n            \"id\": self.uuid,\n            \"type\": self.atype,\n            \"creationDate\": format_date(&self.created_at),\n            \"revisionDate\": format_date(&self.updated_at),\n            \"deletedDate\": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),\n            \"reprompt\": self.reprompt.filter(|r| *r == RepromptType::None as i32 || *r == RepromptType::Password as i32).unwrap_or(RepromptType::None as i32),\n            \"organizationId\": self.organization_uuid,\n            \"key\": self.key,\n            \"attachments\": attachments_json,\n            // We have UseTotp set to true by default within the Organization model.\n            // This variable together with UsersGetPremium is used to show or hide the TOTP counter.\n            \"organizationUseTotp\": true,\n\n            // This field is specific to the cipherDetails type.\n            \"collectionIds\": collection_ids,\n\n            \"name\": self.name,\n            \"notes\": self.notes,\n            \"fields\": fields_json,\n\n            \"data\": data_json,\n\n            \"passwordHistory\": password_history_json,\n\n            // All Cipher types are included by default as null, but only the matching one will be populated\n            \"login\": null,\n            \"secureNote\": null,\n            \"card\": null,\n            \"identity\": null,\n            \"sshKey\": null,\n        });\n\n        // These values are only needed for user/default syncs\n        // Not during an organizational sync like `get_org_details`\n        // Skip adding these fields in that case\n        if sync_type == CipherSyncType::User {\n            json_object[\"folderId\"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {\n                cipher_sync_data.cipher_folders.get(&self.uuid).cloned()\n            } else {\n                self.get_folder_uuid(user_uuid, conn).await\n            });\n            json_object[\"favorite\"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {\n                cipher_sync_data.cipher_favorites.contains(&self.uuid)\n            } else {\n                self.is_favorite(user_uuid, conn).await\n            });\n            // These values are true by default, but can be false if the\n            // cipher belongs to a collection or group where the org owner has enabled\n            // the \"Read Only\" or \"Hide Passwords\" restrictions for the user.\n            json_object[\"edit\"] = json!(!read_only);\n            json_object[\"viewPassword\"] = json!(!hide_passwords);\n            // The new key used by clients since v2025.6.0\n            json_object[\"permissions\"] = json!({\n                \"delete\": !read_only,\n                \"restore\": !read_only,\n            });\n        }\n\n        let key = match self.atype {\n            1 => \"login\",\n            2 => \"secureNote\",\n            3 => \"card\",\n            4 => \"identity\",\n            5 => \"sshKey\",\n            _ => panic!(\"Wrong type\"),\n        };\n\n        json_object[key] = type_data_json;\n        Ok(json_object)\n    }\n\n    pub async fn update_users_revision(&self, conn: &DbConn) -> Vec<UserId> {\n        let mut user_uuids = Vec::new();\n        match self.user_uuid {\n            Some(ref user_uuid) => {\n                User::update_uuid_revision(user_uuid, conn).await;\n                user_uuids.push(user_uuid.clone())\n            }\n            None => {\n                // Belongs to Organization, need to update affected users\n                if let Some(ref org_uuid) = self.organization_uuid {\n                    // users having access to the collection\n                    let mut collection_users = Membership::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;\n                    if CONFIG.org_groups_enabled() {\n                        // members of a group having access to the collection\n                        let group_users =\n                            Membership::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;\n                        collection_users.extend(group_users);\n                    }\n                    for member in collection_users {\n                        User::update_uuid_revision(&member.user_uuid, conn).await;\n                        user_uuids.push(member.user_uuid.clone())\n                    }\n                }\n            }\n        };\n        user_uuids\n    }\n\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n        self.updated_at = Utc::now().naive_utc();\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(ciphers::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(ciphers::table)\n                            .filter(ciphers::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error saving cipher\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving cipher\")\n            }\n            postgresql {\n                diesel::insert_into(ciphers::table)\n                    .values(&*self)\n                    .on_conflict(ciphers::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving cipher\")\n            }\n        }\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n\n        FolderCipher::delete_all_by_cipher(&self.uuid, conn).await?;\n        CollectionCipher::delete_all_by_cipher(&self.uuid, conn).await?;\n        Attachment::delete_all_by_cipher(&self.uuid, conn).await?;\n        Favorite::delete_all_by_cipher(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting cipher\")\n        }}\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        // TODO: Optimize this by executing a DELETE directly on the database, instead of first fetching.\n        for cipher in Self::find_by_org(org_uuid, conn).await {\n            cipher.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        for cipher in Self::find_owned_by_user(user_uuid, conn).await {\n            cipher.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    /// Purge all ciphers that are old enough to be auto-deleted.\n    pub async fn purge_trash(conn: &DbConn) {\n        if let Some(auto_delete_days) = CONFIG.trash_auto_delete_days() {\n            let now = Utc::now().naive_utc();\n            let dt = now - TimeDelta::try_days(auto_delete_days).unwrap();\n            for cipher in Self::find_deleted_before(&dt, conn).await {\n                cipher.delete(conn).await.ok();\n            }\n        }\n    }\n\n    pub async fn move_to_folder(\n        &self,\n        folder_uuid: Option<FolderId>,\n        user_uuid: &UserId,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        User::update_uuid_revision(user_uuid, conn).await;\n\n        match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) {\n            // No changes\n            (None, None) => Ok(()),\n            (Some(ref old_folder), Some(ref new_folder)) if old_folder == new_folder => Ok(()),\n\n            // Add to folder\n            (None, Some(new_folder)) => FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await,\n\n            // Remove from folder\n            (Some(old_folder), None) => {\n                match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {\n                    Some(old_folder) => old_folder.delete(conn).await,\n                    None => err!(\"Couldn't move from previous folder\"),\n                }\n            }\n\n            // Move to another folder\n            (Some(old_folder), Some(new_folder)) => {\n                if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {\n                    old_folder.delete(conn).await?;\n                }\n                FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await\n            }\n        }\n    }\n\n    /// Returns whether this cipher is directly owned by the user.\n    pub fn is_owned_by_user(&self, user_uuid: &UserId) -> bool {\n        self.user_uuid.is_some() && self.user_uuid.as_ref().unwrap() == user_uuid\n    }\n\n    /// Returns whether this cipher is owned by an org in which the user has full access.\n    async fn is_in_full_access_org(\n        &self,\n        user_uuid: &UserId,\n        cipher_sync_data: Option<&CipherSyncData>,\n        conn: &DbConn,\n    ) -> bool {\n        if let Some(ref org_uuid) = self.organization_uuid {\n            if let Some(cipher_sync_data) = cipher_sync_data {\n                if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {\n                    return cached_member.has_full_access();\n                }\n            } else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await {\n                return member.has_full_access();\n            }\n        }\n        false\n    }\n\n    /// Returns whether this cipher is owned by an group in which the user has full access.\n    async fn is_in_full_access_group(\n        &self,\n        user_uuid: &UserId,\n        cipher_sync_data: Option<&CipherSyncData>,\n        conn: &DbConn,\n    ) -> bool {\n        if !CONFIG.org_groups_enabled() {\n            return false;\n        }\n        if let Some(ref org_uuid) = self.organization_uuid {\n            if let Some(cipher_sync_data) = cipher_sync_data {\n                return cipher_sync_data.user_group_full_access_for_organizations.contains(org_uuid);\n            } else {\n                return Group::is_in_full_access_group(user_uuid, org_uuid, conn).await;\n            }\n        }\n        false\n    }\n\n    /// Returns the user's access restrictions to this cipher. A return value\n    /// of None means that this cipher does not belong to the user, and is\n    /// not in any collection the user has access to. Otherwise, the user has\n    /// access to this cipher, and Some(read_only, hide_passwords, manage) represents\n    /// the access restrictions.\n    pub async fn get_access_restrictions(\n        &self,\n        user_uuid: &UserId,\n        cipher_sync_data: Option<&CipherSyncData>,\n        conn: &DbConn,\n    ) -> Option<(bool, bool, bool)> {\n        // Check whether this cipher is directly owned by the user, or is in\n        // a collection that the user has full access to. If so, there are no\n        // access restrictions.\n        if self.is_owned_by_user(user_uuid)\n            || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await\n            || self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await\n        {\n            return Some((false, false, true));\n        }\n\n        let rows = if let Some(cipher_sync_data) = cipher_sync_data {\n            let mut rows: Vec<(bool, bool, bool)> = Vec::new();\n            if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {\n                for collection in collections {\n                    // User permissions\n                    if let Some(cu) = cipher_sync_data.user_collections.get(collection) {\n                        rows.push((cu.read_only, cu.hide_passwords, cu.manage));\n                    // Group permissions\n                    } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {\n                        rows.push((cg.read_only, cg.hide_passwords, cg.manage));\n                    }\n                }\n            }\n            rows\n        } else {\n            let user_permissions = self.get_user_collections_access_flags(user_uuid, conn).await;\n            if !user_permissions.is_empty() {\n                user_permissions\n            } else {\n                self.get_group_collections_access_flags(user_uuid, conn).await\n            }\n        };\n\n        if rows.is_empty() {\n            // This cipher isn't in any collections accessible to the user.\n            return None;\n        }\n\n        // A cipher can be in multiple collections with inconsistent access flags.\n        // Also, user permission overrule group permissions\n        // and only user permissions are returned by the code above.\n        //\n        // For example, a cipher could be in one collection where the user has\n        // read-only access, but also in another collection where the user has\n        // read/write access. For a flag to be in effect for a cipher, upstream\n        // requires all collections the cipher is in to have that flag set.\n        // Therefore, we do a boolean AND of all values in each of the `read_only`\n        // and `hide_passwords` columns. This could ideally be done as part of the\n        // query, but Diesel doesn't support a min() or bool_and() function on\n        // booleans and this behavior isn't portable anyway.\n        //\n        // The only exception is for the `manage` flag, that needs a boolean OR!\n        let mut read_only = true;\n        let mut hide_passwords = true;\n        let mut manage = false;\n        for (ro, hp, mn) in rows.iter() {\n            read_only &= ro;\n            hide_passwords &= hp;\n            manage |= mn;\n        }\n\n        Some((read_only, hide_passwords, manage))\n    }\n\n    async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> {\n        db_run! { conn: {\n            // Check whether this cipher is in any collections accessible to the\n            // user. If so, retrieve the access flags for each collection.\n            ciphers::table\n                .filter(ciphers::uuid.eq(&self.uuid))\n                .inner_join(ciphers_collections::table.on(\n                    ciphers::uuid.eq(ciphers_collections::cipher_uuid)))\n                .inner_join(users_collections::table.on(\n                    ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid))))\n                .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))\n                .load::<(bool, bool, bool)>(conn)\n                .expect(\"Error getting user access restrictions\")\n        }}\n    }\n\n    async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &DbConn) -> Vec<(bool, bool, bool)> {\n        if !CONFIG.org_groups_enabled() {\n            return Vec::new();\n        }\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::uuid.eq(&self.uuid))\n                .inner_join(ciphers_collections::table.on(\n                    ciphers::uuid.eq(ciphers_collections::cipher_uuid)\n                ))\n                .inner_join(collections_groups::table.on(\n                    collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)\n                ))\n                .inner_join(groups_users::table.on(\n                    groups_users::groups_uuid.eq(collections_groups::groups_uuid)\n                ))\n                .inner_join(users_organizations::table.on(\n                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)\n                ))\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))\n                .load::<(bool, bool, bool)>(conn)\n                .expect(\"Error getting group access restrictions\")\n        }}\n    }\n\n    pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        match self.get_access_restrictions(user_uuid, None, conn).await {\n            Some((read_only, _hide_passwords, manage)) => !read_only || manage,\n            None => false,\n        }\n    }\n\n    // used for checking if collection can be edited (only if user has access to a collection they\n    // can write to and also passwords are not hidden to prevent privilege escalation)\n    pub async fn is_in_editable_collection_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        match self.get_access_restrictions(user_uuid, None, conn).await {\n            Some((read_only, hide_passwords, manage)) => (!read_only && !hide_passwords) || manage,\n            None => false,\n        }\n    }\n\n    pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        self.get_access_restrictions(user_uuid, None, conn).await.is_some()\n    }\n\n    // Returns whether this cipher is a favorite of the specified user.\n    pub async fn is_favorite(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        Favorite::is_favorite(&self.uuid, user_uuid, conn).await\n    }\n\n    // Sets whether this cipher is a favorite of the specified user.\n    pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        match favorite {\n            None => Ok(()), // No change requested.\n            Some(status) => Favorite::set_favorite(status, &self.uuid, user_uuid, conn).await,\n        }\n    }\n\n    pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {\n        db_run! { conn: {\n            folders_ciphers::table\n                .inner_join(folders::table)\n                .filter(folders::user_uuid.eq(&user_uuid))\n                .filter(folders_ciphers::cipher_uuid.eq(&self.uuid))\n                .select(folders_ciphers::folder_uuid)\n                .first::<FolderId>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid(uuid: &CipherId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_org(\n        cipher_uuid: &CipherId,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::uuid.eq(cipher_uuid))\n                .filter(ciphers::organization_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    // Find all ciphers accessible or visible to the specified user.\n    //\n    // \"Accessible\" means the user has read access to the cipher, either via\n    // direct ownership, collection or via group access.\n    //\n    // \"Visible\" usually means the same as accessible, except when an org\n    // owner/admin sets their account or group to have access to only selected\n    // collections in the org (presumably because they aren't interested in\n    // the other collections in the org). In this case, if `visible_only` is\n    // true, then the non-interesting ciphers will not be returned. As a\n    // result, those ciphers will not appear in \"My Vault\" for the org\n    // owner/admin, but they can still be accessed via the org vault view.\n    pub async fn find_by_user(\n        user_uuid: &UserId,\n        visible_only: bool,\n        cipher_uuids: &Vec<CipherId>,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                let mut query = ciphers::table\n                    .left_join(ciphers_collections::table.on(\n                            ciphers::uuid.eq(ciphers_collections::cipher_uuid)\n                            ))\n                    .left_join(users_organizations::table.on(\n                            ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())\n                            .and(users_organizations::user_uuid.eq(user_uuid))\n                            .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                            ))\n                    .left_join(users_collections::table.on(\n                            ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)\n                            // Ensure that users_collections::user_uuid is NULL for unconfirmed users.\n                            .and(users_organizations::user_uuid.eq(users_collections::user_uuid))\n                            ))\n                    .left_join(groups_users::table.on(\n                            groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                            ))\n                    .left_join(groups::table.on(\n                            groups::uuid.eq(groups_users::groups_uuid)\n                            ))\n                    .left_join(collections_groups::table.on(\n                            collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(\n                                collections_groups::groups_uuid.eq(groups::uuid)\n                                )\n                            ))\n                    .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner\n                    .or_filter(users_organizations::access_all.eq(true)) // access_all in org\n                    .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection\n                    .or_filter(groups::access_all.eq(true)) // Access via groups\n                    .or_filter(collections_groups::collections_uuid.is_not_null()) // Access via groups\n                    .into_boxed();\n\n                if !visible_only {\n                    query = query.or_filter(\n                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner\n                    );\n                }\n\n                // Only filter for one specific cipher\n                if !cipher_uuids.is_empty() {\n                    query = query.filter(\n                        ciphers::uuid.eq_any(cipher_uuids)\n                    );\n                }\n\n                query\n                    .select(ciphers::all_columns)\n                    .distinct()\n                    .load::<Self>(conn)\n                    .expect(\"Error loading ciphers\")\n            }}\n        } else {\n            db_run! { conn: {\n                let mut query = ciphers::table\n                    .left_join(ciphers_collections::table.on(\n                            ciphers::uuid.eq(ciphers_collections::cipher_uuid)\n                            ))\n                    .left_join(users_organizations::table.on(\n                            ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())\n                            .and(users_organizations::user_uuid.eq(user_uuid))\n                            .and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                            ))\n                    .left_join(users_collections::table.on(\n                            ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)\n                            // Ensure that users_collections::user_uuid is NULL for unconfirmed users.\n                            .and(users_organizations::user_uuid.eq(users_collections::user_uuid))\n                            ))\n                    .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner\n                    .or_filter(users_organizations::access_all.eq(true)) // access_all in org\n                    .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection\n                    .into_boxed();\n\n                if !visible_only {\n                    query = query.or_filter(\n                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner\n                    );\n                }\n\n                // Only filter for one specific cipher\n                if !cipher_uuids.is_empty() {\n                    query = query.filter(\n                        ciphers::uuid.eq_any(cipher_uuids)\n                    );\n                }\n\n                query\n                    .select(ciphers::all_columns)\n                    .distinct()\n                    .load::<Self>(conn)\n                    .expect(\"Error loading ciphers\")\n            }}\n        }\n    }\n\n    // Find all ciphers visible to the specified user.\n    pub async fn find_by_user_visible(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        Self::find_by_user(user_uuid, true, &vec![], conn).await\n    }\n\n    pub async fn find_by_user_and_ciphers(\n        user_uuid: &UserId,\n        cipher_uuids: &Vec<CipherId>,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        Self::find_by_user(user_uuid, true, cipher_uuids, conn).await\n    }\n\n    pub async fn find_by_user_and_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> Option<Self> {\n        Self::find_by_user(user_uuid, true, &vec![cipher_uuid.clone()], conn).await.pop()\n    }\n\n    // Find all ciphers directly owned by the specified user.\n    pub async fn find_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            ciphers::table\n                .filter(\n                    ciphers::user_uuid.eq(user_uuid)\n                    .and(ciphers::organization_uuid.is_null())\n                )\n                .load::<Self>(conn)\n                .expect(\"Error loading ciphers\")\n        }}\n    }\n\n    pub async fn count_owned_by_user(user_uuid: &UserId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::user_uuid.eq(user_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::organization_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading ciphers\")\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::organization_uuid.eq(org_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            folders_ciphers::table.inner_join(ciphers::table)\n                .filter(folders_ciphers::folder_uuid.eq(folder_uuid))\n                .select(ciphers::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading ciphers\")\n        }}\n    }\n\n    /// Find all ciphers that were deleted before the specified datetime.\n    pub async fn find_deleted_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            ciphers::table\n                .filter(ciphers::deleted_at.lt(dt))\n                .load::<Self>(conn)\n                .expect(\"Error loading ciphers\")\n        }}\n    }\n\n    pub async fn get_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec<CollectionId> {\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                ciphers_collections::table\n                    .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))\n                    .inner_join(collections::table.on(\n                        collections::uuid.eq(ciphers_collections::collection_uuid)\n                    ))\n                    .left_join(users_organizations::table.on(\n                        users_organizations::org_uuid.eq(collections::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(groups_users::table.on(\n                        groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                    ))\n                    .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))\n                    .left_join(collections_groups::table.on(\n                        collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(collections_groups::groups_uuid.eq(groups::uuid))\n                    ))\n                    .filter(users_organizations::access_all.eq(true) // User has access all\n                        .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection\n                            .and(users_collections::read_only.eq(false)))\n                        .or(groups::access_all.eq(true)) // Access via groups\n                        .or(collections_groups::collections_uuid.is_not_null() // Access via groups\n                            .and(collections_groups::read_only.eq(false)))\n                    )\n                    .select(ciphers_collections::collection_uuid)\n                    .load::<CollectionId>(conn)\n                    .unwrap_or_default()\n            }}\n        } else {\n            db_run! { conn: {\n                ciphers_collections::table\n                    .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))\n                    .inner_join(collections::table.on(\n                        collections::uuid.eq(ciphers_collections::collection_uuid)\n                    ))\n                    .inner_join(users_organizations::table.on(\n                        users_organizations::org_uuid.eq(collections::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .filter(users_organizations::access_all.eq(true) // User has access all\n                        .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection\n                            .and(users_collections::read_only.eq(false)))\n                    )\n                    .select(ciphers_collections::collection_uuid)\n                    .load::<CollectionId>(conn)\n                    .unwrap_or_default()\n            }}\n        }\n    }\n\n    pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &DbConn) -> Vec<CollectionId> {\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                ciphers_collections::table\n                    .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))\n                    .inner_join(collections::table.on(\n                        collections::uuid.eq(ciphers_collections::collection_uuid)\n                    ))\n                    .left_join(users_organizations::table.on(\n                        users_organizations::org_uuid.eq(collections::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(groups_users::table.on(\n                        groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                    ))\n                    .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))\n                    .left_join(collections_groups::table.on(\n                        collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(collections_groups::groups_uuid.eq(groups::uuid))\n                    ))\n                    .filter(users_organizations::access_all.eq(true) // User has access all\n                        .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection\n                            .and(users_collections::read_only.eq(false)))\n                        .or(groups::access_all.eq(true)) // Access via groups\n                        .or(collections_groups::collections_uuid.is_not_null() // Access via groups\n                            .and(collections_groups::read_only.eq(false)))\n                        .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner\n                    )\n                    .select(ciphers_collections::collection_uuid)\n                    .load::<CollectionId>(conn)\n                    .unwrap_or_default()\n            }}\n        } else {\n            db_run! { conn: {\n                ciphers_collections::table\n                    .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))\n                    .inner_join(collections::table.on(\n                        collections::uuid.eq(ciphers_collections::collection_uuid)\n                    ))\n                    .inner_join(users_organizations::table.on(\n                        users_organizations::org_uuid.eq(collections::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .filter(users_organizations::access_all.eq(true) // User has access all\n                        .or(users_collections::user_uuid.eq(user_uuid) // User has access to collection\n                            .and(users_collections::read_only.eq(false)))\n                        .or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner\n                    )\n                    .select(ciphers_collections::collection_uuid)\n                    .load::<CollectionId>(conn)\n                    .unwrap_or_default()\n            }}\n        }\n    }\n\n    /// Return a Vec with (cipher_uuid, collection_uuid)\n    /// This is used during a full sync so we only need one query for all collections accessible.\n    pub async fn get_collections_with_cipher_by_user(\n        user_uuid: UserId,\n        conn: &DbConn,\n    ) -> Vec<(CipherId, CollectionId)> {\n        db_run! { conn: {\n            ciphers_collections::table\n            .inner_join(collections::table.on(\n                collections::uuid.eq(ciphers_collections::collection_uuid)\n            ))\n            .inner_join(users_organizations::table.on(\n                users_organizations::org_uuid.eq(collections::org_uuid).and(\n                    users_organizations::user_uuid.eq(user_uuid.clone())\n                )\n            ))\n            .left_join(users_collections::table.on(\n                users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(\n                    users_collections::user_uuid.eq(user_uuid.clone())\n                )\n            ))\n            .left_join(groups_users::table.on(\n                groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n            ))\n            .left_join(groups::table.on(\n                groups::uuid.eq(groups_users::groups_uuid)\n            ))\n            .left_join(collections_groups::table.on(\n                collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(\n                    collections_groups::groups_uuid.eq(groups::uuid)\n                )\n            ))\n            .or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection\n            .or_filter(users_organizations::access_all.eq(true)) // User has access all\n            .or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner\n            .or_filter(groups::access_all.eq(true)) //Access via group\n            .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group\n            .select(ciphers_collections::all_columns)\n            .distinct()\n            .load::<(CipherId, CollectionId)>(conn)\n            .unwrap_or_default()\n        }}\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct CipherId(String);\n"
  },
  {
    "path": "src/db/models/collection.rs",
    "content": "use derive_more::{AsRef, Deref, Display, From};\nuse serde_json::Value;\n\nuse super::{\n    CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId,\n    User, UserId,\n};\nuse crate::db::schema::{\n    ciphers_collections, collections, collections_groups, groups, groups_users, users_collections, users_organizations,\n};\nuse crate::CONFIG;\nuse diesel::prelude::*;\nuse macros::UuidFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = collections)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Collection {\n    pub uuid: CollectionId,\n    pub org_uuid: OrganizationId,\n    pub name: String,\n    pub external_id: Option<String>,\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = users_collections)]\n#[diesel(primary_key(user_uuid, collection_uuid))]\npub struct CollectionUser {\n    pub user_uuid: UserId,\n    pub collection_uuid: CollectionId,\n    pub read_only: bool,\n    pub hide_passwords: bool,\n    pub manage: bool,\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = ciphers_collections)]\n#[diesel(primary_key(cipher_uuid, collection_uuid))]\npub struct CollectionCipher {\n    pub cipher_uuid: CipherId,\n    pub collection_uuid: CollectionId,\n}\n\n/// Local methods\nimpl Collection {\n    pub fn new(org_uuid: OrganizationId, name: String, external_id: Option<String>) -> Self {\n        let mut new_model = Self {\n            uuid: CollectionId(crate::util::get_uuid()),\n            org_uuid,\n            name,\n            external_id: None,\n        };\n\n        new_model.set_external_id(external_id);\n        new_model\n    }\n\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"externalId\": self.external_id,\n            \"id\": self.uuid,\n            \"organizationId\": self.org_uuid,\n            \"name\": self.name,\n            \"object\": \"collection\",\n        })\n    }\n\n    pub fn set_external_id(&mut self, external_id: Option<String>) {\n        //Check if external id is empty. We don't want to have\n        //empty strings in the database\n        match external_id {\n            Some(external_id) => {\n                if external_id.is_empty() {\n                    self.external_id = None;\n                } else {\n                    self.external_id = Some(external_id)\n                }\n            }\n            None => self.external_id = None,\n        }\n    }\n\n    pub async fn to_json_details(\n        &self,\n        user_uuid: &UserId,\n        cipher_sync_data: Option<&crate::api::core::CipherSyncData>,\n        conn: &DbConn,\n    ) -> Value {\n        let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data {\n            match cipher_sync_data.members.get(&self.org_uuid) {\n                // Only for Manager types Bitwarden returns true for the manage option\n                // Owners and Admins always have true. Users are not able to have full access\n                Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),\n                Some(m) => {\n                    // Only let a manager manage collections when the have full read/write access\n                    let is_manager = m.atype == MembershipType::Manager;\n                    if let Some(cu) = cipher_sync_data.user_collections.get(&self.uuid) {\n                        (\n                            cu.read_only,\n                            cu.hide_passwords,\n                            is_manager && (cu.manage || (!cu.read_only && !cu.hide_passwords)),\n                        )\n                    } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {\n                        (\n                            cg.read_only,\n                            cg.hide_passwords,\n                            is_manager && (cg.manage || (!cg.read_only && !cg.hide_passwords)),\n                        )\n                    } else {\n                        (false, false, false)\n                    }\n                }\n                _ => (true, true, false),\n            }\n        } else {\n            match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {\n                Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),\n                Some(m) if m.atype == MembershipType::Manager && self.is_manageable_by_user(user_uuid, conn).await => {\n                    (false, false, true)\n                }\n                Some(m) => {\n                    let is_manager = m.atype == MembershipType::Manager;\n                    let read_only = !self.is_writable_by_user(user_uuid, conn).await;\n                    let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;\n                    (read_only, hide_passwords, is_manager && !read_only && !hide_passwords)\n                }\n                _ => (true, true, false),\n            }\n        };\n\n        let mut json_object = self.to_json();\n        json_object[\"object\"] = json!(\"collectionDetails\");\n        json_object[\"readOnly\"] = json!(read_only);\n        json_object[\"hidePasswords\"] = json!(hide_passwords);\n        json_object[\"manage\"] = json!(manage);\n        json_object\n    }\n\n    pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &DbConn) -> bool {\n        member.has_status(MembershipStatus::Confirmed)\n            && (member.has_full_access()\n                || CollectionUser::has_access_to_collection_by_user(col_id, &member.user_uuid, conn).await\n                || (CONFIG.org_groups_enabled()\n                    && (GroupUser::has_full_access_by_member(&member.org_uuid, &member.uuid, conn).await\n                        || GroupUser::has_access_to_collection_by_member(col_id, &member.uuid, conn).await)))\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Collection {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(collections::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(collections::table)\n                            .filter(collections::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving collection\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving collection\")\n            }\n            postgresql {\n                diesel::insert_into(collections::table)\n                    .values(self)\n                    .on_conflict(collections::uuid)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving collection\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n        CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;\n        CollectionUser::delete_all_by_collection(&self.uuid, conn).await?;\n        CollectionGroup::delete_all_by_collection(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting collection\")\n        }}\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        for collection in Self::find_by_organization(org_uuid, conn).await {\n            collection.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn update_users_revision(&self, conn: &DbConn) {\n        for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {\n            User::update_uuid_revision(&member.user_uuid, conn).await;\n        }\n    }\n\n    pub async fn find_by_uuid(uuid: &CollectionId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            collections::table\n                .filter(collections::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user_uuid(user_uuid: UserId, conn: &DbConn) -> Vec<Self> {\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                collections::table\n                .left_join(users_collections::table.on(\n                    users_collections::collection_uuid.eq(collections::uuid).and(\n                        users_collections::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .left_join(users_organizations::table.on(\n                    collections::org_uuid.eq(users_organizations::org_uuid).and(\n                        users_organizations::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .left_join(groups_users::table.on(\n                    groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                ))\n                .left_join(groups::table.on(\n                    groups::uuid.eq(groups_users::groups_uuid)\n                ))\n                .left_join(collections_groups::table.on(\n                    collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(\n                        collections_groups::collections_uuid.eq(collections::uuid)\n                    )\n                ))\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .filter(\n                    users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection\n                        users_organizations::access_all.eq(true) // access_all in Organization\n                    ).or(\n                        groups::access_all.eq(true) // access_all in groups\n                    ).or( // access via groups\n                        groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(\n                            collections_groups::collections_uuid.is_not_null()\n                        )\n                    )\n                )\n                .select(collections::all_columns)\n                .distinct()\n                .load::<Self>(conn)\n                .expect(\"Error loading collections\")\n            }}\n        } else {\n            db_run! { conn: {\n                collections::table\n                .left_join(users_collections::table.on(\n                    users_collections::collection_uuid.eq(collections::uuid).and(\n                        users_collections::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .left_join(users_organizations::table.on(\n                    collections::org_uuid.eq(users_organizations::org_uuid).and(\n                        users_organizations::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .filter(\n                    users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection\n                        users_organizations::access_all.eq(true) // access_all in Organization\n                    )\n                )\n                .select(collections::all_columns)\n                .distinct()\n                .load::<Self>(conn)\n                .expect(\"Error loading collections\")\n            }}\n        }\n    }\n\n    pub async fn find_by_organization_and_user_uuid(\n        org_uuid: &OrganizationId,\n        user_uuid: &UserId,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        Self::find_by_user_uuid(user_uuid.to_owned(), conn)\n            .await\n            .into_iter()\n            .filter(|c| &c.org_uuid == org_uuid)\n            .collect()\n    }\n\n    pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            collections::table\n                .filter(collections::org_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading collections\")\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            collections::table\n                .filter(collections::org_uuid.eq(org_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_uuid_and_org(uuid: &CollectionId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            collections::table\n                .filter(collections::uuid.eq(uuid))\n                .filter(collections::org_uuid.eq(org_uuid))\n                .select(collections::all_columns)\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &DbConn) -> Option<Self> {\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                collections::table\n                .left_join(users_collections::table.on(\n                    users_collections::collection_uuid.eq(collections::uuid).and(\n                        users_collections::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .left_join(users_organizations::table.on(\n                    collections::org_uuid.eq(users_organizations::org_uuid).and(\n                        users_organizations::user_uuid.eq(user_uuid)\n                    )\n                ))\n                .left_join(groups_users::table.on(\n                    groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                ))\n                .left_join(groups::table.on(\n                    groups::uuid.eq(groups_users::groups_uuid)\n                ))\n                .left_join(collections_groups::table.on(\n                    collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(\n                        collections_groups::collections_uuid.eq(collections::uuid)\n                    )\n                ))\n                .filter(collections::uuid.eq(uuid))\n                .filter(\n                    users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection\n                        users_organizations::access_all.eq(true).or( // access_all in Organization\n                            users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                    )).or(\n                        groups::access_all.eq(true) // access_all in groups\n                    ).or( // access via groups\n                        groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(\n                            collections_groups::collections_uuid.is_not_null()\n                        )\n                    )\n                ).select(collections::all_columns)\n                .first::<Self>(conn)\n                .ok()\n            }}\n        } else {\n            db_run! { conn: {\n                collections::table\n                .left_join(users_collections::table.on(\n                    users_collections::collection_uuid.eq(collections::uuid).and(\n                        users_collections::user_uuid.eq(user_uuid.clone())\n                    )\n                ))\n                .left_join(users_organizations::table.on(\n                    collections::org_uuid.eq(users_organizations::org_uuid).and(\n                        users_organizations::user_uuid.eq(user_uuid)\n                    )\n                ))\n                .filter(collections::uuid.eq(uuid))\n                .filter(\n                    users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection\n                        users_organizations::access_all.eq(true).or( // access_all in Organization\n                            users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                    ))\n                ).select(collections::all_columns)\n                .first::<Self>(conn)\n                .ok()\n            }}\n        }\n    }\n\n    pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        let user_uuid = user_uuid.to_string();\n        if CONFIG.org_groups_enabled() {\n            db_run! { conn: {\n                collections::table\n                    .filter(collections::uuid.eq(&self.uuid))\n                    .inner_join(users_organizations::table.on(\n                        collections::org_uuid.eq(users_organizations::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(collections::uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid))\n                    ))\n                    .left_join(groups_users::table.on(\n                        groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n                    ))\n                    .left_join(groups::table.on(\n                        groups::uuid.eq(groups_users::groups_uuid)\n                    ))\n                    .left_join(collections_groups::table.on(\n                        collections_groups::groups_uuid.eq(groups_users::groups_uuid)\n                        .and(collections_groups::collections_uuid.eq(collections::uuid))\n                    ))\n                    .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                        .or(users_organizations::access_all.eq(true)) // access_all via membership\n                        .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection\n                            .and(users_collections::read_only.eq(false)))\n                        .or(groups::access_all.eq(true)) // access_all via group\n                        .or(collections_groups::collections_uuid.is_not_null() // write access given via group\n                            .and(collections_groups::read_only.eq(false)))\n                    )\n                    .count()\n                    .first::<i64>(conn)\n                    .ok()\n                    .unwrap_or(0) != 0\n            }}\n        } else {\n            db_run! { conn: {\n                collections::table\n                    .filter(collections::uuid.eq(&self.uuid))\n                    .inner_join(users_organizations::table.on(\n                        collections::org_uuid.eq(users_organizations::org_uuid)\n                        .and(users_organizations::user_uuid.eq(user_uuid.clone()))\n                    ))\n                    .left_join(users_collections::table.on(\n                        users_collections::collection_uuid.eq(collections::uuid)\n                        .and(users_collections::user_uuid.eq(user_uuid))\n                    ))\n                    .filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                        .or(users_organizations::access_all.eq(true)) // access_all via membership\n                        .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection\n                            .and(users_collections::read_only.eq(false)))\n                    )\n                    .count()\n                    .first::<i64>(conn)\n                    .ok()\n                    .unwrap_or(0) != 0\n            }}\n        }\n    }\n\n    pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        let user_uuid = user_uuid.to_string();\n        db_run! { conn: {\n            collections::table\n            .left_join(users_collections::table.on(\n                users_collections::collection_uuid.eq(collections::uuid).and(\n                    users_collections::user_uuid.eq(user_uuid.clone())\n                )\n            ))\n            .left_join(users_organizations::table.on(\n                collections::org_uuid.eq(users_organizations::org_uuid).and(\n                    users_organizations::user_uuid.eq(user_uuid)\n                )\n            ))\n            .left_join(groups_users::table.on(\n                groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n            ))\n            .left_join(groups::table.on(\n                groups::uuid.eq(groups_users::groups_uuid)\n            ))\n            .left_join(collections_groups::table.on(\n                collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(\n                    collections_groups::collections_uuid.eq(collections::uuid)\n                )\n            ))\n            .filter(collections::uuid.eq(&self.uuid))\n            .filter(\n                users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection\n                    users_organizations::access_all.eq(true).or( // access_all in Organization\n                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                )).or(\n                    groups::access_all.eq(true) // access_all in groups\n                ).or( // access via groups\n                    groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(\n                        collections_groups::collections_uuid.is_not_null().and(\n                            collections_groups::hide_passwords.eq(true))\n                    )\n                )\n            )\n            .count()\n            .first::<i64>(conn)\n            .ok()\n            .unwrap_or(0) != 0\n        }}\n    }\n\n    pub async fn is_coll_manageable_by_user(uuid: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool {\n        let uuid = uuid.to_string();\n        let user_uuid = user_uuid.to_string();\n        db_run! { conn: {\n            collections::table\n            .left_join(users_collections::table.on(\n                users_collections::collection_uuid.eq(collections::uuid).and(\n                    users_collections::user_uuid.eq(user_uuid.clone())\n                )\n            ))\n            .left_join(users_organizations::table.on(\n                collections::org_uuid.eq(users_organizations::org_uuid).and(\n                    users_organizations::user_uuid.eq(user_uuid)\n                )\n            ))\n            .left_join(groups_users::table.on(\n                groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n            ))\n            .left_join(groups::table.on(\n                groups::uuid.eq(groups_users::groups_uuid)\n            ))\n            .left_join(collections_groups::table.on(\n                collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(\n                    collections_groups::collections_uuid.eq(collections::uuid)\n                )\n            ))\n            .filter(collections::uuid.eq(&uuid))\n            .filter(\n                users_collections::collection_uuid.eq(&uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection\n                    users_organizations::access_all.eq(true).or( // access_all in Organization\n                        users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner\n                )).or(\n                    groups::access_all.eq(true) // access_all in groups\n                ).or( // access via groups\n                    groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(\n                        collections_groups::collections_uuid.is_not_null().and(\n                            collections_groups::manage.eq(true))\n                    )\n                )\n            )\n            .count()\n            .first::<i64>(conn)\n            .ok()\n            .unwrap_or(0) != 0\n        }}\n    }\n\n    pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool {\n        Self::is_coll_manageable_by_user(&self.uuid, user_uuid, conn).await\n    }\n}\n\n/// Database methods\nimpl CollectionUser {\n    pub async fn find_by_organization_and_user_uuid(\n        org_uuid: &OrganizationId,\n        user_uuid: &UserId,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            users_collections::table\n                .filter(users_collections::user_uuid.eq(user_uuid))\n                .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))\n                .filter(collections::org_uuid.eq(org_uuid))\n                .select(users_collections::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading users_collections\")\n        }}\n    }\n\n    pub async fn find_by_organization_swap_user_uuid_with_member_uuid(\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Vec<CollectionMembership> {\n        let col_users = db_run! { conn: {\n            users_collections::table\n                .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))\n                .filter(collections::org_uuid.eq(org_uuid))\n                .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))\n                .load::<Self>(conn)\n                .expect(\"Error loading users_collections\")\n        }};\n        col_users.into_iter().map(|c| c.into()).collect()\n    }\n\n    pub async fn save(\n        user_uuid: &UserId,\n        collection_uuid: &CollectionId,\n        read_only: bool,\n        hide_passwords: bool,\n        manage: bool,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        User::update_uuid_revision(user_uuid, conn).await;\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(users_collections::table)\n                    .values((\n                        users_collections::user_uuid.eq(user_uuid),\n                        users_collections::collection_uuid.eq(collection_uuid),\n                        users_collections::read_only.eq(read_only),\n                        users_collections::hide_passwords.eq(hide_passwords),\n                        users_collections::manage.eq(manage),\n                    ))\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(users_collections::table)\n                            .filter(users_collections::user_uuid.eq(user_uuid))\n                            .filter(users_collections::collection_uuid.eq(collection_uuid))\n                            .set((\n                                users_collections::user_uuid.eq(user_uuid),\n                                users_collections::collection_uuid.eq(collection_uuid),\n                                users_collections::read_only.eq(read_only),\n                                users_collections::hide_passwords.eq(hide_passwords),\n                                users_collections::manage.eq(manage),\n                            ))\n                            .execute(conn)\n                            .map_res(\"Error adding user to collection\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error adding user to collection\")\n            }\n            postgresql {\n                diesel::insert_into(users_collections::table)\n                    .values((\n                        users_collections::user_uuid.eq(user_uuid),\n                        users_collections::collection_uuid.eq(collection_uuid),\n                        users_collections::read_only.eq(read_only),\n                        users_collections::hide_passwords.eq(hide_passwords),\n                        users_collections::manage.eq(manage),\n                    ))\n                    .on_conflict((users_collections::user_uuid, users_collections::collection_uuid))\n                    .do_update()\n                    .set((\n                        users_collections::read_only.eq(read_only),\n                        users_collections::hide_passwords.eq(hide_passwords),\n                        users_collections::manage.eq(manage),\n                    ))\n                    .execute(conn)\n                    .map_res(\"Error adding user to collection\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.user_uuid, conn).await;\n\n        db_run! { conn: {\n            diesel::delete(\n                users_collections::table\n                    .filter(users_collections::user_uuid.eq(&self.user_uuid))\n                    .filter(users_collections::collection_uuid.eq(&self.collection_uuid)),\n            )\n            .execute(conn)\n            .map_res(\"Error removing user from collection\")\n        }}\n    }\n\n    pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_collections::table\n                .filter(users_collections::collection_uuid.eq(collection_uuid))\n                .select(users_collections::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading users_collections\")\n        }}\n    }\n\n    pub async fn find_by_org_and_coll_swap_user_uuid_with_member_uuid(\n        org_uuid: &OrganizationId,\n        collection_uuid: &CollectionId,\n        conn: &DbConn,\n    ) -> Vec<CollectionMembership> {\n        let col_users = db_run! { conn: {\n            users_collections::table\n                .filter(users_collections::collection_uuid.eq(collection_uuid))\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))\n                .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))\n                .load::<Self>(conn)\n                .expect(\"Error loading users_collections\")\n        }};\n        col_users.into_iter().map(|c| c.into()).collect()\n    }\n\n    pub async fn find_by_collection_and_user(\n        collection_uuid: &CollectionId,\n        user_uuid: &UserId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            users_collections::table\n                .filter(users_collections::collection_uuid.eq(collection_uuid))\n                .filter(users_collections::user_uuid.eq(user_uuid))\n                .select(users_collections::all_columns)\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_collections::table\n                .filter(users_collections::user_uuid.eq(user_uuid))\n                .select(users_collections::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading users_collections\")\n        }}\n    }\n\n    pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {\n        for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() {\n            User::update_uuid_revision(&collection.user_uuid, conn).await;\n        }\n\n        db_run! { conn: {\n            diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting users from collection\")\n        }}\n    }\n\n    pub async fn delete_all_by_user_and_org(\n        user_uuid: &UserId,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await;\n\n        db_run! { conn: {\n            for user in collectionusers {\n                let _: () = diesel::delete(users_collections::table.filter(\n                    users_collections::user_uuid.eq(user_uuid)\n                    .and(users_collections::collection_uuid.eq(user.collection_uuid))\n                ))\n                    .execute(conn)\n                    .map_res(\"Error removing user from collections\")?;\n            }\n            Ok(())\n        }}\n    }\n\n    pub async fn has_access_to_collection_by_user(col_id: &CollectionId, user_uuid: &UserId, conn: &DbConn) -> bool {\n        Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some()\n    }\n}\n\n/// Database methods\nimpl CollectionCipher {\n    pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {\n        Self::update_users_revision(collection_uuid, conn).await;\n\n        db_run! { conn:\n            sqlite, mysql {\n                // Not checking for ForeignKey Constraints here.\n                // Table ciphers_collections does not have ForeignKey Constraints which would cause conflicts.\n                // This table has no constraints pointing to itself, but only to others.\n                diesel::replace_into(ciphers_collections::table)\n                    .values((\n                        ciphers_collections::cipher_uuid.eq(cipher_uuid),\n                        ciphers_collections::collection_uuid.eq(collection_uuid),\n                    ))\n                    .execute(conn)\n                    .map_res(\"Error adding cipher to collection\")\n            }\n            postgresql {\n                diesel::insert_into(ciphers_collections::table)\n                    .values((\n                        ciphers_collections::cipher_uuid.eq(cipher_uuid),\n                        ciphers_collections::collection_uuid.eq(collection_uuid),\n                    ))\n                    .on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))\n                    .do_nothing()\n                    .execute(conn)\n                    .map_res(\"Error adding cipher to collection\")\n            }\n        }\n    }\n\n    pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {\n        Self::update_users_revision(collection_uuid, conn).await;\n\n        db_run! { conn: {\n            diesel::delete(\n                ciphers_collections::table\n                    .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))\n                    .filter(ciphers_collections::collection_uuid.eq(collection_uuid)),\n            )\n            .execute(conn)\n            .map_res(\"Error deleting cipher from collection\")\n        }}\n    }\n\n    pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing cipher from collections\")\n        }}\n    }\n\n    pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing ciphers from collection\")\n        }}\n    }\n\n    pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &DbConn) {\n        if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn).await {\n            collection.update_users_revision(conn).await;\n        }\n    }\n}\n\n// Added in case we need the membership_uuid instead of the user_uuid\npub struct CollectionMembership {\n    pub membership_uuid: MembershipId,\n    pub collection_uuid: CollectionId,\n    pub read_only: bool,\n    pub hide_passwords: bool,\n    pub manage: bool,\n}\n\nimpl CollectionMembership {\n    pub fn to_json_details_for_member(&self, membership_type: i32) -> Value {\n        json!({\n            \"id\": self.membership_uuid,\n            \"readOnly\": self.read_only,\n            \"hidePasswords\": self.hide_passwords,\n            \"manage\": membership_type >= MembershipType::Admin\n                || self.manage\n                || (membership_type == MembershipType::Manager\n                    && !self.read_only\n                    && !self.hide_passwords),\n        })\n    }\n}\n\nimpl From<CollectionUser> for CollectionMembership {\n    fn from(c: CollectionUser) -> Self {\n        Self {\n            membership_uuid: c.user_uuid.to_string().into(),\n            collection_uuid: c.collection_uuid,\n            read_only: c.read_only,\n            hide_passwords: c.hide_passwords,\n            manage: c.manage,\n        }\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct CollectionId(String);\n"
  },
  {
    "path": "src/db/models/device.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\n\nuse data_encoding::{BASE64, BASE64URL};\nuse derive_more::{Display, From};\nuse serde_json::Value;\n\nuse super::{AuthRequest, UserId};\nuse crate::db::schema::devices;\nuse crate::{\n    crypto,\n    util::{format_date, get_uuid},\n};\nuse diesel::prelude::*;\nuse macros::{IdFromParam, UuidFromParam};\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = devices)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid, user_uuid))]\npub struct Device {\n    pub uuid: DeviceId,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n\n    pub user_uuid: UserId,\n\n    pub name: String,\n    pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs\n    pub push_uuid: Option<PushId>,\n    pub push_token: Option<String>,\n\n    pub refresh_token: String,\n    pub twofactor_remember: Option<String>,\n}\n\n/// Local methods\nimpl Device {\n    pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid,\n            created_at: now,\n            updated_at: now,\n\n            user_uuid,\n            name,\n            atype,\n\n            push_uuid: Some(PushId(get_uuid())),\n            push_token: None,\n            refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL),\n            twofactor_remember: None,\n        }\n    }\n\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"name\": self.name,\n            \"type\": self.atype,\n            \"identifier\": self.uuid,\n            \"creationDate\": format_date(&self.created_at),\n            \"isTrusted\": false,\n            \"object\":\"device\"\n        })\n    }\n\n    pub fn refresh_twofactor_remember(&mut self) -> String {\n        let twofactor_remember = crypto::encode_random_bytes::<180>(&BASE64);\n        self.twofactor_remember = Some(twofactor_remember.clone());\n\n        twofactor_remember\n    }\n\n    pub fn delete_twofactor_remember(&mut self) {\n        self.twofactor_remember = None;\n    }\n\n    // This rely on the fact we only update the device after a successful login\n    pub fn is_new(&self) -> bool {\n        self.created_at == self.updated_at\n    }\n\n    pub fn is_push_device(&self) -> bool {\n        matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)\n    }\n\n    pub fn is_cli(&self) -> bool {\n        matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)\n    }\n\n    pub fn is_mobile(&self) -> bool {\n        matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)\n    }\n}\n\npub struct DeviceWithAuthRequest {\n    pub device: Device,\n    pub pending_auth_request: Option<AuthRequest>,\n}\n\nimpl DeviceWithAuthRequest {\n    pub fn to_json(&self) -> Value {\n        let auth_request = match &self.pending_auth_request {\n            Some(auth_request) => auth_request.to_json_for_pending_device(),\n            None => Value::Null,\n        };\n        json!({\n            \"id\": self.device.uuid,\n            \"name\": self.device.name,\n            \"type\": self.device.atype,\n            \"identifier\": self.device.uuid,\n            \"creationDate\": format_date(&self.device.created_at),\n            \"devicePendingAuthRequest\": auth_request,\n            \"isTrusted\": false,\n            \"encryptedPublicKey\": null,\n            \"encryptedUserKey\": null,\n            \"object\": \"device\",\n        })\n    }\n\n    pub fn from(c: Device, a: Option<AuthRequest>) -> Self {\n        Self {\n            device: c,\n            pending_auth_request: a,\n        }\n    }\n}\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Device {\n    pub async fn save(&mut self, update_time: bool, conn: &DbConn) -> EmptyResult {\n        if update_time {\n            self.updated_at = Utc::now().naive_utc();\n        }\n\n        db_run! { conn:\n            sqlite, mysql {\n                crate::util::retry(||\n                    diesel::replace_into(devices::table)\n                        .values(&*self)\n                        .execute(conn),\n                    10,\n                ).map_res(\"Error saving device\")\n            }\n            postgresql {\n                crate::util::retry(||\n                    diesel::insert_into(devices::table)\n                        .values(&*self)\n                        .on_conflict((devices::uuid, devices::user_uuid))\n                        .do_update()\n                        .set(&*self)\n                        .execute(conn),\n                    10,\n                ).map_res(\"Error saving device\")\n            }\n        }\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing devices for user\")\n        }}\n    }\n\n    pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::uuid.eq(uuid))\n                .filter(devices::user_uuid.eq(user_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<DeviceWithAuthRequest> {\n        let devices = Self::find_by_user(user_uuid, conn).await;\n        let mut result = Vec::new();\n        for device in devices {\n            let auth_request = AuthRequest::find_by_user_and_requested_device(user_uuid, &device.uuid, conn).await;\n            result.push(DeviceWithAuthRequest::from(device, auth_request));\n        }\n        result\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading devices\")\n        }}\n    }\n\n    pub async fn find_by_uuid(uuid: &DeviceId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::update(devices::table)\n                .filter(devices::uuid.eq(uuid))\n                .set(devices::push_token.eq::<Option<String>>(None))\n                .execute(conn)\n                .map_res(\"Error removing push token\")\n        }}\n    }\n    pub async fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::refresh_token.eq(refresh_token))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::user_uuid.eq(user_uuid))\n                .order(devices::updated_at.desc())\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            devices::table\n                .filter(devices::user_uuid.eq(user_uuid))\n                .filter(devices::push_token.is_not_null())\n                .load::<Self>(conn)\n                .expect(\"Error loading push devices\")\n        }}\n    }\n\n    pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &DbConn) -> bool {\n        db_run! { conn: {\n            devices::table\n            .filter(devices::user_uuid.eq(user_uuid))\n            .filter(devices::push_token.is_not_null())\n            .count()\n            .first::<i64>(conn)\n            .ok()\n            .unwrap_or(0) != 0\n        }}\n    }\n}\n\n#[derive(Display)]\npub enum DeviceType {\n    #[display(\"Android\")]\n    Android = 0,\n    #[display(\"iOS\")]\n    Ios = 1,\n    #[display(\"Chrome Extension\")]\n    ChromeExtension = 2,\n    #[display(\"Firefox Extension\")]\n    FirefoxExtension = 3,\n    #[display(\"Opera Extension\")]\n    OperaExtension = 4,\n    #[display(\"Edge Extension\")]\n    EdgeExtension = 5,\n    #[display(\"Windows\")]\n    WindowsDesktop = 6,\n    #[display(\"macOS\")]\n    MacOsDesktop = 7,\n    #[display(\"Linux\")]\n    LinuxDesktop = 8,\n    #[display(\"Chrome\")]\n    ChromeBrowser = 9,\n    #[display(\"Firefox\")]\n    FirefoxBrowser = 10,\n    #[display(\"Opera\")]\n    OperaBrowser = 11,\n    #[display(\"Edge\")]\n    EdgeBrowser = 12,\n    #[display(\"Internet Explorer\")]\n    IEBrowser = 13,\n    #[display(\"Unknown Browser\")]\n    UnknownBrowser = 14,\n    #[display(\"Android\")]\n    AndroidAmazon = 15,\n    #[display(\"UWP\")]\n    Uwp = 16,\n    #[display(\"Safari\")]\n    SafariBrowser = 17,\n    #[display(\"Vivaldi\")]\n    VivaldiBrowser = 18,\n    #[display(\"Vivaldi Extension\")]\n    VivaldiExtension = 19,\n    #[display(\"Safari Extension\")]\n    SafariExtension = 20,\n    #[display(\"SDK\")]\n    Sdk = 21,\n    #[display(\"Server\")]\n    Server = 22,\n    #[display(\"Windows CLI\")]\n    WindowsCLI = 23,\n    #[display(\"macOS CLI\")]\n    MacOsCLI = 24,\n    #[display(\"Linux CLI\")]\n    LinuxCLI = 25,\n}\n\nimpl DeviceType {\n    pub fn from_i32(value: i32) -> DeviceType {\n        match value {\n            0 => DeviceType::Android,\n            1 => DeviceType::Ios,\n            2 => DeviceType::ChromeExtension,\n            3 => DeviceType::FirefoxExtension,\n            4 => DeviceType::OperaExtension,\n            5 => DeviceType::EdgeExtension,\n            6 => DeviceType::WindowsDesktop,\n            7 => DeviceType::MacOsDesktop,\n            8 => DeviceType::LinuxDesktop,\n            9 => DeviceType::ChromeBrowser,\n            10 => DeviceType::FirefoxBrowser,\n            11 => DeviceType::OperaBrowser,\n            12 => DeviceType::EdgeBrowser,\n            13 => DeviceType::IEBrowser,\n            14 => DeviceType::UnknownBrowser,\n            15 => DeviceType::AndroidAmazon,\n            16 => DeviceType::Uwp,\n            17 => DeviceType::SafariBrowser,\n            18 => DeviceType::VivaldiBrowser,\n            19 => DeviceType::VivaldiExtension,\n            20 => DeviceType::SafariExtension,\n            21 => DeviceType::Sdk,\n            22 => DeviceType::Server,\n            23 => DeviceType::WindowsCLI,\n            24 => DeviceType::MacOsCLI,\n            25 => DeviceType::LinuxCLI,\n            _ => DeviceType::UnknownBrowser,\n        }\n    }\n}\n\n#[derive(\n    Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,\n)]\npub struct DeviceId(String);\n\n#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Serialize, Deserialize, UuidFromParam)]\npub struct PushId(pub String);\n"
  },
  {
    "path": "src/db/models/emergency_access.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse serde_json::Value;\n\nuse super::{User, UserId};\nuse crate::db::schema::emergency_access;\nuse crate::{api::EmptyResult, db::DbConn, error::MapResult};\nuse diesel::prelude::*;\nuse macros::UuidFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = emergency_access)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct EmergencyAccess {\n    pub uuid: EmergencyAccessId,\n    pub grantor_uuid: UserId,\n    pub grantee_uuid: Option<UserId>,\n    pub email: Option<String>,\n    pub key_encrypted: Option<String>,\n    pub atype: i32,  //EmergencyAccessType\n    pub status: i32, //EmergencyAccessStatus\n    pub wait_time_days: i32,\n    pub recovery_initiated_at: Option<NaiveDateTime>,\n    pub last_notification_at: Option<NaiveDateTime>,\n    pub updated_at: NaiveDateTime,\n    pub created_at: NaiveDateTime,\n}\n\n// Local methods\nimpl EmergencyAccess {\n    pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid: EmergencyAccessId(crate::util::get_uuid()),\n            grantor_uuid,\n            grantee_uuid: None,\n            email: Some(email),\n            status,\n            atype,\n            wait_time_days,\n            recovery_initiated_at: None,\n            created_at: now,\n            updated_at: now,\n            key_encrypted: None,\n            last_notification_at: None,\n        }\n    }\n\n    pub fn get_type_as_str(&self) -> &'static str {\n        if self.atype == EmergencyAccessType::View as i32 {\n            \"View\"\n        } else {\n            \"Takeover\"\n        }\n    }\n\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"status\": self.status,\n            \"type\": self.atype,\n            \"waitTimeDays\": self.wait_time_days,\n            \"object\": \"emergencyAccess\",\n        })\n    }\n\n    pub async fn to_json_grantor_details(&self, conn: &DbConn) -> Value {\n        let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).await.expect(\"Grantor user not found.\");\n\n        json!({\n            \"id\": self.uuid,\n            \"status\": self.status,\n            \"type\": self.atype,\n            \"waitTimeDays\": self.wait_time_days,\n            \"grantorId\": grantor_user.uuid,\n            \"email\": grantor_user.email,\n            \"name\": grantor_user.name,\n            \"avatarColor\": grantor_user.avatar_color,\n            \"object\": \"emergencyAccessGrantorDetails\",\n        })\n    }\n\n    pub async fn to_json_grantee_details(&self, conn: &DbConn) -> Option<Value> {\n        let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid {\n            User::find_by_uuid(grantee_uuid, conn).await.expect(\"Grantee user not found.\")\n        } else if let Some(email) = self.email.as_deref() {\n            match User::find_by_mail(email, conn).await {\n                Some(user) => user,\n                None => {\n                    // remove outstanding invitations which should not exist\n                    Self::delete_all_by_grantee_email(email, conn).await.ok();\n                    return None;\n                }\n            }\n        } else {\n            return None;\n        };\n\n        Some(json!({\n            \"id\": self.uuid,\n            \"status\": self.status,\n            \"type\": self.atype,\n            \"waitTimeDays\": self.wait_time_days,\n            \"granteeId\": grantee_user.uuid,\n            \"email\": grantee_user.email,\n            \"name\": grantee_user.name,\n            \"avatarColor\": grantee_user.avatar_color,\n            \"object\": \"emergencyAccessGranteeDetails\",\n        }))\n    }\n}\n\n#[derive(Copy, Clone)]\npub enum EmergencyAccessType {\n    View = 0,\n    Takeover = 1,\n}\n\nimpl EmergencyAccessType {\n    pub fn from_str(s: &str) -> Option<Self> {\n        match s {\n            \"0\" | \"View\" => Some(EmergencyAccessType::View),\n            \"1\" | \"Takeover\" => Some(EmergencyAccessType::Takeover),\n            _ => None,\n        }\n    }\n}\n\npub enum EmergencyAccessStatus {\n    Invited = 0,\n    Accepted = 1,\n    Confirmed = 2,\n    RecoveryInitiated = 3,\n    RecoveryApproved = 4,\n}\n\n// region Database methods\n\nimpl EmergencyAccess {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.grantor_uuid, conn).await;\n        self.updated_at = Utc::now().naive_utc();\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(emergency_access::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(emergency_access::table)\n                            .filter(emergency_access::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error updating emergency access\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving emergency access\")\n            }\n            postgresql {\n                diesel::insert_into(emergency_access::table)\n                    .values(&*self)\n                    .on_conflict(emergency_access::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving emergency access\")\n            }\n        }\n    }\n\n    pub async fn update_access_status_and_save(\n        &mut self,\n        status: i32,\n        date: &NaiveDateTime,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        // Update the grantee so that it will refresh it's status.\n        User::update_uuid_revision(self.grantee_uuid.as_ref().expect(\"Error getting grantee\"), conn).await;\n        self.status = status;\n        date.clone_into(&mut self.updated_at);\n\n        db_run! { conn: {\n            crate::util::retry(|| {\n                diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))\n                    .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date)))\n                    .execute(conn)\n            }, 10)\n            .map_res(\"Error updating emergency access status\")\n        }}\n    }\n\n    pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {\n        self.last_notification_at = Some(date.to_owned());\n        date.clone_into(&mut self.updated_at);\n\n        db_run! { conn: {\n            crate::util::retry(|| {\n                diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))\n                    .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date)))\n                    .execute(conn)\n            }, 10)\n            .map_res(\"Error updating emergency access status\")\n        }}\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await {\n            ea.delete(conn).await?;\n        }\n        for ea in Self::find_all_by_grantee_uuid(user_uuid, conn).await {\n            ea.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn delete_all_by_grantee_email(grantee_email: &str, conn: &DbConn) -> EmptyResult {\n        for ea in Self::find_all_invited_by_grantee_email(grantee_email, conn).await {\n            ea.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.grantor_uuid, conn).await;\n\n        db_run! { conn: {\n            diesel::delete(emergency_access::table.filter(emergency_access::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error removing user from emergency access\")\n        }}\n    }\n\n    pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email(\n        grantor_uuid: &UserId,\n        grantee_uuid: &UserId,\n        email: &str,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))\n                .filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email)))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_all_recoveries_initiated(conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32))\n                .filter(emergency_access::recovery_initiated_at.is_not_null())\n                .load::<Self>(conn)\n                .expect(\"Error loading emergency_access\")\n        }}\n    }\n\n    pub async fn find_by_uuid_and_grantor_uuid(\n        uuid: &EmergencyAccessId,\n        grantor_uuid: &UserId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::uuid.eq(uuid))\n                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_grantee_uuid(\n        uuid: &EmergencyAccessId,\n        grantee_uuid: &UserId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::uuid.eq(uuid))\n                .filter(emergency_access::grantee_uuid.eq(grantee_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_grantee_email(\n        uuid: &EmergencyAccessId,\n        grantee_email: &str,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::uuid.eq(uuid))\n                .filter(emergency_access::email.eq(grantee_email))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::grantee_uuid.eq(grantee_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading emergency_access\")\n        }}\n    }\n\n    pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::email.eq(grantee_email))\n                .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::email.eq(grantee_email))\n                .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))\n                .load::<Self>(conn)\n                .expect(\"Error loading emergency_access\")\n        }}\n    }\n\n    pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading emergency_access\")\n        }}\n    }\n\n    pub async fn find_all_confirmed_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            emergency_access::table\n                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))\n                .filter(emergency_access::status.ge(EmergencyAccessStatus::Confirmed as i32))\n                .load::<Self>(conn)\n                .expect(\"Error loading emergency_access\")\n        }}\n    }\n\n    pub async fn accept_invite(&mut self, grantee_uuid: &UserId, grantee_email: &str, conn: &DbConn) -> EmptyResult {\n        if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {\n            err!(\"User email does not match invite.\");\n        }\n\n        if self.status == EmergencyAccessStatus::Accepted as i32 {\n            err!(\"Emergency contact already accepted.\");\n        }\n\n        self.status = EmergencyAccessStatus::Accepted as i32;\n        self.grantee_uuid = Some(grantee_uuid.clone());\n        self.email = None;\n        self.save(conn).await\n    }\n}\n\n// endregion\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct EmergencyAccessId(String);\n"
  },
  {
    "path": "src/db/models/event.rs",
    "content": "use chrono::{NaiveDateTime, TimeDelta, Utc};\n//use derive_more::{AsRef, Deref, Display, From};\nuse serde_json::Value;\n\nuse super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId};\nuse crate::db::schema::{event, users_organizations};\nuse crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};\nuse diesel::prelude::*;\n\n// https://bitwarden.com/help/event-logs/\n\n// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs\n// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs\n// Upstream SQL: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Sql/dbo/Tables/Event.sql\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = event)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Event {\n    pub uuid: EventId,\n    pub event_type: i32, // EventType\n    pub user_uuid: Option<UserId>,\n    pub org_uuid: Option<OrganizationId>,\n    pub cipher_uuid: Option<CipherId>,\n    pub collection_uuid: Option<CollectionId>,\n    pub group_uuid: Option<GroupId>,\n    pub org_user_uuid: Option<MembershipId>,\n    pub act_user_uuid: Option<UserId>,\n    // Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs\n    pub device_type: Option<i32>,\n    pub ip_address: Option<String>,\n    pub event_date: NaiveDateTime,\n    pub policy_uuid: Option<OrgPolicyId>,\n    pub provider_uuid: Option<String>,\n    pub provider_user_uuid: Option<String>,\n    pub provider_org_uuid: Option<String>,\n}\n\n// Upstream enum: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/EventType.cs\n#[derive(Debug, Copy, Clone)]\npub enum EventType {\n    // User\n    UserLoggedIn = 1000,\n    UserChangedPassword = 1001,\n    UserUpdated2fa = 1002,\n    UserDisabled2fa = 1003,\n    UserRecovered2fa = 1004,\n    UserFailedLogIn = 1005,\n    UserFailedLogIn2fa = 1006,\n    UserClientExportedVault = 1007,\n    // UserUpdatedTempPassword = 1008, // Not supported\n    // UserMigratedKeyToKeyConnector = 1009, // Not supported\n    UserRequestedDeviceApproval = 1010,\n    // UserTdeOffboardingPasswordSet = 1011, // Not supported\n\n    // Cipher\n    CipherCreated = 1100,\n    CipherUpdated = 1101,\n    CipherDeleted = 1102,\n    CipherAttachmentCreated = 1103,\n    CipherAttachmentDeleted = 1104,\n    CipherShared = 1105,\n    CipherUpdatedCollections = 1106,\n    CipherClientViewed = 1107,\n    CipherClientToggledPasswordVisible = 1108,\n    CipherClientToggledHiddenFieldVisible = 1109,\n    CipherClientToggledCardCodeVisible = 1110,\n    CipherClientCopiedPassword = 1111,\n    CipherClientCopiedHiddenField = 1112,\n    CipherClientCopiedCardCode = 1113,\n    CipherClientAutofilled = 1114,\n    CipherSoftDeleted = 1115,\n    CipherRestored = 1116,\n    CipherClientToggledCardNumberVisible = 1117,\n\n    // Collection\n    CollectionCreated = 1300,\n    CollectionUpdated = 1301,\n    CollectionDeleted = 1302,\n\n    // Group\n    GroupCreated = 1400,\n    GroupUpdated = 1401,\n    GroupDeleted = 1402,\n\n    // OrganizationUser\n    OrganizationUserInvited = 1500,\n    OrganizationUserConfirmed = 1501,\n    OrganizationUserUpdated = 1502,\n    OrganizationUserRemoved = 1503, // Organization user data was deleted\n    OrganizationUserUpdatedGroups = 1504,\n    OrganizationUserUnlinkedSso = 1505,\n    OrganizationUserResetPasswordEnroll = 1506,\n    OrganizationUserResetPasswordWithdraw = 1507,\n    OrganizationUserAdminResetPassword = 1508,\n    // OrganizationUserResetSsoLink = 1509, // Not supported\n    // OrganizationUserFirstSsoLogin = 1510, // Not supported\n    OrganizationUserRevoked = 1511,\n    OrganizationUserRestored = 1512,\n    OrganizationUserApprovedAuthRequest = 1513,\n    OrganizationUserRejectedAuthRequest = 1514,\n    OrganizationUserDeleted = 1515, // Both user and organization user data were deleted\n    OrganizationUserLeft = 1516,    // User voluntarily left the organization\n\n    // Organization\n    OrganizationUpdated = 1600,\n    OrganizationPurgedVault = 1601,\n    OrganizationClientExportedVault = 1602,\n    // OrganizationVaultAccessed = 1603,\n    // OrganizationEnabledSso = 1604, // Not supported\n    // OrganizationDisabledSso = 1605, // Not supported\n    // OrganizationEnabledKeyConnector = 1606, // Not supported\n    // OrganizationDisabledKeyConnector = 1607, // Not supported\n    // OrganizationSponsorshipsSynced = 1608, // Not supported\n    // OrganizationCollectionManagementUpdated = 1609, // Not supported\n\n    // Policy\n    PolicyUpdated = 1700,\n    // Provider (Not yet supported)\n    // ProviderUserInvited = 1800, // Not supported\n    // ProviderUserConfirmed = 1801, // Not supported\n    // ProviderUserUpdated = 1802, // Not supported\n    // ProviderUserRemoved = 1803, // Not supported\n    // ProviderOrganizationCreated = 1900, // Not supported\n    // ProviderOrganizationAdded = 1901, // Not supported\n    // ProviderOrganizationRemoved = 1902, // Not supported\n    // ProviderOrganizationVaultAccessed = 1903, // Not supported\n\n    // OrganizationDomainAdded = 2000, // Not supported\n    // OrganizationDomainRemoved = 2001, // Not supported\n    // OrganizationDomainVerified = 2002, // Not supported\n    // OrganizationDomainNotVerified = 2003, // Not supported\n\n    // SecretRetrieved = 2100, // Not supported\n}\n\n/// Local methods\nimpl Event {\n    pub fn new(event_type: i32, event_date: Option<NaiveDateTime>) -> Self {\n        let event_date = match event_date {\n            Some(d) => d,\n            None => Utc::now().naive_utc(),\n        };\n\n        Self {\n            uuid: EventId(crate::util::get_uuid()),\n            event_type,\n            user_uuid: None,\n            org_uuid: None,\n            cipher_uuid: None,\n            collection_uuid: None,\n            group_uuid: None,\n            org_user_uuid: None,\n            act_user_uuid: None,\n            device_type: None,\n            ip_address: None,\n            event_date,\n            policy_uuid: None,\n            provider_uuid: None,\n            provider_user_uuid: None,\n            provider_org_uuid: None,\n        }\n    }\n\n    pub fn to_json(&self) -> Value {\n        use crate::util::format_date;\n\n        json!({\n            \"type\": self.event_type,\n            \"userId\": self.user_uuid,\n            \"organizationId\": self.org_uuid,\n            \"cipherId\": self.cipher_uuid,\n            \"collectionId\": self.collection_uuid,\n            \"groupId\": self.group_uuid,\n            \"organizationUserId\": self.org_user_uuid,\n            \"actingUserId\": self.act_user_uuid,\n            \"date\": format_date(&self.event_date),\n            \"deviceType\": self.device_type,\n            \"ipAddress\": self.ip_address,\n            \"policyId\": self.policy_uuid,\n            \"providerId\": self.provider_uuid,\n            \"providerUserId\": self.provider_user_uuid,\n            \"providerOrganizationId\": self.provider_org_uuid,\n            // \"installationId\": null, // Not supported\n        })\n    }\n}\n\n/// Database methods\n/// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Services/Implementations/EventService.cs\nimpl Event {\n    pub const PAGE_SIZE: i64 = 30;\n\n    /// #############\n    /// Basic Queries\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                diesel::replace_into(event::table)\n                .values(self)\n                .execute(conn)\n                .map_res(\"Error saving event\")\n            }\n            postgresql {\n                diesel::insert_into(event::table)\n                .values(self)\n                .on_conflict(event::uuid)\n                .do_update()\n                .set(self)\n                .execute(conn)\n                .map_res(\"Error saving event\")\n            }\n        }\n    }\n\n    pub async fn save_user_event(events: Vec<Event>, conn: &DbConn) -> EmptyResult {\n        // Special save function which is able to handle multiple events.\n        // SQLite doesn't support the DEFAULT argument, and does not support inserting multiple values at the same time.\n        // MySQL and PostgreSQL do.\n        // We also ignore duplicate if they ever will exists, else it could break the whole flow.\n        db_run! { conn:\n            // Unfortunately SQLite does not support inserting multiple records at the same time\n            // We loop through the events here and insert them one at a time.\n            sqlite {\n                for event in events {\n                    diesel::insert_or_ignore_into(event::table)\n                    .values(&event)\n                    .execute(conn)\n                    .unwrap_or_default();\n                }\n                Ok(())\n            }\n            mysql {\n                diesel::insert_or_ignore_into(event::table)\n                .values(&events)\n                .execute(conn)\n                .unwrap_or_default();\n                Ok(())\n            }\n            postgresql {\n                diesel::insert_into(event::table)\n                .values(&events)\n                .on_conflict_do_nothing()\n                .execute(conn)\n                .unwrap_or_default();\n                Ok(())\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(event::table.filter(event::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting event\")\n        }}\n    }\n\n    /// ##############\n    /// Custom Queries\n    pub async fn find_by_organization_uuid(\n        org_uuid: &OrganizationId,\n        start: &NaiveDateTime,\n        end: &NaiveDateTime,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            event::table\n                .filter(event::org_uuid.eq(org_uuid))\n                .filter(event::event_date.between(start, end))\n                .order_by(event::event_date.desc())\n                .limit(Self::PAGE_SIZE)\n                .load::<Self>(conn)\n                .expect(\"Error filtering events\")\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            event::table\n                .filter(event::org_uuid.eq(org_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_org_and_member(\n        org_uuid: &OrganizationId,\n        member_uuid: &MembershipId,\n        start: &NaiveDateTime,\n        end: &NaiveDateTime,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            event::table\n                .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))\n                .filter(event::org_uuid.eq(org_uuid))\n                .filter(event::event_date.between(start, end))\n                .filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))\n                .select(event::all_columns)\n                .order_by(event::event_date.desc())\n                .limit(Self::PAGE_SIZE)\n                .load::<Self>(conn)\n                .expect(\"Error filtering events\")\n        }}\n    }\n\n    pub async fn find_by_cipher_uuid(\n        cipher_uuid: &CipherId,\n        start: &NaiveDateTime,\n        end: &NaiveDateTime,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            event::table\n                .filter(event::cipher_uuid.eq(cipher_uuid))\n                .filter(event::event_date.between(start, end))\n                .order_by(event::event_date.desc())\n                .limit(Self::PAGE_SIZE)\n                .load::<Self>(conn)\n                .expect(\"Error filtering events\")\n        }}\n    }\n\n    pub async fn clean_events(conn: &DbConn) -> EmptyResult {\n        if let Some(days_to_retain) = CONFIG.events_days_retain() {\n            let dt = Utc::now().naive_utc() - TimeDelta::try_days(days_to_retain).unwrap();\n            db_run! { conn: {\n                diesel::delete(event::table.filter(event::event_date.lt(dt)))\n                .execute(conn)\n                .map_res(\"Error cleaning old events\")\n            }}\n        } else {\n            Ok(())\n        }\n    }\n}\n\n#[derive(Clone, Debug, DieselNewType, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]\npub struct EventId(String);\n"
  },
  {
    "path": "src/db/models/favorite.rs",
    "content": "use super::{CipherId, User, UserId};\nuse crate::db::schema::favorites;\nuse diesel::prelude::*;\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = favorites)]\n#[diesel(primary_key(user_uuid, cipher_uuid))]\npub struct Favorite {\n    pub user_uuid: UserId,\n    pub cipher_uuid: CipherId,\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\nimpl Favorite {\n    // Returns whether the specified cipher is a favorite of the specified user.\n    pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> bool {\n        db_run! { conn: {\n            let query = favorites::table\n                .filter(favorites::cipher_uuid.eq(cipher_uuid))\n                .filter(favorites::user_uuid.eq(user_uuid))\n                .count();\n\n            query.first::<i64>(conn)\n                .ok()\n                .unwrap_or(0) != 0\n        }}\n    }\n\n    // Sets whether the specified cipher is a favorite of the specified user.\n    pub async fn set_favorite(\n        favorite: bool,\n        cipher_uuid: &CipherId,\n        user_uuid: &UserId,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite);\n        match (old, new) {\n            (false, true) => {\n                User::update_uuid_revision(user_uuid, conn).await;\n                db_run! { conn: {\n                diesel::insert_into(favorites::table)\n                    .values((\n                        favorites::user_uuid.eq(user_uuid),\n                        favorites::cipher_uuid.eq(cipher_uuid),\n                    ))\n                    .execute(conn)\n                    .map_res(\"Error adding favorite\")\n                }}\n            }\n            (true, false) => {\n                User::update_uuid_revision(user_uuid, conn).await;\n                db_run! { conn: {\n                    diesel::delete(\n                        favorites::table\n                            .filter(favorites::user_uuid.eq(user_uuid))\n                            .filter(favorites::cipher_uuid.eq(cipher_uuid))\n                    )\n                    .execute(conn)\n                    .map_res(\"Error removing favorite\")\n                }}\n            }\n            // Otherwise, the favorite status is already what it should be.\n            _ => Ok(()),\n        }\n    }\n\n    // Delete all favorite entries associated with the specified cipher.\n    pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing favorites by cipher\")\n        }}\n    }\n\n    // Delete all favorite entries associated with the specified user.\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing favorites by user\")\n        }}\n    }\n\n    /// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers\n    /// This is used during a full sync so we only need one query for all favorite cipher matches.\n    pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<CipherId> {\n        db_run! { conn: {\n            favorites::table\n                .filter(favorites::user_uuid.eq(user_uuid))\n                .select(favorites::cipher_uuid)\n                .load::<CipherId>(conn)\n                .unwrap_or_default()\n        }}\n    }\n}\n"
  },
  {
    "path": "src/db/models/folder.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse serde_json::Value;\n\nuse super::{CipherId, User, UserId};\nuse crate::db::schema::{folders, folders_ciphers};\nuse diesel::prelude::*;\nuse macros::UuidFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = folders)]\n#[diesel(primary_key(uuid))]\npub struct Folder {\n    pub uuid: FolderId,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n    pub user_uuid: UserId,\n    pub name: String,\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = folders_ciphers)]\n#[diesel(primary_key(cipher_uuid, folder_uuid))]\npub struct FolderCipher {\n    pub cipher_uuid: CipherId,\n    pub folder_uuid: FolderId,\n}\n\n/// Local methods\nimpl Folder {\n    pub fn new(user_uuid: UserId, name: String) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid: FolderId(crate::util::get_uuid()),\n            created_at: now,\n            updated_at: now,\n\n            user_uuid,\n            name,\n        }\n    }\n\n    pub fn to_json(&self) -> Value {\n        use crate::util::format_date;\n\n        json!({\n            \"id\": self.uuid,\n            \"revisionDate\": format_date(&self.updated_at),\n            \"name\": self.name,\n            \"object\": \"folder\",\n        })\n    }\n}\n\nimpl FolderCipher {\n    pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self {\n        Self {\n            folder_uuid,\n            cipher_uuid,\n        }\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Folder {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.user_uuid, conn).await;\n        self.updated_at = Utc::now().naive_utc();\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(folders::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(folders::table)\n                            .filter(folders::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error saving folder\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving folder\")\n            }\n            postgresql {\n                diesel::insert_into(folders::table)\n                    .values(&*self)\n                    .on_conflict(folders::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving folder\")\n            }\n        }\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.user_uuid, conn).await;\n        FolderCipher::delete_all_by_folder(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting folder\")\n        }}\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        for folder in Self::find_by_user(user_uuid, conn).await {\n            folder.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            folders::table\n                .filter(folders::uuid.eq(uuid))\n                .filter(folders::user_uuid.eq(user_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            folders::table\n                .filter(folders::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading folders\")\n        }}\n    }\n}\n\nimpl FolderCipher {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                // Not checking for ForeignKey Constraints here.\n                // Table folders_ciphers does not have ForeignKey Constraints which would cause conflicts.\n                // This table has no constraints pointing to itself, but only to others.\n                diesel::replace_into(folders_ciphers::table)\n                    .values(self)\n                    .execute(conn)\n                    .map_res(\"Error adding cipher to folder\")\n            }\n            postgresql {\n                diesel::insert_into(folders_ciphers::table)\n                    .values(self)\n                    .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))\n                    .do_nothing()\n                    .execute(conn)\n                    .map_res(\"Error adding cipher to folder\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(\n                folders_ciphers::table\n                    .filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))\n                    .filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),\n            )\n            .execute(conn)\n            .map_res(\"Error removing cipher from folder\")\n        }}\n    }\n\n    pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing cipher from folders\")\n        }}\n    }\n\n    pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing ciphers from folder\")\n        }}\n    }\n\n    pub async fn find_by_folder_and_cipher(\n        folder_uuid: &FolderId,\n        cipher_uuid: &CipherId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            folders_ciphers::table\n                .filter(folders_ciphers::folder_uuid.eq(folder_uuid))\n                .filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_folder(folder_uuid: &FolderId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            folders_ciphers::table\n                .filter(folders_ciphers::folder_uuid.eq(folder_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading folders\")\n        }}\n    }\n\n    /// Return a vec with (cipher_uuid, folder_uuid)\n    /// This is used during a full sync so we only need one query for all folder matches.\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, FolderId)> {\n        db_run! { conn: {\n            folders_ciphers::table\n                .inner_join(folders::table)\n                .filter(folders::user_uuid.eq(user_uuid))\n                .select(folders_ciphers::all_columns)\n                .load::<(CipherId, FolderId)>(conn)\n                .unwrap_or_default()\n        }}\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct FolderId(String);\n"
  },
  {
    "path": "src/db/models/group.rs",
    "content": "use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};\nuse crate::api::EmptyResult;\nuse crate::db::schema::{collections_groups, groups, groups_users, users_organizations};\nuse crate::db::DbConn;\nuse crate::error::MapResult;\nuse chrono::{NaiveDateTime, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse diesel::prelude::*;\nuse macros::UuidFromParam;\nuse serde_json::Value;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = groups)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Group {\n    pub uuid: GroupId,\n    pub organizations_uuid: OrganizationId,\n    pub name: String,\n    pub access_all: bool,\n    pub external_id: Option<String>,\n    pub creation_date: NaiveDateTime,\n    pub revision_date: NaiveDateTime,\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = collections_groups)]\n#[diesel(primary_key(collections_uuid, groups_uuid))]\npub struct CollectionGroup {\n    pub collections_uuid: CollectionId,\n    pub groups_uuid: GroupId,\n    pub read_only: bool,\n    pub hide_passwords: bool,\n    pub manage: bool,\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = groups_users)]\n#[diesel(primary_key(groups_uuid, users_organizations_uuid))]\npub struct GroupUser {\n    pub groups_uuid: GroupId,\n    pub users_organizations_uuid: MembershipId,\n}\n\n/// Local methods\nimpl Group {\n    pub fn new(\n        organizations_uuid: OrganizationId,\n        name: String,\n        access_all: bool,\n        external_id: Option<String>,\n    ) -> Self {\n        let now = Utc::now().naive_utc();\n\n        let mut new_model = Self {\n            uuid: GroupId(crate::util::get_uuid()),\n            organizations_uuid,\n            name,\n            access_all,\n            external_id: None,\n            creation_date: now,\n            revision_date: now,\n        };\n\n        new_model.set_external_id(external_id);\n\n        new_model\n    }\n\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"organizationId\": self.organizations_uuid,\n            \"name\": self.name,\n            \"externalId\": self.external_id,\n            \"object\": \"group\"\n        })\n    }\n\n    pub async fn to_json_details(&self, conn: &DbConn) -> Value {\n        // If both read_only and hide_passwords are false, then manage should be true\n        // You can't have an entry with read_only and manage, or hide_passwords and manage\n        // Or an entry with everything to false\n        let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)\n            .await\n            .iter()\n            .map(|entry| {\n                json!({\n                    \"id\": entry.collections_uuid,\n                    \"readOnly\": entry.read_only,\n                    \"hidePasswords\": entry.hide_passwords,\n                    \"manage\": entry.manage,\n                })\n            })\n            .collect();\n\n        json!({\n            \"id\": self.uuid,\n            \"organizationId\": self.organizations_uuid,\n            \"name\": self.name,\n            \"accessAll\": self.access_all,\n            \"externalId\": self.external_id,\n            \"collections\": collections_groups,\n            \"object\": \"groupDetails\"\n        })\n    }\n\n    pub fn set_external_id(&mut self, external_id: Option<String>) {\n        // Check if external_id is empty. We do not want to have empty strings in the database\n        self.external_id = match external_id {\n            Some(external_id) if !external_id.trim().is_empty() => Some(external_id),\n            _ => None,\n        };\n    }\n}\n\nimpl CollectionGroup {\n    pub fn new(\n        collections_uuid: CollectionId,\n        groups_uuid: GroupId,\n        read_only: bool,\n        hide_passwords: bool,\n        manage: bool,\n    ) -> Self {\n        Self {\n            collections_uuid,\n            groups_uuid,\n            read_only,\n            hide_passwords,\n            manage,\n        }\n    }\n\n    pub fn to_json_details_for_group(&self) -> Value {\n        // If both read_only and hide_passwords are false, then manage should be true\n        // You can't have an entry with read_only and manage, or hide_passwords and manage\n        // Or an entry with everything to false\n        // For backwards compatibility and migration proposes we keep checking read_only and hide_password\n        json!({\n            \"id\": self.groups_uuid,\n            \"readOnly\": self.read_only,\n            \"hidePasswords\": self.hide_passwords,\n            \"manage\": self.manage || (!self.read_only && !self.hide_passwords),\n        })\n    }\n}\n\nimpl GroupUser {\n    pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self {\n        Self {\n            groups_uuid,\n            users_organizations_uuid,\n        }\n    }\n}\n\n/// Database methods\nimpl Group {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        self.revision_date = Utc::now().naive_utc();\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(groups::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(groups::table)\n                            .filter(groups::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error saving group\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving group\")\n            }\n            postgresql {\n                diesel::insert_into(groups::table)\n                    .values(&*self)\n                    .on_conflict(groups::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving group\")\n            }\n        }\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        for group in Self::find_by_organization(org_uuid, conn).await {\n            group.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            groups::table\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading groups\")\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            groups::table\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            groups::table\n                .filter(groups::uuid.eq(uuid))\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_external_id_and_org(\n        external_id: &str,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            groups::table\n                .filter(groups::external_id.eq(external_id))\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n    //Returns all organizations the user has full access to\n    pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {\n        db_run! { conn: {\n            groups_users::table\n                .inner_join(users_organizations::table.on(\n                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)\n                ))\n                .inner_join(groups::table.on(\n                    groups::uuid.eq(groups_users::groups_uuid)\n                ))\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(groups::access_all.eq(true))\n                .select(groups::organizations_uuid)\n                .distinct()\n                .load::<OrganizationId>(conn)\n                .expect(\"Error loading organization group full access information for user\")\n        }}\n    }\n\n    pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> bool {\n        db_run! { conn: {\n            groups::table\n                .inner_join(groups_users::table.on(\n                    groups_users::groups_uuid.eq(groups::uuid)\n                ))\n                .inner_join(users_organizations::table.on(\n                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)\n                ))\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .filter(groups::access_all.eq(true))\n                .select(groups::access_all)\n                .first::<bool>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        CollectionGroup::delete_all_by_group(&self.uuid, conn).await?;\n        GroupUser::delete_all_by_group(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting group\")\n        }}\n    }\n\n    pub async fn update_revision(uuid: &GroupId, conn: &DbConn) {\n        if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {\n            warn!(\"Failed to update revision for {uuid}: {e:#?}\");\n        }\n    }\n\n    async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            crate::util::retry(|| {\n                diesel::update(groups::table.filter(groups::uuid.eq(uuid)))\n                    .set(groups::revision_date.eq(date))\n                    .execute(conn)\n            }, 10)\n            .map_res(\"Error updating group revision\")\n        }}\n    }\n}\n\nimpl CollectionGroup {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;\n        for group_user in group_users {\n            group_user.update_user_revision(conn).await;\n        }\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(collections_groups::table)\n                    .values((\n                        collections_groups::collections_uuid.eq(&self.collections_uuid),\n                        collections_groups::groups_uuid.eq(&self.groups_uuid),\n                        collections_groups::read_only.eq(&self.read_only),\n                        collections_groups::hide_passwords.eq(&self.hide_passwords),\n                        collections_groups::manage.eq(&self.manage),\n                    ))\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(collections_groups::table)\n                            .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))\n                            .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))\n                            .set((\n                                collections_groups::collections_uuid.eq(&self.collections_uuid),\n                                collections_groups::groups_uuid.eq(&self.groups_uuid),\n                                collections_groups::read_only.eq(&self.read_only),\n                                collections_groups::hide_passwords.eq(&self.hide_passwords),\n                                collections_groups::manage.eq(&self.manage),\n                            ))\n                            .execute(conn)\n                            .map_res(\"Error adding group to collection\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error adding group to collection\")\n            }\n            postgresql {\n                diesel::insert_into(collections_groups::table)\n                    .values((\n                        collections_groups::collections_uuid.eq(&self.collections_uuid),\n                        collections_groups::groups_uuid.eq(&self.groups_uuid),\n                        collections_groups::read_only.eq(self.read_only),\n                        collections_groups::hide_passwords.eq(self.hide_passwords),\n                        collections_groups::manage.eq(self.manage),\n                    ))\n                    .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))\n                    .do_update()\n                    .set((\n                        collections_groups::read_only.eq(self.read_only),\n                        collections_groups::hide_passwords.eq(self.hide_passwords),\n                        collections_groups::manage.eq(self.manage),\n                    ))\n                    .execute(conn)\n                    .map_res(\"Error adding group to collection\")\n            }\n        }\n    }\n\n    pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            collections_groups::table\n                .filter(collections_groups::groups_uuid.eq(group_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading collection groups\")\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            collections_groups::table\n                .inner_join(groups_users::table.on(\n                    groups_users::groups_uuid.eq(collections_groups::groups_uuid)\n                ))\n                .inner_join(users_organizations::table.on(\n                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)\n                ))\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .select(collections_groups::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading user collection groups\")\n        }}\n    }\n\n    pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            collections_groups::table\n                .filter(collections_groups::collections_uuid.eq(collection_uuid))\n                .select(collections_groups::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading collection groups\")\n        }}\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;\n        for group_user in group_users {\n            group_user.update_user_revision(conn).await;\n        }\n\n        db_run! { conn: {\n            diesel::delete(collections_groups::table)\n                .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))\n                .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting collection group\")\n        }}\n    }\n\n    pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {\n        let group_users = GroupUser::find_by_group(group_uuid, conn).await;\n        for group_user in group_users {\n            group_user.update_user_revision(conn).await;\n        }\n\n        db_run! { conn: {\n            diesel::delete(collections_groups::table)\n                .filter(collections_groups::groups_uuid.eq(group_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting collection group\")\n        }}\n    }\n\n    pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult {\n        let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;\n        for collection_assigned_to_group in collection_assigned_to_groups {\n            let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await;\n            for group_user in group_users {\n                group_user.update_user_revision(conn).await;\n            }\n        }\n\n        db_run! { conn: {\n            diesel::delete(collections_groups::table)\n                .filter(collections_groups::collections_uuid.eq(collection_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting collection group\")\n        }}\n    }\n}\n\nimpl GroupUser {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        self.update_user_revision(conn).await;\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(groups_users::table)\n                    .values((\n                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),\n                        groups_users::groups_uuid.eq(&self.groups_uuid),\n                    ))\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(groups_users::table)\n                            .filter(groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid))\n                            .filter(groups_users::groups_uuid.eq(&self.groups_uuid))\n                            .set((\n                                groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),\n                                groups_users::groups_uuid.eq(&self.groups_uuid),\n                            ))\n                            .execute(conn)\n                            .map_res(\"Error adding user to group\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error adding user to group\")\n            }\n            postgresql {\n                diesel::insert_into(groups_users::table)\n                    .values((\n                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),\n                        groups_users::groups_uuid.eq(&self.groups_uuid),\n                    ))\n                    .on_conflict((groups_users::users_organizations_uuid, groups_users::groups_uuid))\n                    .do_update()\n                    .set((\n                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),\n                        groups_users::groups_uuid.eq(&self.groups_uuid),\n                    ))\n                    .execute(conn)\n                    .map_res(\"Error adding user to group\")\n            }\n        }\n    }\n\n    pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            groups_users::table\n                .filter(groups_users::groups_uuid.eq(group_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading group users\")\n        }}\n    }\n\n    pub async fn find_by_member(member_uuid: &MembershipId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            groups_users::table\n                .filter(groups_users::users_organizations_uuid.eq(member_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading groups for user\")\n        }}\n    }\n\n    pub async fn has_access_to_collection_by_member(\n        collection_uuid: &CollectionId,\n        member_uuid: &MembershipId,\n        conn: &DbConn,\n    ) -> bool {\n        db_run! { conn: {\n            groups_users::table\n                .inner_join(collections_groups::table.on(\n                    collections_groups::groups_uuid.eq(groups_users::groups_uuid)\n                ))\n                .filter(collections_groups::collections_uuid.eq(collection_uuid))\n                .filter(groups_users::users_organizations_uuid.eq(member_uuid))\n                .count()\n                .first::<i64>(conn)\n                .unwrap_or(0) != 0\n        }}\n    }\n\n    pub async fn has_full_access_by_member(\n        org_uuid: &OrganizationId,\n        member_uuid: &MembershipId,\n        conn: &DbConn,\n    ) -> bool {\n        db_run! { conn: {\n            groups_users::table\n                .inner_join(groups::table.on(\n                    groups::uuid.eq(groups_users::groups_uuid)\n                ))\n                .filter(groups::organizations_uuid.eq(org_uuid))\n                .filter(groups::access_all.eq(true))\n                .filter(groups_users::users_organizations_uuid.eq(member_uuid))\n                .count()\n                .first::<i64>(conn)\n                .unwrap_or(0) != 0\n        }}\n    }\n\n    pub async fn update_user_revision(&self, conn: &DbConn) {\n        match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await {\n            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,\n            None => warn!(\"Member could not be found!\"),\n        }\n    }\n\n    pub async fn delete_by_group_and_member(\n        group_uuid: &GroupId,\n        member_uuid: &MembershipId,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        match Membership::find_by_uuid(member_uuid, conn).await {\n            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,\n            None => warn!(\"Member could not be found!\"),\n        };\n\n        db_run! { conn: {\n            diesel::delete(groups_users::table)\n                .filter(groups_users::groups_uuid.eq(group_uuid))\n                .filter(groups_users::users_organizations_uuid.eq(member_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting group users\")\n        }}\n    }\n\n    pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult {\n        let group_users = GroupUser::find_by_group(group_uuid, conn).await;\n        for group_user in group_users {\n            group_user.update_user_revision(conn).await;\n        }\n\n        db_run! { conn: {\n            diesel::delete(groups_users::table)\n                .filter(groups_users::groups_uuid.eq(group_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting group users\")\n        }}\n    }\n\n    pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &DbConn) -> EmptyResult {\n        match Membership::find_by_uuid(member_uuid, conn).await {\n            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,\n            None => warn!(\"Member could not be found!\"),\n        }\n\n        db_run! { conn: {\n            diesel::delete(groups_users::table)\n                .filter(groups_users::users_organizations_uuid.eq(member_uuid))\n                .execute(conn)\n                .map_res(\"Error deleting user groups\")\n        }}\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct GroupId(String);\n"
  },
  {
    "path": "src/db/models/mod.rs",
    "content": "mod attachment;\nmod auth_request;\nmod cipher;\nmod collection;\nmod device;\nmod emergency_access;\nmod event;\nmod favorite;\nmod folder;\nmod group;\nmod org_policy;\nmod organization;\nmod send;\nmod sso_auth;\nmod two_factor;\nmod two_factor_duo_context;\nmod two_factor_incomplete;\nmod user;\n\npub use self::attachment::{Attachment, AttachmentId};\npub use self::auth_request::{AuthRequest, AuthRequestId};\npub use self::cipher::{Cipher, CipherId, RepromptType};\npub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};\npub use self::device::{Device, DeviceId, DeviceType, PushId};\npub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};\npub use self::event::{Event, EventType};\npub use self::favorite::Favorite;\npub use self::folder::{Folder, FolderCipher, FolderId};\npub use self::group::{CollectionGroup, Group, GroupId, GroupUser};\npub use self::org_policy::{OrgPolicy, OrgPolicyId, OrgPolicyType};\npub use self::organization::{\n    Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey,\n    OrganizationId,\n};\npub use self::send::{\n    id::{SendFileId, SendId},\n    Send, SendType,\n};\npub use self::sso_auth::{OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth};\npub use self::two_factor::{TwoFactor, TwoFactorType};\npub use self::two_factor_duo_context::TwoFactorDuoContext;\npub use self::two_factor_incomplete::TwoFactorIncomplete;\npub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException};\n"
  },
  {
    "path": "src/db/models/org_policy.rs",
    "content": "use derive_more::{AsRef, From};\nuse serde::Deserialize;\nuse serde_json::Value;\n\nuse crate::api::core::two_factor;\nuse crate::api::EmptyResult;\nuse crate::db::schema::{org_policies, users_organizations};\nuse crate::db::DbConn;\nuse crate::error::MapResult;\nuse crate::CONFIG;\nuse diesel::prelude::*;\n\nuse super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId};\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = org_policies)]\n#[diesel(primary_key(uuid))]\npub struct OrgPolicy {\n    pub uuid: OrgPolicyId,\n    pub org_uuid: OrganizationId,\n    pub atype: i32,\n    pub enabled: bool,\n    pub data: String,\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs\n#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]\npub enum OrgPolicyType {\n    TwoFactorAuthentication = 0,\n    MasterPassword = 1,\n    PasswordGenerator = 2,\n    SingleOrg = 3,\n    // RequireSso = 4, // Not supported\n    PersonalOwnership = 5,\n    DisableSend = 6,\n    SendOptions = 7,\n    ResetPassword = 8,\n    // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)\n    // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)\n    // ActivateAutofill = 11,\n    // AutomaticAppLogIn = 12,\n    // FreeFamiliesSponsorshipPolicy = 13,\n    RemoveUnlockWithPin = 14,\n    RestrictedItemTypes = 15,\n    UriMatchDefaults = 16,\n    // AutotypeDefaultSetting = 17, // Not supported yet\n    // AutoConfirm = 18, // Not supported (not implemented yet)\n    // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed)\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct SendOptionsPolicyData {\n    #[serde(rename = \"disableHideEmail\", alias = \"DisableHideEmail\")]\n    pub disable_hide_email: bool,\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs\n#[derive(Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ResetPasswordDataModel {\n    #[serde(rename = \"autoEnrollEnabled\", alias = \"AutoEnrollEnabled\")]\n    pub auto_enroll_enabled: bool,\n}\n\n/// Local methods\nimpl OrgPolicy {\n    pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, enabled: bool, data: String) -> Self {\n        Self {\n            uuid: OrgPolicyId(crate::util::get_uuid()),\n            org_uuid,\n            atype: atype as i32,\n            enabled,\n            data,\n        }\n    }\n\n    pub fn has_type(&self, policy_type: OrgPolicyType) -> bool {\n        self.atype == policy_type as i32\n    }\n\n    pub fn to_json(&self) -> Value {\n        let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);\n        let mut policy = json!({\n            \"id\": self.uuid,\n            \"organizationId\": self.org_uuid,\n            \"type\": self.atype,\n            \"data\": data_json,\n            \"enabled\": self.enabled,\n            \"object\": \"policy\",\n        });\n\n        // Upstream adds this key/value\n        // Allow enabling Single Org policy when the organization has claimed domains.\n        // See: (https://github.com/bitwarden/server/pull/5565)\n        // We return the same to prevent possible issues\n        if self.atype == 8i32 {\n            policy[\"canToggleState\"] = json!(true);\n        }\n\n        policy\n    }\n}\n\n/// Database methods\nimpl OrgPolicy {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(org_policies::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(org_policies::table)\n                            .filter(org_policies::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving org_policy\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving org_policy\")\n            }\n            postgresql {\n                // We need to make sure we're not going to violate the unique constraint on org_uuid and atype.\n                // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does\n                // not support multiple constraints on ON CONFLICT clauses.\n                let _: () = diesel::delete(\n                    org_policies::table\n                        .filter(org_policies::org_uuid.eq(&self.org_uuid))\n                        .filter(org_policies::atype.eq(&self.atype)),\n                )\n                .execute(conn)\n                .map_res(\"Error deleting org_policy for insert\")?;\n\n                diesel::insert_into(org_policies::table)\n                    .values(self)\n                    .on_conflict(org_policies::uuid)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving org_policy\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting org_policy\")\n        }}\n    }\n\n    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            org_policies::table\n                .filter(org_policies::org_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading org_policy\")\n        }}\n    }\n\n    pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            org_policies::table\n                .inner_join(\n                    users_organizations::table.on(\n                        users_organizations::org_uuid.eq(org_policies::org_uuid)\n                            .and(users_organizations::user_uuid.eq(user_uuid)))\n                )\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .select(org_policies::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading org_policy\")\n        }}\n    }\n\n    pub async fn find_by_org_and_type(\n        org_uuid: &OrganizationId,\n        policy_type: OrgPolicyType,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            org_policies::table\n                .filter(org_policies::org_uuid.eq(org_uuid))\n                .filter(org_policies::atype.eq(policy_type as i32))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting org_policy\")\n        }}\n    }\n\n    pub async fn find_accepted_and_confirmed_by_user_and_active_policy(\n        user_uuid: &UserId,\n        policy_type: OrgPolicyType,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            org_policies::table\n                .inner_join(\n                    users_organizations::table.on(\n                        users_organizations::org_uuid.eq(org_policies::org_uuid)\n                            .and(users_organizations::user_uuid.eq(user_uuid)))\n                )\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Accepted as i32)\n                )\n                .or_filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .filter(org_policies::atype.eq(policy_type as i32))\n                .filter(org_policies::enabled.eq(true))\n                .select(org_policies::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading org_policy\")\n        }}\n    }\n\n    pub async fn find_confirmed_by_user_and_active_policy(\n        user_uuid: &UserId,\n        policy_type: OrgPolicyType,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            org_policies::table\n                .inner_join(\n                    users_organizations::table.on(\n                        users_organizations::org_uuid.eq(org_policies::org_uuid)\n                            .and(users_organizations::user_uuid.eq(user_uuid)))\n                )\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .filter(org_policies::atype.eq(policy_type as i32))\n                .filter(org_policies::enabled.eq(true))\n                .select(org_policies::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading org_policy\")\n        }}\n    }\n\n    /// Returns true if the user belongs to an org that has enabled the specified policy type,\n    /// and the user is not an owner or admin of that org. This is only useful for checking\n    /// applicability of policy types that have these particular semantics.\n    pub async fn is_applicable_to_user(\n        user_uuid: &UserId,\n        policy_type: OrgPolicyType,\n        exclude_org_uuid: Option<&OrganizationId>,\n        conn: &DbConn,\n    ) -> bool {\n        for policy in\n            OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await\n        {\n            // Check if we need to skip this organization.\n            if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid {\n                continue;\n            }\n\n            if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {\n                if user.atype < MembershipType::Admin {\n                    return true;\n                }\n            }\n        }\n        false\n    }\n\n    pub async fn check_user_allowed(m: &Membership, action: &str, conn: &DbConn) -> EmptyResult {\n        if m.atype < MembershipType::Admin && m.status > (MembershipStatus::Invited as i32) {\n            // Enforce TwoFactor/TwoStep login\n            if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await\n            {\n                if p.enabled && TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty() {\n                    if CONFIG.email_2fa_auto_fallback() {\n                        two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?;\n                    } else {\n                        err!(format!(\"Cannot {} because 2FA is required (membership {})\", action, m.uuid));\n                    }\n                }\n            }\n\n            // Check if the user is part of another Organization with SingleOrg activated\n            if Self::is_applicable_to_user(&m.user_uuid, OrgPolicyType::SingleOrg, Some(&m.org_uuid), conn).await {\n                err!(format!(\n                    \"Cannot {} because another organization policy forbids it (membership {})\",\n                    action, m.uuid\n                ));\n            }\n\n            if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await {\n                if p.enabled\n                    && Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0\n                {\n                    err!(format!(\"Cannot {} because the organization policy forbids being part of other organization (membership {})\", action, m.uuid));\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool {\n        match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {\n            Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {\n                Ok(opts) => {\n                    return policy.enabled && opts.auto_enroll_enabled;\n                }\n                _ => error!(\"Failed to deserialize ResetPasswordDataModel: {}\", policy.data),\n            },\n            None => return false,\n        }\n\n        false\n    }\n\n    /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`\n    /// option of the `Send Options` policy, and the user is not an owner or admin of that org.\n    pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &DbConn) -> bool {\n        for policy in\n            OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await\n        {\n            if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {\n                if user.atype < MembershipType::Admin {\n                    match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {\n                        Ok(opts) => {\n                            if opts.disable_hide_email {\n                                return true;\n                            }\n                        }\n                        _ => error!(\"Failed to deserialize SendOptionsPolicyData: {}\", policy.data),\n                    }\n                }\n            }\n        }\n        false\n    }\n\n    pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool {\n        if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await {\n            if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await {\n                return policy.enabled;\n            }\n        }\n        false\n    }\n}\n\n#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct OrgPolicyId(String);\n"
  },
  {
    "path": "src/db/models/organization.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse diesel::prelude::*;\nuse num_traits::FromPrimitive;\nuse serde_json::Value;\nuse std::{\n    cmp::Ordering,\n    collections::{HashMap, HashSet},\n};\n\nuse super::{\n    CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,\n    OrgPolicyType, TwoFactor, User, UserId,\n};\nuse crate::db::schema::{\n    ciphers, ciphers_collections, collections_groups, groups, groups_users, org_policies, organization_api_key,\n    organizations, users, users_collections, users_organizations,\n};\nuse crate::CONFIG;\nuse macros::UuidFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = organizations)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Organization {\n    pub uuid: OrganizationId,\n    pub name: String,\n    pub billing_email: String,\n    pub private_key: Option<String>,\n    pub public_key: Option<String>,\n}\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = users_organizations)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Membership {\n    pub uuid: MembershipId,\n    pub user_uuid: UserId,\n    pub org_uuid: OrganizationId,\n\n    pub invited_by_email: Option<String>,\n\n    pub access_all: bool,\n    pub akey: String,\n    pub status: i32,\n    pub atype: i32,\n    pub reset_password_key: Option<String>,\n    pub external_id: Option<String>,\n}\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = organization_api_key)]\n#[diesel(primary_key(uuid, org_uuid))]\npub struct OrganizationApiKey {\n    pub uuid: OrgApiKeyId,\n    pub org_uuid: OrganizationId,\n    pub atype: i32,\n    pub api_key: String,\n    pub revision_date: NaiveDateTime,\n}\n\n// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs\n#[derive(PartialEq)]\npub enum MembershipStatus {\n    Revoked = -1,\n    Invited = 0,\n    Accepted = 1,\n    Confirmed = 2,\n}\n\nimpl MembershipStatus {\n    pub fn from_i32(status: i32) -> Option<Self> {\n        match status {\n            0 => Some(Self::Invited),\n            1 => Some(Self::Accepted),\n            2 => Some(Self::Confirmed),\n            // NOTE: we don't care about revoked members where this is used\n            // if this ever changes also adapt the OrgHeaders check.\n            _ => None,\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]\npub enum MembershipType {\n    Owner = 0,\n    Admin = 1,\n    User = 2,\n    Manager = 3,\n}\n\nimpl MembershipType {\n    pub fn from_str(s: &str) -> Option<Self> {\n        match s {\n            \"0\" | \"Owner\" => Some(MembershipType::Owner),\n            \"1\" | \"Admin\" => Some(MembershipType::Admin),\n            \"2\" | \"User\" => Some(MembershipType::User),\n            \"3\" | \"Manager\" => Some(MembershipType::Manager),\n            // HACK: We convert the custom role to a manager role\n            \"4\" | \"Custom\" => Some(MembershipType::Manager),\n            _ => None,\n        }\n    }\n}\n\nimpl Ord for MembershipType {\n    fn cmp(&self, other: &MembershipType) -> Ordering {\n        // For easy comparison, map each variant to an access level (where 0 is lowest).\n        const ACCESS_LEVEL: [i32; 4] = [\n            3, // Owner\n            2, // Admin\n            0, // User\n            1, // Manager && Custom\n        ];\n        ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize])\n    }\n}\n\nimpl PartialOrd for MembershipType {\n    fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {\n        Some(self.cmp(other))\n    }\n}\n\nimpl PartialEq<i32> for MembershipType {\n    fn eq(&self, other: &i32) -> bool {\n        *other == *self as i32\n    }\n}\n\nimpl PartialOrd<i32> for MembershipType {\n    fn partial_cmp(&self, other: &i32) -> Option<Ordering> {\n        if let Some(other) = Self::from_i32(*other) {\n            return Some(self.cmp(&other));\n        }\n        None\n    }\n\n    fn gt(&self, other: &i32) -> bool {\n        matches!(self.partial_cmp(other), Some(Ordering::Greater))\n    }\n\n    fn ge(&self, other: &i32) -> bool {\n        matches!(self.partial_cmp(other), Some(Ordering::Greater | Ordering::Equal))\n    }\n}\n\nimpl PartialEq<MembershipType> for i32 {\n    fn eq(&self, other: &MembershipType) -> bool {\n        *self == *other as i32\n    }\n}\n\nimpl PartialOrd<MembershipType> for i32 {\n    fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {\n        if let Some(self_type) = MembershipType::from_i32(*self) {\n            return Some(self_type.cmp(other));\n        }\n        None\n    }\n\n    fn lt(&self, other: &MembershipType) -> bool {\n        matches!(self.partial_cmp(other), Some(Ordering::Less) | None)\n    }\n\n    fn le(&self, other: &MembershipType) -> bool {\n        matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None)\n    }\n}\n\n/// Local methods\nimpl Organization {\n    pub fn new(name: String, billing_email: &str, private_key: Option<String>, public_key: Option<String>) -> Self {\n        let billing_email = billing_email.to_lowercase();\n        Self {\n            uuid: OrganizationId(crate::util::get_uuid()),\n            name,\n            billing_email,\n            private_key,\n            public_key,\n        }\n    }\n    // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"name\": self.name,\n            \"seats\": null,\n            \"maxCollections\": null,\n            \"maxStorageGb\": i16::MAX, // The value doesn't matter, we don't check server-side\n            \"use2fa\": true,\n            \"useCustomPermissions\": true,\n            \"useDirectory\": false, // Is supported, but this value isn't checked anywhere (yet)\n            \"useEvents\": CONFIG.org_events_enabled(),\n            \"useGroups\": CONFIG.org_groups_enabled(),\n            \"useTotp\": true,\n            \"usePolicies\": true,\n            \"useScim\": false, // Not supported (Not AGPLv3 Licensed)\n            \"useSso\": false, // Not supported\n            \"useKeyConnector\": false, // Not supported\n            \"usePasswordManager\": true,\n            \"useSecretsManager\": false, // Not supported (Not AGPLv3 Licensed)\n            \"selfHost\": true,\n            \"useApi\": true,\n            \"hasPublicAndPrivateKeys\": self.private_key.is_some() && self.public_key.is_some(),\n            \"useResetPassword\": CONFIG.mail_enabled(),\n            \"allowAdminAccessToAllCollectionItems\": true,\n            \"limitCollectionCreation\": true,\n            \"limitCollectionDeletion\": true,\n\n            \"businessName\": self.name,\n            \"businessAddress1\": null,\n            \"businessAddress2\": null,\n            \"businessAddress3\": null,\n            \"businessCountry\": null,\n            \"businessTaxNumber\": null,\n\n            \"maxAutoscaleSeats\": null,\n            \"maxAutoscaleSmSeats\": null,\n            \"maxAutoscaleSmServiceAccounts\": null,\n\n            \"secretsManagerPlan\": null,\n            \"smSeats\": null,\n            \"smServiceAccounts\": null,\n\n            \"billingEmail\": self.billing_email,\n            \"planType\": 6, // Custom plan\n            \"usersGetPremium\": true,\n            \"object\": \"organization\",\n        })\n    }\n}\n\n// Used to either subtract or add to the current status\n// The number 128 should be fine, it is well within the range of an i32\n// The same goes for the database where we only use INTEGER (the same as an i32)\n// It should also provide enough room for 100+ types, which i doubt will ever happen.\nconst ACTIVATE_REVOKE_DIFF: i32 = 128;\n\nimpl Membership {\n    pub fn new(user_uuid: UserId, org_uuid: OrganizationId, invited_by_email: Option<String>) -> Self {\n        Self {\n            uuid: MembershipId(crate::util::get_uuid()),\n\n            user_uuid,\n            org_uuid,\n            invited_by_email,\n\n            access_all: false,\n            akey: String::new(),\n            status: MembershipStatus::Accepted as i32,\n            atype: MembershipType::User as i32,\n            reset_password_key: None,\n            external_id: None,\n        }\n    }\n\n    pub fn restore(&mut self) -> bool {\n        if self.status < MembershipStatus::Invited as i32 {\n            self.status += ACTIVATE_REVOKE_DIFF;\n            return true;\n        }\n        false\n    }\n\n    pub fn revoke(&mut self) -> bool {\n        if self.status > MembershipStatus::Revoked as i32 {\n            self.status -= ACTIVATE_REVOKE_DIFF;\n            return true;\n        }\n        false\n    }\n\n    /// Return the status of the user in an unrevoked state\n    pub fn get_unrevoked_status(&self) -> i32 {\n        if self.status <= MembershipStatus::Revoked as i32 {\n            return self.status + ACTIVATE_REVOKE_DIFF;\n        }\n        self.status\n    }\n\n    pub fn set_external_id(&mut self, external_id: Option<String>) -> bool {\n        //Check if external id is empty. We don't want to have\n        //empty strings in the database\n        if self.external_id != external_id {\n            self.external_id = match external_id {\n                Some(external_id) if !external_id.is_empty() => Some(external_id),\n                _ => None,\n            };\n            return true;\n        }\n        false\n    }\n\n    /// HACK: Convert the manager type to a custom type\n    /// It will be converted back on other locations\n    pub fn type_manager_as_custom(&self) -> i32 {\n        match self.atype {\n            3 => 4,\n            _ => self.atype,\n        }\n    }\n}\n\nimpl OrganizationApiKey {\n    pub fn new(org_uuid: OrganizationId, api_key: String) -> Self {\n        Self {\n            uuid: OrgApiKeyId(crate::util::get_uuid()),\n\n            org_uuid,\n            atype: 0, // Type 0 is the default and only type we support currently\n            api_key,\n            revision_date: Utc::now().naive_utc(),\n        }\n    }\n\n    pub fn check_valid_api_key(&self, api_key: &str) -> bool {\n        crate::crypto::ct_eq(&self.api_key, api_key)\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\n\n/// Database methods\nimpl Organization {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        if !crate::util::is_valid_email(&self.billing_email) {\n            err!(format!(\"BillingEmail {} is not a valid email address\", self.billing_email))\n        }\n\n        for member in Membership::find_by_org(&self.uuid, conn).await.iter() {\n            User::update_uuid_revision(&member.user_uuid, conn).await;\n        }\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(organizations::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(organizations::table)\n                            .filter(organizations::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving organization\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving organization\")\n\n            }\n            postgresql {\n                diesel::insert_into(organizations::table)\n                    .values(self)\n                    .on_conflict(organizations::uuid)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving organization\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        use super::{Cipher, Collection};\n\n        Cipher::delete_all_by_organization(&self.uuid, conn).await?;\n        Collection::delete_all_by_organization(&self.uuid, conn).await?;\n        Membership::delete_all_by_organization(&self.uuid, conn).await?;\n        OrgPolicy::delete_all_by_organization(&self.uuid, conn).await?;\n        Group::delete_all_by_organization(&self.uuid, conn).await?;\n        OrganizationApiKey::delete_all_by_organization(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error saving organization\")\n        }}\n    }\n\n    pub async fn find_by_uuid(uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            organizations::table\n                .filter(organizations::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_name(name: &str, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            organizations::table\n                .filter(organizations::name.eq(name))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn get_all(conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            organizations::table\n                .load::<Self>(conn)\n                .expect(\"Error loading organizations\")\n        }}\n    }\n\n    pub async fn find_main_org_user_email(user_email: &str, conn: &DbConn) -> Option<Self> {\n        let lower_mail = user_email.to_lowercase();\n\n        db_run! { conn: {\n            organizations::table\n                .inner_join(users_organizations::table.on(users_organizations::org_uuid.eq(organizations::uuid)))\n                .inner_join(users::table.on(users::uuid.eq(users_organizations::user_uuid)))\n                .filter(users::email.eq(lower_mail))\n                .filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))\n                .order(users_organizations::atype.asc())\n                .select(organizations::all_columns)\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_org_user_email(user_email: &str, conn: &DbConn) -> Vec<Self> {\n        let lower_mail = user_email.to_lowercase();\n\n        db_run! { conn: {\n            organizations::table\n                .inner_join(users_organizations::table.on(users_organizations::org_uuid.eq(organizations::uuid)))\n                .inner_join(users::table.on(users::uuid.eq(users_organizations::user_uuid)))\n                .filter(users::email.eq(lower_mail))\n                .filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))\n                .order(users_organizations::atype.asc())\n                .select(organizations::all_columns)\n                .load::<Self>(conn)\n                .expect(\"Error loading user orgs\")\n        }}\n    }\n}\n\nimpl Membership {\n    pub async fn to_json(&self, conn: &DbConn) -> Value {\n        let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();\n\n        // HACK: Convert the manager type to a custom type\n        // It will be converted back on other locations\n        let membership_type = self.type_manager_as_custom();\n\n        let permissions = json!({\n                // TODO: Add full support for Custom User Roles\n                // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role\n                // Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission\n                \"accessEventLogs\": false,\n                \"accessImportExport\": false,\n                \"accessReports\": false,\n                // If the following 3 Collection roles are set to true a custom user has access all permission\n                \"createNewCollections\": membership_type == 4 && self.access_all,\n                \"editAnyCollection\": membership_type == 4 && self.access_all,\n                \"deleteAnyCollection\": membership_type == 4 && self.access_all,\n                \"manageGroups\": false,\n                \"managePolicies\": false,\n                \"manageSso\": false, // Not supported\n                \"manageUsers\": false,\n                \"manageResetPassword\": false,\n                \"manageScim\": false // Not supported (Not AGPLv3 Licensed)\n        });\n\n        // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs\n        json!({\n            \"id\": self.org_uuid,\n            \"identifier\": null, // Not supported\n            \"name\": org.name,\n            \"seats\": 20, // hardcoded maxEmailsCount in the web-vault\n            \"maxCollections\": null,\n            \"usersGetPremium\": true,\n            \"use2fa\": true,\n            \"useDirectory\": false, // Is supported, but this value isn't checked anywhere (yet)\n            \"useEvents\": CONFIG.org_events_enabled(),\n            \"useGroups\": CONFIG.org_groups_enabled(),\n            \"useTotp\": true,\n            \"useScim\": false, // Not supported (Not AGPLv3 Licensed)\n            \"usePolicies\": true,\n            \"useApi\": true,\n            \"selfHost\": true,\n            \"hasPublicAndPrivateKeys\": org.private_key.is_some() && org.public_key.is_some(),\n            \"resetPasswordEnrolled\": self.reset_password_key.is_some(),\n            \"useResetPassword\": CONFIG.mail_enabled(),\n            \"ssoBound\": false, // Not supported\n            \"useSso\": false, // Not supported\n            \"useKeyConnector\": false,\n            \"useSecretsManager\": false, // Not supported (Not AGPLv3 Licensed)\n            \"usePasswordManager\": true,\n            \"useCustomPermissions\": true,\n            \"useActivateAutofillPolicy\": false,\n            \"useAdminSponsoredFamilies\": false,\n            \"useRiskInsights\": false, // Not supported (Not AGPLv3 Licensed)\n\n            \"organizationUserId\": self.uuid,\n            \"providerId\": null,\n            \"providerName\": null,\n            \"providerType\": null,\n            \"familySponsorshipFriendlyName\": null,\n            \"familySponsorshipAvailable\": false,\n            \"productTierType\": 3, // Enterprise tier\n            \"keyConnectorEnabled\": false,\n            \"keyConnectorUrl\": null,\n            \"familySponsorshipLastSyncDate\": null,\n            \"familySponsorshipValidUntil\": null,\n            \"familySponsorshipToDelete\": null,\n            \"accessSecretsManager\": false,\n            \"limitCollectionCreation\": self.atype < MembershipType::Manager, // If less then a manager return true, to limit collection creations\n            \"limitCollectionDeletion\": true,\n            \"limitItemDeletion\": false,\n            \"allowAdminAccessToAllCollectionItems\": true,\n            \"userIsManagedByOrganization\": false, // Means not managed via the Members UI, like SSO\n            \"userIsClaimedByOrganization\": false, // The new key instead of the obsolete userIsManagedByOrganization\n\n            \"permissions\": permissions,\n\n            \"maxStorageGb\": i16::MAX, // The value doesn't matter, we don't check server-side\n\n            // These are per user\n            \"userId\": self.user_uuid,\n            \"key\": self.akey,\n            \"status\": self.status,\n            \"type\": membership_type,\n            \"enabled\": true,\n\n            \"object\": \"profileOrganization\",\n        })\n    }\n\n    pub async fn to_json_user_details(&self, include_collections: bool, include_groups: bool, conn: &DbConn) -> Value {\n        let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();\n\n        // Because BitWarden want the status to be -1 for revoked users we need to catch that here.\n        // We subtract/add a number so we can restore/activate the user to it's previous state again.\n        let status = if self.status < MembershipStatus::Revoked as i32 {\n            MembershipStatus::Revoked as i32\n        } else {\n            self.status\n        };\n\n        let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty();\n\n        let groups: Vec<GroupId> = if include_groups && CONFIG.org_groups_enabled() {\n            GroupUser::find_by_member(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect()\n        } else {\n            // The Bitwarden clients seem to call this API regardless of whether groups are enabled,\n            // so just act as if there are no groups.\n            Vec::with_capacity(0)\n        };\n\n        // Check if a user is in a group which has access to all collections\n        // If that is the case, we should not return individual collections!\n        let full_access_group =\n            CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await;\n\n        // If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self\n        let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {\n            // Get all collections for the user here already to prevent more queries\n            let cu: HashMap<CollectionId, CollectionUser> =\n                CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)\n                    .await\n                    .into_iter()\n                    .map(|cu| (cu.collection_uuid.clone(), cu))\n                    .collect();\n\n            // Get all collection groups for this user to prevent there inclusion\n            let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)\n                .await\n                .into_iter()\n                .map(|cg| cg.collections_uuid)\n                .collect();\n\n            Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)\n                .await\n                .into_iter()\n                .filter_map(|c| {\n                    let (read_only, hide_passwords, manage) = if self.has_full_access() {\n                        (false, false, self.atype >= MembershipType::Manager)\n                    } else if let Some(cu) = cu.get(&c.uuid) {\n                        (\n                            cu.read_only,\n                            cu.hide_passwords,\n                            cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),\n                        )\n                    // If previous checks failed it might be that this user has access via a group, but we should not return those elements here\n                    // Those are returned via a special group endpoint\n                    } else if cg.contains(&c.uuid) {\n                        return None;\n                    } else {\n                        (true, true, false)\n                    };\n\n                    Some(json!({\n                        \"id\": c.uuid,\n                        \"readOnly\": read_only,\n                        \"hidePasswords\": hide_passwords,\n                        \"manage\": manage,\n                    }))\n                })\n                .collect()\n        } else {\n            Vec::with_capacity(0)\n        };\n\n        // HACK: Convert the manager type to a custom type\n        // It will be converted back on other locations\n        let membership_type = self.type_manager_as_custom();\n\n        // HACK: Only return permissions if the user is of type custom and has access_all\n        // Else Bitwarden will assume the defaults of all false\n        let permissions = if membership_type == 4 && self.access_all {\n            json!({\n                // TODO: Add full support for Custom User Roles\n                // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role\n                // Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission\n                \"accessEventLogs\": false,\n                \"accessImportExport\": false,\n                \"accessReports\": false,\n                // If the following 3 Collection roles are set to true a custom user has access all permission\n                \"createNewCollections\": true,\n                \"editAnyCollection\": true,\n                \"deleteAnyCollection\": true,\n                \"manageGroups\": false,\n                \"managePolicies\": false,\n                \"manageSso\": false, // Not supported\n                \"manageUsers\": false,\n                \"manageResetPassword\": false,\n                \"manageScim\": false // Not supported (Not AGPLv3 Licensed)\n            })\n        } else {\n            json!(null)\n        };\n\n        json!({\n            \"id\": self.uuid,\n            \"userId\": self.user_uuid,\n            \"name\": if self.get_unrevoked_status() >= MembershipStatus::Accepted as i32 { Some(user.name) } else { None },\n            \"email\": user.email,\n            \"externalId\": self.external_id,\n            \"avatarColor\": user.avatar_color,\n            \"groups\": groups,\n            \"collections\": collections,\n\n            \"status\": status,\n            \"type\": membership_type,\n            \"accessAll\": self.access_all,\n            \"twoFactorEnabled\": twofactor_enabled,\n            \"resetPasswordEnrolled\": self.reset_password_key.is_some(),\n            \"hasMasterPassword\": !user.password_hash.is_empty(),\n\n            \"permissions\": permissions,\n\n            \"ssoBound\": false, // Not supported\n            \"managedByOrganization\": false, // This key is obsolete replaced by claimedByOrganization\n            \"claimedByOrganization\": false, // Means not managed via the Members UI, like SSO\n            \"usesKeyConnector\": false, // Not supported\n            \"accessSecretsManager\": false, // Not supported (Not AGPLv3 Licensed)\n\n            \"object\": \"organizationUserUserDetails\",\n        })\n    }\n\n    pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value {\n        json!({\n            \"id\": self.uuid,\n            \"readOnly\": col_user.read_only,\n            \"hidePasswords\": col_user.hide_passwords,\n            \"manage\": col_user.manage,\n        })\n    }\n\n    pub async fn to_json_details(&self, conn: &DbConn) -> Value {\n        let coll_uuids = if self.access_all {\n            vec![] // If we have complete access, no need to fill the array\n        } else {\n            let collections =\n                CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn).await;\n            collections\n                .iter()\n                .map(|cu| {\n                    json!({\n                        \"id\": cu.collection_uuid,\n                        \"readOnly\": cu.read_only,\n                        \"hidePasswords\": cu.hide_passwords,\n                        \"manage\": cu.manage,\n                    })\n                })\n                .collect()\n        };\n\n        // Because BitWarden want the status to be -1 for revoked users we need to catch that here.\n        // We subtract/add a number so we can restore/activate the user to it's previous state again.\n        let status = if self.status < MembershipStatus::Revoked as i32 {\n            MembershipStatus::Revoked as i32\n        } else {\n            self.status\n        };\n\n        json!({\n            \"id\": self.uuid,\n            \"userId\": self.user_uuid,\n\n            \"status\": status,\n            \"type\": self.atype,\n            \"accessAll\": self.access_all,\n            \"collections\": coll_uuids,\n\n            \"object\": \"organizationUserDetails\",\n        })\n    }\n\n    pub async fn to_json_mini_details(&self, conn: &DbConn) -> Value {\n        let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();\n\n        // Because Bitwarden wants the status to be -1 for revoked users we need to catch that here.\n        // We subtract/add a number so we can restore/activate the user to it's previous state again.\n        let status = if self.status < MembershipStatus::Revoked as i32 {\n            MembershipStatus::Revoked as i32\n        } else {\n            self.status\n        };\n\n        json!({\n            \"id\": self.uuid,\n            \"userId\": self.user_uuid,\n            \"type\": self.type_manager_as_custom(), // HACK: Convert the manager type to a custom type\n            \"status\": status,\n            \"name\": user.name,\n            \"email\": user.email,\n            \"object\": \"organizationUserUserMiniDetails\",\n        })\n    }\n\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.user_uuid, conn).await;\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(users_organizations::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(users_organizations::table)\n                            .filter(users_organizations::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error adding user to organization\")\n                    },\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error adding user to organization\")\n            }\n            postgresql {\n                diesel::insert_into(users_organizations::table)\n                    .values(self)\n                    .on_conflict(users_organizations::uuid)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error adding user to organization\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        User::update_uuid_revision(&self.user_uuid, conn).await;\n\n        CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?;\n        GroupUser::delete_all_by_member(&self.uuid, conn).await?;\n\n        db_run! { conn: {\n            diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error removing user from organization\")\n        }}\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        for member in Self::find_by_org(org_uuid, conn).await {\n            member.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        for member in Self::find_any_state_by_user(user_uuid, conn).await {\n            member.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn find_by_email_and_org(email: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Membership> {\n        if let Some(user) = User::find_by_mail(email, conn).await {\n            if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await {\n                return Some(member);\n            }\n        }\n\n        None\n    }\n\n    pub fn has_status(&self, status: MembershipStatus) -> bool {\n        self.status == status as i32\n    }\n\n    pub fn has_type(&self, user_type: MembershipType) -> bool {\n        self.atype == user_type as i32\n    }\n\n    pub fn has_full_access(&self) -> bool {\n        (self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed)\n    }\n\n    pub async fn find_by_uuid(uuid: &MembershipId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_org(uuid: &MembershipId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::uuid.eq(uuid))\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn find_invited_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::status.eq(MembershipStatus::Invited as i32))\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    // Should be used only when email are disabled.\n    // In Organizations::send_invite status is set to Accepted only if the user has a password.\n    pub async fn accept_user_invitations(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::update(users_organizations::table)\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::status.eq(MembershipStatus::Invited as i32))\n                .set(users_organizations::status.eq(MembershipStatus::Accepted as i32))\n                .execute(conn)\n                .map_res(\"Error confirming invitations\")\n        }}\n    }\n\n    pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn count_accepted_and_confirmed_by_user(\n        user_uuid: &UserId,\n        excluded_org: &OrganizationId,\n        conn: &DbConn,\n    ) -> i64 {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::org_uuid.ne(excluded_org))\n                .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32).or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)))\n                .count()\n                .first::<i64>(conn)\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading user organizations\")\n        }}\n    }\n\n    pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    // Get all users which are either owner or admin, or a manager which can manage/access all\n    pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                .filter(\n                    users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32])\n                    .or(users_organizations::atype.eq(MembershipType::Manager as i32).and(users_organizations::access_all.eq(true)))\n                )\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .count()\n                .first::<i64>(conn)\n                .ok()\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_org_and_type(org_uuid: &OrganizationId, atype: MembershipType, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .filter(users_organizations::atype.eq(atype as i32))\n                .load::<Self>(conn)\n                .expect(\"Error loading user organizations\")\n        }}\n    }\n\n    pub async fn count_confirmed_by_org_and_type(\n        org_uuid: &OrganizationId,\n        atype: MembershipType,\n        conn: &DbConn,\n    ) -> i64 {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .filter(users_organizations::atype.eq(atype as i32))\n                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))\n                .count()\n                .first::<i64>(conn)\n                .unwrap_or(0)\n        }}\n    }\n\n    pub async fn find_by_user_and_org(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_confirmed_by_user_and_org(\n        user_uuid: &UserId,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::org_uuid.eq(org_uuid))\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading user organizations\")\n        }}\n    }\n\n    pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .select(users_organizations::org_uuid)\n                .load::<OrganizationId>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .inner_join(\n                    org_policies::table.on(\n                        org_policies::org_uuid.eq(users_organizations::org_uuid)\n                            .and(users_organizations::user_uuid.eq(user_uuid))\n                            .and(org_policies::atype.eq(policy_type as i32))\n                            .and(org_policies::enabled.eq(true)))\n                )\n                .filter(\n                    users_organizations::status.eq(MembershipStatus::Confirmed as i32)\n                )\n                .select(users_organizations::all_columns)\n                .load::<Self>(conn)\n                .unwrap_or_default()\n        }}\n    }\n\n    pub async fn find_by_cipher_and_org(cipher_uuid: &CipherId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n            .filter(users_organizations::org_uuid.eq(org_uuid))\n            .left_join(users_collections::table.on(\n                users_collections::user_uuid.eq(users_organizations::user_uuid)\n            ))\n            .left_join(ciphers_collections::table.on(\n                ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(\n                    ciphers_collections::cipher_uuid.eq(&cipher_uuid)\n                )\n            ))\n            .filter(\n                users_organizations::access_all.eq(true).or( // AccessAll..\n                    ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher\n                )\n            )\n            .select(users_organizations::all_columns)\n            .distinct()\n            .load::<Self>(conn)\n            .expect(\"Error loading user organizations\")\n        }}\n    }\n\n    pub async fn find_by_cipher_and_org_with_group(\n        cipher_uuid: &CipherId,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n            .filter(users_organizations::org_uuid.eq(org_uuid))\n            .inner_join(groups_users::table.on(\n                groups_users::users_organizations_uuid.eq(users_organizations::uuid)\n            ))\n            .left_join(collections_groups::table.on(\n                collections_groups::groups_uuid.eq(groups_users::groups_uuid)\n            ))\n            .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))\n            .left_join(ciphers_collections::table.on(\n                    ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))\n\n                ))\n            .filter(\n                    groups::access_all.eq(true).or( // AccessAll via groups\n                        ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection via group\n                    )\n                )\n                .select(users_organizations::all_columns)\n                .distinct()\n            .load::<Self>(conn)\n            .expect(\"Error loading user organizations with groups\")\n        }}\n    }\n\n    pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> bool {\n        db_run! { conn: {\n            users_organizations::table\n            .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))))\n            .filter(users_organizations::user_uuid.eq(user_uuid))\n            .filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]))\n            .count()\n            .first::<i64>(conn)\n            .ok()\n            .unwrap_or(0) != 0\n        }}\n    }\n\n    pub async fn find_by_collection_and_org(\n        collection_uuid: &CollectionId,\n        org_uuid: &OrganizationId,\n        conn: &DbConn,\n    ) -> Vec<Self> {\n        db_run! { conn: {\n            users_organizations::table\n            .filter(users_organizations::org_uuid.eq(org_uuid))\n            .left_join(users_collections::table.on(\n                users_collections::user_uuid.eq(users_organizations::user_uuid)\n            ))\n            .filter(\n                users_organizations::access_all.eq(true).or( // AccessAll..\n                    users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher\n                )\n            )\n            .select(users_organizations::all_columns)\n            .load::<Self>(conn)\n            .expect(\"Error loading user organizations\")\n        }}\n    }\n\n    pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n            .filter(\n                users_organizations::external_id.eq(ext_id)\n                .and(users_organizations::org_uuid.eq(org_uuid))\n            )\n            .first::<Self>(conn)\n            .ok()\n        }}\n    }\n\n    pub async fn find_main_user_org(user_uuid: &str, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users_organizations::table\n                .filter(users_organizations::user_uuid.eq(user_uuid))\n                .filter(users_organizations::status.ne(MembershipStatus::Revoked as i32))\n                .order(users_organizations::atype.asc())\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n}\n\nimpl OrganizationApiKey {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(organization_api_key::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(organization_api_key::table)\n                            .filter(organization_api_key::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving organization\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving organization\")\n\n            }\n            postgresql {\n                diesel::insert_into(organization_api_key::table)\n                    .values(self)\n                    .on_conflict((organization_api_key::uuid, organization_api_key::org_uuid))\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving organization\")\n            }\n        }\n    }\n\n    pub async fn find_by_org_uuid(org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            organization_api_key::table\n                .filter(organization_api_key::org_uuid.eq(org_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid)))\n                .execute(conn)\n                .map_res(\"Error removing organization api key from organization\")\n        }}\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    AsRef,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\n#[deref(forward)]\n#[from(forward)]\npub struct OrganizationId(String);\n\n#[derive(\n    Clone,\n    Debug,\n    Deref,\n    DieselNewType,\n    Display,\n    From,\n    FromForm,\n    Hash,\n    PartialEq,\n    Eq,\n    Serialize,\n    Deserialize,\n    UuidFromParam,\n)]\npub struct MembershipId(String);\n\n#[derive(Clone, Debug, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]\npub struct OrgApiKeyId(String);\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn partial_cmp_MembershipType() {\n        assert!(MembershipType::Owner > MembershipType::Admin);\n        assert!(MembershipType::Admin > MembershipType::Manager);\n        assert!(MembershipType::Manager > MembershipType::User);\n        assert!(MembershipType::Manager == MembershipType::from_str(\"4\").unwrap());\n    }\n}\n"
  },
  {
    "path": "src/db/models/send.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\nuse serde_json::Value;\n\nuse crate::{config::PathType, util::LowerCase, CONFIG};\n\nuse super::{OrganizationId, User, UserId};\nuse crate::db::schema::sends;\nuse diesel::prelude::*;\nuse id::SendId;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = sends)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct Send {\n    pub uuid: SendId,\n\n    pub user_uuid: Option<UserId>,\n    pub organization_uuid: Option<OrganizationId>,\n\n    pub name: String,\n    pub notes: Option<String>,\n\n    pub atype: i32,\n    pub data: String,\n    pub akey: String,\n    pub password_hash: Option<Vec<u8>>,\n    password_salt: Option<Vec<u8>>,\n    password_iter: Option<i32>,\n\n    pub max_access_count: Option<i32>,\n    pub access_count: i32,\n\n    pub creation_date: NaiveDateTime,\n    pub revision_date: NaiveDateTime,\n    pub expiration_date: Option<NaiveDateTime>,\n    pub deletion_date: NaiveDateTime,\n\n    pub disabled: bool,\n    pub hide_email: Option<bool>,\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]\npub enum SendType {\n    Text = 0,\n    File = 1,\n}\n\nimpl Send {\n    pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {\n        let now = Utc::now().naive_utc();\n\n        Self {\n            uuid: SendId::from(crate::util::get_uuid()),\n            user_uuid: None,\n            organization_uuid: None,\n\n            name,\n            notes: None,\n\n            atype,\n            data,\n            akey,\n            password_hash: None,\n            password_salt: None,\n            password_iter: None,\n\n            max_access_count: None,\n            access_count: 0,\n\n            creation_date: now,\n            revision_date: now,\n            expiration_date: None,\n            deletion_date,\n\n            disabled: false,\n            hide_email: None,\n        }\n    }\n\n    pub fn set_password(&mut self, password: Option<&str>) {\n        const PASSWORD_ITER: i32 = 100_000;\n\n        if let Some(password) = password {\n            self.password_iter = Some(PASSWORD_ITER);\n            let salt = crate::crypto::get_random_bytes::<64>().to_vec();\n            let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32);\n            self.password_salt = Some(salt);\n            self.password_hash = Some(hash);\n        } else {\n            self.password_iter = None;\n            self.password_salt = None;\n            self.password_hash = None;\n        }\n    }\n\n    pub fn check_password(&self, password: &str) -> bool {\n        match (&self.password_hash, &self.password_salt, self.password_iter) {\n            (Some(hash), Some(salt), Some(iter)) => {\n                crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32)\n            }\n            _ => false,\n        }\n    }\n\n    pub async fn creator_identifier(&self, conn: &DbConn) -> Option<String> {\n        if let Some(hide_email) = self.hide_email {\n            if hide_email {\n                return None;\n            }\n        }\n\n        if let Some(user_uuid) = &self.user_uuid {\n            if let Some(user) = User::find_by_uuid(user_uuid, conn).await {\n                return Some(user.email);\n            }\n        }\n\n        None\n    }\n\n    pub fn to_json(&self) -> Value {\n        use crate::util::format_date;\n        use data_encoding::BASE64URL_NOPAD;\n        use uuid::Uuid;\n\n        let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();\n\n        // Mobile clients expect size to be a string instead of a number\n        if let Some(size) = data.get(\"size\").and_then(|v| v.as_i64()) {\n            data[\"size\"] = Value::String(size.to_string());\n        }\n\n        json!({\n            \"id\": self.uuid,\n            \"accessId\": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),\n            \"type\": self.atype,\n\n            \"name\": self.name,\n            \"notes\": self.notes,\n            \"text\": if self.atype == SendType::Text as i32 { Some(&data) } else { None },\n            \"file\": if self.atype == SendType::File as i32 { Some(&data) } else { None },\n\n            \"key\": self.akey,\n            \"maxAccessCount\": self.max_access_count,\n            \"accessCount\": self.access_count,\n            \"password\": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),\n            \"disabled\": self.disabled,\n            \"hideEmail\": self.hide_email,\n\n            \"revisionDate\": format_date(&self.revision_date),\n            \"expirationDate\": self.expiration_date.as_ref().map(format_date),\n            \"deletionDate\": format_date(&self.deletion_date),\n            \"object\": \"send\",\n        })\n    }\n\n    pub async fn to_json_access(&self, conn: &DbConn) -> Value {\n        use crate::util::format_date;\n\n        let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();\n\n        // Mobile clients expect size to be a string instead of a number\n        if let Some(size) = data.get(\"size\").and_then(|v| v.as_i64()) {\n            data[\"size\"] = Value::String(size.to_string());\n        }\n\n        json!({\n            \"id\": self.uuid,\n            \"type\": self.atype,\n\n            \"name\": self.name,\n            \"text\": if self.atype == SendType::Text as i32 { Some(&data) } else { None },\n            \"file\": if self.atype == SendType::File as i32 { Some(&data) } else { None },\n\n            \"expirationDate\": self.expiration_date.as_ref().map(format_date),\n            \"creatorIdentifier\": self.creator_identifier(conn).await,\n            \"object\": \"send-access\",\n        })\n    }\n}\n\nuse crate::db::DbConn;\n\nuse crate::api::EmptyResult;\nuse crate::error::MapResult;\nuse crate::util::NumberOrString;\n\nimpl Send {\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n        self.revision_date = Utc::now().naive_utc();\n\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(sends::table)\n                    .values(&*self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(sends::table)\n                            .filter(sends::uuid.eq(&self.uuid))\n                            .set(&*self)\n                            .execute(conn)\n                            .map_res(\"Error saving send\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving send\")\n            }\n            postgresql {\n                diesel::insert_into(sends::table)\n                    .values(&*self)\n                    .on_conflict(sends::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving send\")\n            }\n        }\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        self.update_users_revision(conn).await;\n\n        if self.atype == SendType::File as i32 {\n            let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;\n            operator.remove_all(&self.uuid).await.ok();\n        }\n\n        db_run! { conn: {\n            diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting send\")\n        }}\n    }\n\n    /// Purge all sends that are past their deletion date.\n    pub async fn purge(conn: &DbConn) {\n        for send in Self::find_by_past_deletion_date(conn).await {\n            send.delete(conn).await.ok();\n        }\n    }\n\n    pub async fn update_users_revision(&self, conn: &DbConn) -> Vec<UserId> {\n        let mut user_uuids = Vec::new();\n        match &self.user_uuid {\n            Some(user_uuid) => {\n                User::update_uuid_revision(user_uuid, conn).await;\n                user_uuids.push(user_uuid.clone())\n            }\n            None => {\n                // Belongs to Organization, not implemented\n            }\n        };\n        user_uuids\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        for send in Self::find_by_user(user_uuid, conn).await {\n            send.delete(conn).await?;\n        }\n        Ok(())\n    }\n\n    pub async fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {\n        use data_encoding::BASE64URL_NOPAD;\n        use uuid::Uuid;\n\n        let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else {\n            return None;\n        };\n\n        let uuid = match Uuid::from_slice(&uuid_vec) {\n            Ok(u) => SendId::from(u.to_string()),\n            Err(_) => return None,\n        };\n\n        Self::find_by_uuid(&uuid, conn).await\n    }\n\n    pub async fn find_by_uuid(uuid: &SendId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            sends::table\n                .filter(sends::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            sends::table\n                .filter(sends::uuid.eq(uuid))\n                .filter(sends::user_uuid.eq(user_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            sends::table\n                .filter(sends::user_uuid.eq(user_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading sends\")\n        }}\n    }\n\n    pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<i64> {\n        let sends = Self::find_by_user(user_uuid, conn).await;\n\n        #[derive(serde::Deserialize)]\n        struct FileData {\n            #[serde(rename = \"size\", alias = \"Size\")]\n            size: NumberOrString,\n        }\n\n        let mut total: i64 = 0;\n        for send in sends {\n            if send.atype == SendType::File as i32 {\n                if let Ok(size) =\n                    serde_json::from_str::<FileData>(&send.data).map_err(Into::into).and_then(|d| d.size.into_i64())\n                {\n                    total = total.checked_add(size)?;\n                };\n            }\n        }\n\n        Some(total)\n    }\n\n    pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            sends::table\n                .filter(sends::organization_uuid.eq(org_uuid))\n                .load::<Self>(conn)\n                .expect(\"Error loading sends\")\n        }}\n    }\n\n    pub async fn find_by_past_deletion_date(conn: &DbConn) -> Vec<Self> {\n        let now = Utc::now().naive_utc();\n        db_run! { conn: {\n            sends::table\n                .filter(sends::deletion_date.lt(now))\n                .load::<Self>(conn)\n                .expect(\"Error loading sends\")\n        }}\n    }\n}\n\n// separate namespace to avoid name collision with std::marker::Send\npub mod id {\n    use derive_more::{AsRef, Deref, Display, From};\n    use macros::{IdFromParam, UuidFromParam};\n    use std::marker::Send;\n    use std::path::Path;\n\n    #[derive(\n        Clone,\n        Debug,\n        AsRef,\n        Deref,\n        DieselNewType,\n        Display,\n        From,\n        FromForm,\n        Hash,\n        PartialEq,\n        Eq,\n        Serialize,\n        Deserialize,\n        UuidFromParam,\n    )]\n    pub struct SendId(String);\n\n    impl AsRef<Path> for SendId {\n        #[inline]\n        fn as_ref(&self) -> &Path {\n            Path::new(&self.0)\n        }\n    }\n\n    #[derive(\n        Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,\n    )]\n    pub struct SendFileId(String);\n\n    impl AsRef<Path> for SendFileId {\n        #[inline]\n        fn as_ref(&self) -> &Path {\n            Path::new(&self.0)\n        }\n    }\n}\n"
  },
  {
    "path": "src/db/models/sso_auth.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\nuse std::time::Duration;\n\nuse crate::api::EmptyResult;\nuse crate::db::schema::sso_auth;\nuse crate::db::{DbConn, DbPool};\nuse crate::error::MapResult;\nuse crate::sso::{OIDCCode, OIDCCodeChallenge, OIDCIdentifier, OIDCState, SSO_AUTH_EXPIRATION};\n\nuse diesel::deserialize::FromSql;\nuse diesel::expression::AsExpression;\nuse diesel::prelude::*;\nuse diesel::serialize::{Output, ToSql};\nuse diesel::sql_types::Text;\n\n#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]\n#[diesel(sql_type = Text)]\npub enum OIDCCodeWrapper {\n    Ok {\n        code: OIDCCode,\n    },\n    Error {\n        error: String,\n        error_description: Option<String>,\n    },\n}\n\nimpl_FromToSqlText!(OIDCCodeWrapper);\n\n#[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)]\n#[diesel(sql_type = Text)]\npub struct OIDCAuthenticatedUser {\n    pub refresh_token: Option<String>,\n    pub access_token: String,\n    pub expires_in: Option<Duration>,\n    pub identifier: OIDCIdentifier,\n    pub email: String,\n    pub email_verified: Option<bool>,\n    pub user_name: Option<String>,\n}\n\nimpl_FromToSqlText!(OIDCAuthenticatedUser);\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]\n#[diesel(table_name = sso_auth)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(state))]\npub struct SsoAuth {\n    pub state: OIDCState,\n    pub client_challenge: OIDCCodeChallenge,\n    pub nonce: String,\n    pub redirect_uri: String,\n    pub code_response: Option<OIDCCodeWrapper>,\n    pub auth_response: Option<OIDCAuthenticatedUser>,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n}\n\n/// Local methods\nimpl SsoAuth {\n    pub fn new(state: OIDCState, client_challenge: OIDCCodeChallenge, nonce: String, redirect_uri: String) -> Self {\n        let now = Utc::now().naive_utc();\n\n        SsoAuth {\n            state,\n            client_challenge,\n            nonce,\n            redirect_uri,\n            created_at: now,\n            updated_at: now,\n            code_response: None,\n            auth_response: None,\n        }\n    }\n}\n\n/// Database methods\nimpl SsoAuth {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            mysql {\n                diesel::insert_into(sso_auth::table)\n                    .values(self)\n                    .on_conflict(diesel::dsl::DuplicatedKeys)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving SSO auth\")\n            }\n            postgresql, sqlite {\n                diesel::insert_into(sso_auth::table)\n                    .values(self)\n                    .on_conflict(sso_auth::state)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving SSO auth\")\n            }\n        }\n    }\n\n    pub async fn find(state: &OIDCState, conn: &DbConn) -> Option<Self> {\n        let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION;\n        db_run! { conn: {\n            sso_auth::table\n                .filter(sso_auth::state.eq(state))\n                .filter(sso_auth::created_at.ge(oldest))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! {conn: {\n            diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state)))\n                .execute(conn)\n                .map_res(\"Error deleting sso_auth\")\n        }}\n    }\n\n    pub async fn delete_expired(pool: DbPool) -> EmptyResult {\n        debug!(\"Purging expired sso_auth\");\n        if let Ok(conn) = pool.get().await {\n            let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION;\n            db_run! { conn: {\n                diesel::delete(sso_auth::table.filter(sso_auth::created_at.lt(oldest)))\n                    .execute(conn)\n                    .map_res(\"Error deleting expired SSO nonce\")\n            }}\n        } else {\n            err!(\"Failed to get DB connection while purging expired sso_auth\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/db/models/two_factor.rs",
    "content": "use super::UserId;\nuse crate::api::core::two_factor::webauthn::WebauthnRegistration;\nuse crate::db::schema::twofactor;\nuse crate::{api::EmptyResult, db::DbConn, error::MapResult};\nuse diesel::prelude::*;\nuse serde_json::Value;\nuse webauthn_rs::prelude::{Credential, ParsedAttestation};\nuse webauthn_rs_core::proto::CredentialV3;\nuse webauthn_rs_proto::{AttestationFormat, RegisteredExtensions};\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = twofactor)]\n#[diesel(primary_key(uuid))]\npub struct TwoFactor {\n    pub uuid: TwoFactorId,\n    pub user_uuid: UserId,\n    pub atype: i32,\n    pub enabled: bool,\n    pub data: String,\n    pub last_used: i64,\n}\n\n#[allow(dead_code)]\n#[derive(num_derive::FromPrimitive)]\npub enum TwoFactorType {\n    Authenticator = 0,\n    Email = 1,\n    Duo = 2,\n    YubiKey = 3,\n    U2f = 4,\n    Remember = 5,\n    OrganizationDuo = 6,\n    Webauthn = 7,\n    RecoveryCode = 8,\n\n    // These are implementation details\n    U2fRegisterChallenge = 1000,\n    U2fLoginChallenge = 1001,\n    EmailVerificationChallenge = 1002,\n    WebauthnRegisterChallenge = 1003,\n    WebauthnLoginChallenge = 1004,\n\n    // Special type for Protected Actions verification via email\n    ProtectedActions = 2000,\n}\n\n/// Local methods\nimpl TwoFactor {\n    pub fn new(user_uuid: UserId, atype: TwoFactorType, data: String) -> Self {\n        Self {\n            uuid: TwoFactorId(crate::util::get_uuid()),\n            user_uuid,\n            atype: atype as i32,\n            enabled: true,\n            data,\n            last_used: 0,\n        }\n    }\n\n    pub fn to_json(&self) -> Value {\n        json!({\n            \"enabled\": self.enabled,\n            \"key\": \"\", // This key and value vary\n            \"Oobject\": \"twoFactorAuthenticator\" // This value varies\n        })\n    }\n\n    pub fn to_json_provider(&self) -> Value {\n        json!({\n            \"enabled\": self.enabled,\n            \"type\": self.atype,\n            \"object\": \"twoFactorProvider\"\n        })\n    }\n}\n\n/// Database methods\nimpl TwoFactor {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                match diesel::replace_into(twofactor::table)\n                    .values(self)\n                    .execute(conn)\n                {\n                    Ok(_) => Ok(()),\n                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.\n                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {\n                        diesel::update(twofactor::table)\n                            .filter(twofactor::uuid.eq(&self.uuid))\n                            .set(self)\n                            .execute(conn)\n                            .map_res(\"Error saving twofactor\")\n                    }\n                    Err(e) => Err(e.into()),\n                }.map_res(\"Error saving twofactor\")\n            }\n            postgresql {\n                // We need to make sure we're not going to violate the unique constraint on user_uuid and atype.\n                // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does\n                // not support multiple constraints on ON CONFLICT clauses.\n                let _: () = diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))\n                    .execute(conn)\n                    .map_res(\"Error deleting twofactor for insert\")?;\n\n                diesel::insert_into(twofactor::table)\n                    .values(self)\n                    .on_conflict(twofactor::uuid)\n                    .do_update()\n                    .set(self)\n                    .execute(conn)\n                    .map_res(\"Error saving twofactor\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting twofactor\")\n        }}\n    }\n\n    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            twofactor::table\n                .filter(twofactor::user_uuid.eq(user_uuid))\n                .filter(twofactor::atype.lt(1000)) // Filter implementation types\n                .load::<Self>(conn)\n                .expect(\"Error loading twofactor\")\n        }}\n    }\n\n    pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            twofactor::table\n                .filter(twofactor::user_uuid.eq(user_uuid))\n                .filter(twofactor::atype.eq(atype))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting twofactors\")\n        }}\n    }\n\n    pub async fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult {\n        let u2f_factors = db_run! { conn: {\n            twofactor::table\n                .filter(twofactor::atype.eq(TwoFactorType::U2f as i32))\n                .load::<Self>(conn)\n                .expect(\"Error loading twofactor\")\n        }};\n\n        use crate::api::core::two_factor::webauthn::U2FRegistration;\n        use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};\n        use webauthn_rs::prelude::{COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve};\n        use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy};\n\n        for mut u2f in u2f_factors {\n            let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;\n            // If there are no registrations or they are migrated (we do the migration in batch so we can consider them all migrated when the first one is)\n            if regs.is_empty() || regs[0].migrated == Some(true) {\n                continue;\n            }\n\n            let (_, mut webauthn_regs) = get_webauthn_registrations(&u2f.user_uuid, conn).await?;\n\n            // If the user already has webauthn registrations saved, don't overwrite them\n            if !webauthn_regs.is_empty() {\n                continue;\n            }\n\n            for reg in &mut regs {\n                let x: [u8; 32] = reg.reg.pub_key[1..33].try_into().unwrap();\n                let y: [u8; 32] = reg.reg.pub_key[33..65].try_into().unwrap();\n\n                let key = COSEKey {\n                    type_: COSEAlgorithm::ES256,\n                    key: COSEKeyType::EC_EC2(COSEEC2Key {\n                        curve: ECDSACurve::SECP256R1,\n                        x: x.into(),\n                        y: y.into(),\n                    }),\n                };\n\n                let new_reg = WebauthnRegistration {\n                    id: reg.id,\n                    migrated: true,\n                    name: reg.name.clone(),\n                    credential: Credential {\n                        counter: reg.counter,\n                        user_verified: false,\n                        cred: key,\n                        cred_id: reg.reg.key_handle.clone().into(),\n                        registration_policy: UserVerificationPolicy::Discouraged_DO_NOT_USE,\n\n                        transports: None,\n                        backup_eligible: false,\n                        backup_state: false,\n                        extensions: RegisteredExtensions::none(),\n                        attestation: ParsedAttestation::default(),\n                        attestation_format: AttestationFormat::None,\n                    }\n                    .into(),\n                };\n\n                webauthn_regs.push(new_reg);\n\n                reg.migrated = Some(true);\n            }\n\n            u2f.data = serde_json::to_string(&regs)?;\n            u2f.save(conn).await?;\n\n            TwoFactor::new(u2f.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&webauthn_regs)?)\n                .save(conn)\n                .await?;\n        }\n\n        Ok(())\n    }\n\n    pub async fn migrate_credential_to_passkey(conn: &DbConn) -> EmptyResult {\n        let webauthn_factors = db_run! { conn: {\n            twofactor::table\n                .filter(twofactor::atype.eq(TwoFactorType::Webauthn as i32))\n                .load::<Self>(conn)\n                .expect(\"Error loading twofactor\")\n        }};\n\n        for webauthn_factor in webauthn_factors {\n            // assume that a failure to parse into the old struct, means that it was already converted\n            // alternatively this could also be checked via an extra field in the db\n            let Ok(regs) = serde_json::from_str::<Vec<WebauthnRegistrationV3>>(&webauthn_factor.data) else {\n                continue;\n            };\n\n            let regs = regs.into_iter().map(|r| r.into()).collect::<Vec<WebauthnRegistration>>();\n\n            TwoFactor::new(webauthn_factor.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&regs)?)\n                .save(conn)\n                .await?;\n        }\n\n        Ok(())\n    }\n}\n\n#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct TwoFactorId(String);\n\n#[derive(Deserialize)]\npub struct WebauthnRegistrationV3 {\n    pub id: i32,\n    pub name: String,\n    pub migrated: bool,\n    pub credential: CredentialV3,\n}\n\nimpl From<WebauthnRegistrationV3> for WebauthnRegistration {\n    fn from(value: WebauthnRegistrationV3) -> Self {\n        Self {\n            id: value.id,\n            name: value.name,\n            migrated: value.migrated,\n            credential: Credential::from(value.credential).into(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/db/models/two_factor_duo_context.rs",
    "content": "use chrono::Utc;\n\nuse crate::db::schema::twofactor_duo_ctx;\nuse crate::{api::EmptyResult, db::DbConn, error::MapResult};\nuse diesel::prelude::*;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = twofactor_duo_ctx)]\n#[diesel(primary_key(state))]\npub struct TwoFactorDuoContext {\n    pub state: String,\n    pub user_email: String,\n    pub nonce: String,\n    pub exp: i64,\n}\n\nimpl TwoFactorDuoContext {\n    pub async fn find_by_state(state: &str, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            twofactor_duo_ctx::table\n                .filter(twofactor_duo_ctx::state.eq(state))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &DbConn) -> EmptyResult {\n        // A saved context should never be changed, only created or deleted.\n        let exists = Self::find_by_state(state, conn).await;\n        if exists.is_some() {\n            return Ok(());\n        };\n\n        let exp = Utc::now().timestamp() + ttl;\n\n        db_run! { conn: {\n            diesel::insert_into(twofactor_duo_ctx::table)\n                .values((\n                    twofactor_duo_ctx::state.eq(state),\n                    twofactor_duo_ctx::user_email.eq(user_email),\n                    twofactor_duo_ctx::nonce.eq(nonce),\n                    twofactor_duo_ctx::exp.eq(exp)\n            ))\n            .execute(conn)\n            .map_res(\"Error saving context to twofactor_duo_ctx\")\n        }}\n    }\n\n    pub async fn find_expired(conn: &DbConn) -> Vec<Self> {\n        let now = Utc::now().timestamp();\n        db_run! { conn: {\n            twofactor_duo_ctx::table\n                .filter(twofactor_duo_ctx::exp.lt(now))\n                .load::<Self>(conn)\n                .expect(\"Error finding expired contexts in twofactor_duo_ctx\")\n        }}\n    }\n\n    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(\n                twofactor_duo_ctx::table\n                .filter(twofactor_duo_ctx::state.eq(&self.state)))\n                .execute(conn)\n                .map_res(\"Error deleting from twofactor_duo_ctx\")\n        }}\n    }\n\n    pub async fn purge_expired_duo_contexts(conn: &DbConn) {\n        for context in Self::find_expired(conn).await {\n            context.delete(conn).await.ok();\n        }\n    }\n}\n"
  },
  {
    "path": "src/db/models/two_factor_incomplete.rs",
    "content": "use chrono::{NaiveDateTime, Utc};\n\nuse crate::db::schema::twofactor_incomplete;\nuse crate::{\n    api::EmptyResult,\n    auth::ClientIp,\n    db::{\n        models::{DeviceId, UserId},\n        DbConn,\n    },\n    error::MapResult,\n    CONFIG,\n};\nuse diesel::prelude::*;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset)]\n#[diesel(table_name = twofactor_incomplete)]\n#[diesel(primary_key(user_uuid, device_uuid))]\npub struct TwoFactorIncomplete {\n    pub user_uuid: UserId,\n    // This device UUID is simply what's claimed by the device. It doesn't\n    // necessarily correspond to any UUID in the devices table, since a device\n    // must complete 2FA login before being added into the devices table.\n    pub device_uuid: DeviceId,\n    pub device_name: String,\n    pub device_type: i32,\n    pub login_time: NaiveDateTime,\n    pub ip_address: String,\n}\n\nimpl TwoFactorIncomplete {\n    pub async fn mark_incomplete(\n        user_uuid: &UserId,\n        device_uuid: &DeviceId,\n        device_name: &str,\n        device_type: i32,\n        ip: &ClientIp,\n        conn: &DbConn,\n    ) -> EmptyResult {\n        if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {\n            return Ok(());\n        }\n\n        // Don't update the data for an existing user/device pair, since that\n        // would allow an attacker to arbitrarily delay notifications by\n        // sending repeated 2FA attempts to reset the timer.\n        let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn).await;\n        if existing.is_some() {\n            return Ok(());\n        }\n\n        db_run! { conn: {\n            diesel::insert_into(twofactor_incomplete::table)\n                .values((\n                    twofactor_incomplete::user_uuid.eq(user_uuid),\n                    twofactor_incomplete::device_uuid.eq(device_uuid),\n                    twofactor_incomplete::device_name.eq(device_name),\n                    twofactor_incomplete::device_type.eq(device_type),\n                    twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),\n                    twofactor_incomplete::ip_address.eq(ip.ip.to_string()),\n                ))\n                .execute(conn)\n                .map_res(\"Error adding twofactor_incomplete record\")\n        }}\n    }\n\n    pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {\n        if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {\n            return Ok(());\n        }\n\n        Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await\n    }\n\n    pub async fn find_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            twofactor_incomplete::table\n                .filter(twofactor_incomplete::user_uuid.eq(user_uuid))\n                .filter(twofactor_incomplete::device_uuid.eq(device_uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> {\n        db_run! { conn: {\n            twofactor_incomplete::table\n                .filter(twofactor_incomplete::login_time.lt(dt))\n                .load::<Self>(conn)\n                .expect(\"Error loading twofactor_incomplete\")\n        }}\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await\n    }\n\n    pub async fn delete_by_user_and_device(user_uuid: &UserId, device_uuid: &DeviceId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(twofactor_incomplete::table\n                           .filter(twofactor_incomplete::user_uuid.eq(user_uuid))\n                           .filter(twofactor_incomplete::device_uuid.eq(device_uuid)))\n                .execute(conn)\n                .map_res(\"Error in twofactor_incomplete::delete_by_user_and_device()\")\n        }}\n    }\n\n    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))\n                .execute(conn)\n                .map_res(\"Error in twofactor_incomplete::delete_all_by_user()\")\n        }}\n    }\n}\n"
  },
  {
    "path": "src/db/models/user.rs",
    "content": "use crate::db::schema::{invitations, sso_users, twofactor_incomplete, users};\nuse chrono::{NaiveDateTime, TimeDelta, Utc};\nuse derive_more::{AsRef, Deref, Display, From};\nuse diesel::prelude::*;\nuse serde_json::Value;\n\nuse super::{\n    Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete,\n};\nuse crate::{\n    api::EmptyResult,\n    crypto,\n    db::{models::DeviceId, DbConn},\n    error::MapResult,\n    sso::OIDCIdentifier,\n    util::{format_date, get_uuid, retry},\n    CONFIG,\n};\nuse macros::UuidFromParam;\n\n#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]\n#[diesel(table_name = users)]\n#[diesel(treat_none_as_null = true)]\n#[diesel(primary_key(uuid))]\npub struct User {\n    pub uuid: UserId,\n    pub enabled: bool,\n    pub created_at: NaiveDateTime,\n    pub updated_at: NaiveDateTime,\n    pub verified_at: Option<NaiveDateTime>,\n    pub last_verifying_at: Option<NaiveDateTime>,\n    pub login_verify_count: i32,\n\n    pub email: String,\n    pub email_new: Option<String>,\n    pub email_new_token: Option<String>,\n    pub name: String,\n\n    pub password_hash: Vec<u8>,\n    pub salt: Vec<u8>,\n    pub password_iterations: i32,\n    pub password_hint: Option<String>,\n\n    pub akey: String,\n    pub private_key: Option<String>,\n    pub public_key: Option<String>,\n\n    #[diesel(column_name = \"totp_secret\")] // Note, this is only added to the UserDb structs, not to User\n    _totp_secret: Option<String>,\n    pub totp_recover: Option<String>,\n\n    pub security_stamp: String,\n    pub stamp_exception: Option<String>,\n\n    pub equivalent_domains: String,\n    pub excluded_globals: String,\n\n    pub client_kdf_type: i32,\n    pub client_kdf_iter: i32,\n    pub client_kdf_memory: Option<i32>,\n    pub client_kdf_parallelism: Option<i32>,\n\n    pub api_key: Option<String>,\n\n    pub avatar_color: Option<String>,\n\n    pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.\n}\n\n#[derive(Identifiable, Queryable, Insertable)]\n#[diesel(table_name = invitations)]\n#[diesel(primary_key(email))]\npub struct Invitation {\n    pub email: String,\n}\n\n#[derive(Identifiable, Queryable, Insertable, Selectable)]\n#[diesel(table_name = sso_users)]\n#[diesel(primary_key(user_uuid))]\npub struct SsoUser {\n    pub user_uuid: UserId,\n    pub identifier: OIDCIdentifier,\n}\n\npub enum UserKdfType {\n    Pbkdf2 = 0,\n    Argon2id = 1,\n}\n\nenum UserStatus {\n    Enabled = 0,\n    Invited = 1,\n    _Disabled = 2,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct UserStampException {\n    pub routes: Vec<String>,\n    pub security_stamp: String,\n    pub expire: i64,\n}\n\n/// Local methods\nimpl User {\n    pub const CLIENT_KDF_TYPE_DEFAULT: i32 = UserKdfType::Pbkdf2 as i32;\n    pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000;\n\n    pub fn new(email: &str, name: Option<String>) -> Self {\n        let now = Utc::now().naive_utc();\n        let email = email.to_lowercase();\n\n        Self {\n            uuid: UserId(get_uuid()),\n            enabled: true,\n            created_at: now,\n            updated_at: now,\n            verified_at: None,\n            last_verifying_at: None,\n            login_verify_count: 0,\n            name: name.unwrap_or(email.clone()),\n            email,\n            akey: String::new(),\n            email_new: None,\n            email_new_token: None,\n\n            password_hash: Vec::new(),\n            salt: crypto::get_random_bytes::<64>().to_vec(),\n            password_iterations: CONFIG.password_iterations(),\n\n            security_stamp: get_uuid(),\n            stamp_exception: None,\n\n            password_hint: None,\n            private_key: None,\n            public_key: None,\n\n            _totp_secret: None,\n            totp_recover: None,\n\n            equivalent_domains: \"[]\".to_string(),\n            excluded_globals: \"[]\".to_string(),\n\n            client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT,\n            client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT,\n            client_kdf_memory: None,\n            client_kdf_parallelism: None,\n\n            api_key: None,\n\n            avatar_color: None,\n\n            external_id: None, // Todo: Needs to be removed in the future, this is not used anymore.\n        }\n    }\n\n    pub fn check_valid_password(&self, password: &str) -> bool {\n        crypto::verify_password_hash(\n            password.as_bytes(),\n            &self.salt,\n            &self.password_hash,\n            self.password_iterations as u32,\n        )\n    }\n\n    pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {\n        if let Some(ref totp_recover) = self.totp_recover {\n            crypto::ct_eq(recovery_code, totp_recover.to_lowercase())\n        } else {\n            false\n        }\n    }\n\n    pub fn check_valid_api_key(&self, key: &str) -> bool {\n        matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key))\n    }\n\n    /// Set the password hash generated\n    /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.\n    ///\n    /// # Arguments\n    ///\n    /// * `password` - A str which contains a hashed version of the users master password.\n    /// * `new_key` - A String  which contains the new aKey value of the users master password.\n    /// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.\n    ///   These routes are able to use the previous stamp id for the next 2 minutes.\n    ///   After these 2 minutes this stamp will expire.\n    ///\n    pub fn set_password(\n        &mut self,\n        password: &str,\n        new_key: Option<String>,\n        reset_security_stamp: bool,\n        allow_next_route: Option<Vec<String>>,\n    ) {\n        self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);\n\n        if let Some(route) = allow_next_route {\n            self.set_stamp_exception(route);\n        }\n\n        if let Some(new_key) = new_key {\n            self.akey = new_key;\n        }\n\n        if reset_security_stamp {\n            self.reset_security_stamp()\n        }\n    }\n\n    pub fn reset_security_stamp(&mut self) {\n        self.security_stamp = get_uuid();\n    }\n\n    /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.\n    ///\n    /// # Arguments\n    /// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.\n    ///   These routes are able to use the previous stamp id for the next 2 minutes.\n    ///   After these 2 minutes this stamp will expire.\n    ///\n    pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {\n        let stamp_exception = UserStampException {\n            routes: route_exception,\n            security_stamp: self.security_stamp.clone(),\n            expire: (Utc::now() + TimeDelta::try_minutes(2).unwrap()).timestamp(),\n        };\n        self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());\n    }\n\n    /// Resets the stamp_exception to prevent re-use of the previous security-stamp\n    pub fn reset_stamp_exception(&mut self) {\n        self.stamp_exception = None;\n    }\n\n    pub fn display_name(&self) -> &str {\n        // default to email if name is empty\n        if !&self.name.is_empty() {\n            &self.name\n        } else {\n            &self.email\n        }\n    }\n}\n\n/// Database methods\nimpl User {\n    pub async fn to_json(&self, conn: &DbConn) -> Value {\n        let mut orgs_json = Vec::new();\n        for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {\n            orgs_json.push(c.to_json(conn).await);\n        }\n\n        let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).await.is_empty();\n\n        // TODO: Might want to save the status field in the DB\n        let status = if self.password_hash.is_empty() {\n            UserStatus::Invited\n        } else {\n            UserStatus::Enabled\n        };\n\n        json!({\n            \"_status\": status as i32,\n            \"id\": self.uuid,\n            \"name\": self.name,\n            \"email\": self.email,\n            \"emailVerified\": !CONFIG.mail_enabled() || self.verified_at.is_some(),\n            \"premium\": true,\n            \"premiumFromOrganization\": false,\n            \"culture\": \"en-US\",\n            \"twoFactorEnabled\": twofactor_enabled,\n            \"key\": self.akey,\n            \"privateKey\": self.private_key,\n            \"securityStamp\": self.security_stamp,\n            \"organizations\": orgs_json,\n            \"providers\": [],\n            \"providerOrganizations\": [],\n            \"forcePasswordReset\": false,\n            \"avatarColor\": self.avatar_color,\n            \"usesKeyConnector\": false,\n            \"creationDate\": format_date(&self.created_at),\n            \"object\": \"profile\",\n        })\n    }\n\n    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {\n        if !crate::util::is_valid_email(&self.email) {\n            err!(format!(\"User email {} is not a valid email address\", self.email))\n        }\n\n        self.updated_at = Utc::now().naive_utc();\n\n        db_run! { conn:\n            mysql {\n                diesel::insert_into(users::table)\n                    .values(&*self)\n                    .on_conflict(diesel::dsl::DuplicatedKeys)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving user\")\n            }\n            postgresql, sqlite {\n                diesel::insert_into(users::table) // Insert or update\n                    .values(&*self)\n                    .on_conflict(users::uuid)\n                    .do_update()\n                    .set(&*self)\n                    .execute(conn)\n                    .map_res(\"Error saving user\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        for member in Membership::find_confirmed_by_user(&self.uuid, conn).await {\n            if member.atype == MembershipType::Owner\n                && Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1\n            {\n                err!(\"Can't delete last owner\")\n            }\n        }\n\n        super::Send::delete_all_by_user(&self.uuid, conn).await?;\n        EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?;\n        EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?;\n        Membership::delete_all_by_user(&self.uuid, conn).await?;\n        Cipher::delete_all_by_user(&self.uuid, conn).await?;\n        Favorite::delete_all_by_user(&self.uuid, conn).await?;\n        Folder::delete_all_by_user(&self.uuid, conn).await?;\n        Device::delete_all_by_user(&self.uuid, conn).await?;\n        TwoFactor::delete_all_by_user(&self.uuid, conn).await?;\n        TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?;\n        Invitation::take(&self.email, conn).await; // Delete invitation if any\n\n        db_run! { conn: {\n            diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting user\")\n        }}\n    }\n\n    pub async fn update_uuid_revision(uuid: &UserId, conn: &DbConn) {\n        if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {\n            warn!(\"Failed to update revision for {uuid}: {e:#?}\");\n        }\n    }\n\n    pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult {\n        let updated_at = Utc::now().naive_utc();\n\n        db_run! { conn: {\n            retry(|| {\n                diesel::update(users::table)\n                    .set(users::updated_at.eq(updated_at))\n                    .execute(conn)\n            }, 10)\n            .map_res(\"Error updating revision date for all users\")\n        }}\n    }\n\n    pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {\n        self.updated_at = Utc::now().naive_utc();\n\n        Self::_update_revision(&self.uuid, &self.updated_at, conn).await\n    }\n\n    async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            retry(|| {\n                diesel::update(users::table.filter(users::uuid.eq(uuid)))\n                    .set(users::updated_at.eq(date))\n                    .execute(conn)\n            }, 10)\n            .map_res(\"Error updating user revision\")\n        }}\n    }\n\n    pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {\n        let lower_mail = mail.to_lowercase();\n        db_run! { conn: {\n            users::table\n                .filter(users::email.eq(lower_mail))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_uuid(uuid: &UserId, conn: &DbConn) -> Option<Self> {\n        db_run! { conn: {\n            users::table\n                .filter(users::uuid.eq(uuid))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_device_for_email2fa(device_uuid: &DeviceId, conn: &DbConn) -> Option<Self> {\n        if let Some(user_uuid) = db_run! ( conn: {\n            twofactor_incomplete::table\n                .filter(twofactor_incomplete::device_uuid.eq(device_uuid))\n                .order_by(twofactor_incomplete::login_time.desc())\n                .select(twofactor_incomplete::user_uuid)\n                .first::<UserId>(conn)\n                .ok()\n        }) {\n            return Self::find_by_uuid(&user_uuid, conn).await;\n        }\n        None\n    }\n\n    pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option<SsoUser>)> {\n        db_run! { conn: {\n            users::table\n                .left_join(sso_users::table)\n                .select(<(Self, Option<SsoUser>)>::as_select())\n                .load(conn)\n                .expect(\"Error loading groups for user\")\n                .into_iter()\n                .collect()\n        }}\n    }\n\n    pub async fn last_active(&self, conn: &DbConn) -> Option<NaiveDateTime> {\n        match Device::find_latest_active_by_user(&self.uuid, conn).await {\n            Some(device) => Some(device.updated_at),\n            None => None,\n        }\n    }\n}\n\nimpl Invitation {\n    pub fn new(email: &str) -> Self {\n        let email = email.to_lowercase();\n        Self {\n            email,\n        }\n    }\n\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        if !crate::util::is_valid_email(&self.email) {\n            err!(format!(\"Invitation email {} is not a valid email address\", self.email))\n        }\n\n        db_run! { conn:\n            sqlite, mysql {\n                // Not checking for ForeignKey Constraints here\n                // Table invitations does not have any ForeignKey Constraints.\n                diesel::replace_into(invitations::table)\n                    .values(self)\n                    .execute(conn)\n                    .map_res(\"Error saving invitation\")\n            }\n            postgresql {\n                diesel::insert_into(invitations::table)\n                    .values(self)\n                    .on_conflict(invitations::email)\n                    .do_nothing()\n                    .execute(conn)\n                    .map_res(\"Error saving invitation\")\n            }\n        }\n    }\n\n    pub async fn delete(self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))\n                .execute(conn)\n                .map_res(\"Error deleting invitation\")\n        }}\n    }\n\n    pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {\n        let lower_mail = mail.to_lowercase();\n        db_run! { conn: {\n            invitations::table\n                .filter(invitations::email.eq(lower_mail))\n                .first::<Self>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn take(mail: &str, conn: &DbConn) -> bool {\n        match Self::find_by_mail(mail, conn).await {\n            Some(invitation) => invitation.delete(conn).await.is_ok(),\n            None => false,\n        }\n    }\n}\n\n#[derive(\n    Clone,\n    Debug,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    From,\n    UuidFromParam,\n)]\n#[deref(forward)]\n#[from(forward)]\npub struct UserId(String);\n\nimpl SsoUser {\n    pub async fn save(&self, conn: &DbConn) -> EmptyResult {\n        db_run! { conn:\n            sqlite, mysql {\n                diesel::replace_into(sso_users::table)\n                    .values(self)\n                    .execute(conn)\n                    .map_res(\"Error saving SSO user\")\n            }\n            postgresql {\n                diesel::insert_into(sso_users::table)\n                    .values(self)\n                    .execute(conn)\n                    .map_res(\"Error saving SSO user\")\n            }\n        }\n    }\n\n    pub async fn find_by_identifier(identifier: &str, conn: &DbConn) -> Option<(User, Self)> {\n        db_run! { conn: {\n            users::table\n                .inner_join(sso_users::table)\n                .select(<(User, Self)>::as_select())\n                .filter(sso_users::identifier.eq(identifier))\n                .first::<(User, Self)>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<(User, Option<Self>)> {\n        let lower_mail = mail.to_lowercase();\n\n        db_run! { conn: {\n            users::table\n                .left_join(sso_users::table)\n                .select(<(User, Option<Self>)>::as_select())\n                .filter(users::email.eq(lower_mail))\n                .first::<(User, Option<Self>)>(conn)\n                .ok()\n        }}\n    }\n\n    pub async fn delete(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {\n        db_run! { conn: {\n            diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid)))\n                .execute(conn)\n                .map_res(\"Error deleting sso user\")\n        }}\n    }\n}\n"
  },
  {
    "path": "src/db/query_logger.rs",
    "content": "use diesel::connection::{Instrumentation, InstrumentationEvent};\nuse std::{cell::RefCell, collections::HashMap, time::Instant};\n\nthread_local! {\n    static QUERY_PERF_TRACKER: RefCell<HashMap<String, Instant>> = RefCell::new(HashMap::new());\n}\n\npub fn simple_logger() -> Option<Box<dyn Instrumentation>> {\n    Some(Box::new(|event: InstrumentationEvent<'_>| match event {\n        InstrumentationEvent::StartEstablishConnection {\n            url,\n            ..\n        } => {\n            debug!(\"Establishing connection: {url}\")\n        }\n        InstrumentationEvent::FinishEstablishConnection {\n            url,\n            error,\n            ..\n        } => {\n            if let Some(e) = error {\n                error!(\"Error during establishing a connection with {url}: {e:?}\")\n            } else {\n                debug!(\"Connection established: {url}\")\n            }\n        }\n        InstrumentationEvent::StartQuery {\n            query,\n            ..\n        } => {\n            let query_string = format!(\"{query:?}\");\n            let start = Instant::now();\n            QUERY_PERF_TRACKER.with_borrow_mut(|map| {\n                map.insert(query_string, start);\n            });\n        }\n        InstrumentationEvent::FinishQuery {\n            query,\n            ..\n        } => {\n            let query_string = format!(\"{query:?}\");\n            QUERY_PERF_TRACKER.with_borrow_mut(|map| {\n                if let Some(start) = map.remove(&query_string) {\n                    let duration = start.elapsed();\n                    if duration.as_secs() >= 5 {\n                        warn!(\"SLOW QUERY [{:.2}s]: {}\", duration.as_secs_f32(), query_string);\n                    } else if duration.as_secs() >= 1 {\n                        info!(\"SLOW QUERY [{:.2}s]: {}\", duration.as_secs_f32(), query_string);\n                    } else {\n                        debug!(\"QUERY [{:?}]: {}\", duration, query_string);\n                    }\n                }\n            });\n        }\n        _ => {}\n    }))\n}\n"
  },
  {
    "path": "src/db/schema.rs",
    "content": "table! {\n    attachments (id) {\n        id -> Text,\n        cipher_uuid -> Text,\n        file_name -> Text,\n        file_size -> BigInt,\n        akey -> Nullable<Text>,\n    }\n}\n\ntable! {\n    ciphers (uuid) {\n        uuid -> Text,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n        user_uuid -> Nullable<Text>,\n        organization_uuid -> Nullable<Text>,\n        key -> Nullable<Text>,\n        atype -> Integer,\n        name -> Text,\n        notes -> Nullable<Text>,\n        fields -> Nullable<Text>,\n        data -> Text,\n        password_history -> Nullable<Text>,\n        deleted_at -> Nullable<Timestamp>,\n        reprompt -> Nullable<Integer>,\n    }\n}\n\ntable! {\n    ciphers_collections (cipher_uuid, collection_uuid) {\n        cipher_uuid -> Text,\n        collection_uuid -> Text,\n    }\n}\n\ntable! {\n    collections (uuid) {\n        uuid -> Text,\n        org_uuid -> Text,\n        name -> Text,\n        external_id -> Nullable<Text>,\n    }\n}\n\ntable! {\n    devices (uuid, user_uuid) {\n        uuid -> Text,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n        user_uuid -> Text,\n        name -> Text,\n        atype -> Integer,\n        push_uuid -> Nullable<Text>,\n        push_token -> Nullable<Text>,\n        refresh_token -> Text,\n        twofactor_remember -> Nullable<Text>,\n    }\n}\n\ntable! {\n    event (uuid) {\n        uuid -> Text,\n        event_type -> Integer,\n        user_uuid -> Nullable<Text>,\n        org_uuid -> Nullable<Text>,\n        cipher_uuid -> Nullable<Text>,\n        collection_uuid -> Nullable<Text>,\n        group_uuid -> Nullable<Text>,\n        org_user_uuid -> Nullable<Text>,\n        act_user_uuid -> Nullable<Text>,\n        device_type -> Nullable<Integer>,\n        ip_address -> Nullable<Text>,\n        event_date -> Timestamp,\n        policy_uuid -> Nullable<Text>,\n        provider_uuid -> Nullable<Text>,\n        provider_user_uuid -> Nullable<Text>,\n        provider_org_uuid -> Nullable<Text>,\n    }\n}\n\ntable! {\n    favorites (user_uuid, cipher_uuid) {\n        user_uuid -> Text,\n        cipher_uuid -> Text,\n    }\n}\n\ntable! {\n    folders (uuid) {\n        uuid -> Text,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n        user_uuid -> Text,\n        name -> Text,\n    }\n}\n\ntable! {\n    folders_ciphers (cipher_uuid, folder_uuid) {\n        cipher_uuid -> Text,\n        folder_uuid -> Text,\n    }\n}\n\ntable! {\n    invitations (email) {\n        email -> Text,\n    }\n}\n\ntable! {\n    org_policies (uuid) {\n        uuid -> Text,\n        org_uuid -> Text,\n        atype -> Integer,\n        enabled -> Bool,\n        data -> Text,\n    }\n}\n\ntable! {\n    organizations (uuid) {\n        uuid -> Text,\n        name -> Text,\n        billing_email -> Text,\n        private_key -> Nullable<Text>,\n        public_key -> Nullable<Text>,\n    }\n}\n\ntable! {\n    sends (uuid) {\n        uuid -> Text,\n        user_uuid -> Nullable<Text>,\n        organization_uuid -> Nullable<Text>,\n        name -> Text,\n        notes -> Nullable<Text>,\n        atype -> Integer,\n        data -> Text,\n        akey -> Text,\n        password_hash -> Nullable<Binary>,\n        password_salt -> Nullable<Binary>,\n        password_iter -> Nullable<Integer>,\n        max_access_count -> Nullable<Integer>,\n        access_count -> Integer,\n        creation_date -> Timestamp,\n        revision_date -> Timestamp,\n        expiration_date -> Nullable<Timestamp>,\n        deletion_date -> Timestamp,\n        disabled -> Bool,\n        hide_email -> Nullable<Bool>,\n    }\n}\n\ntable! {\n    twofactor (uuid) {\n        uuid -> Text,\n        user_uuid -> Text,\n        atype -> Integer,\n        enabled -> Bool,\n        data -> Text,\n        last_used -> BigInt,\n    }\n}\n\ntable! {\n    twofactor_incomplete (user_uuid, device_uuid) {\n        user_uuid -> Text,\n        device_uuid -> Text,\n        device_name -> Text,\n        device_type -> Integer,\n        login_time -> Timestamp,\n        ip_address -> Text,\n    }\n}\n\ntable! {\n    twofactor_duo_ctx (state) {\n        state -> Text,\n        user_email -> Text,\n        nonce -> Text,\n        exp -> BigInt,\n    }\n}\n\ntable! {\n    users (uuid) {\n        uuid -> Text,\n        enabled -> Bool,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n        verified_at -> Nullable<Timestamp>,\n        last_verifying_at -> Nullable<Timestamp>,\n        login_verify_count -> Integer,\n        email -> Text,\n        email_new -> Nullable<Text>,\n        email_new_token -> Nullable<Text>,\n        name -> Text,\n        password_hash -> Binary,\n        salt -> Binary,\n        password_iterations -> Integer,\n        password_hint -> Nullable<Text>,\n        akey -> Text,\n        private_key -> Nullable<Text>,\n        public_key -> Nullable<Text>,\n        totp_secret -> Nullable<Text>,\n        totp_recover -> Nullable<Text>,\n        security_stamp -> Text,\n        stamp_exception -> Nullable<Text>,\n        equivalent_domains -> Text,\n        excluded_globals -> Text,\n        client_kdf_type -> Integer,\n        client_kdf_iter -> Integer,\n        client_kdf_memory -> Nullable<Integer>,\n        client_kdf_parallelism -> Nullable<Integer>,\n        api_key -> Nullable<Text>,\n        avatar_color -> Nullable<Text>,\n        external_id -> Nullable<Text>,\n    }\n}\n\ntable! {\n    users_collections (user_uuid, collection_uuid) {\n        user_uuid -> Text,\n        collection_uuid -> Text,\n        read_only -> Bool,\n        hide_passwords -> Bool,\n        manage -> Bool,\n    }\n}\n\ntable! {\n    users_organizations (uuid) {\n        uuid -> Text,\n        user_uuid -> Text,\n        org_uuid -> Text,\n        invited_by_email -> Nullable<Text>,\n        access_all -> Bool,\n        akey -> Text,\n        status -> Integer,\n        atype -> Integer,\n        reset_password_key -> Nullable<Text>,\n        external_id -> Nullable<Text>,\n    }\n}\n\ntable! {\n    organization_api_key (uuid, org_uuid) {\n        uuid -> Text,\n        org_uuid -> Text,\n        atype -> Integer,\n        api_key -> Text,\n        revision_date -> Timestamp,\n    }\n}\n\ntable! {\n    sso_auth (state) {\n        state -> Text,\n        client_challenge -> Text,\n        nonce -> Text,\n        redirect_uri -> Text,\n        code_response -> Nullable<Text>,\n        auth_response -> Nullable<Text>,\n        created_at -> Timestamp,\n        updated_at -> Timestamp,\n    }\n}\n\ntable! {\n    sso_users (user_uuid) {\n        user_uuid -> Text,\n        identifier -> Text,\n    }\n}\n\ntable! {\n    emergency_access (uuid) {\n        uuid -> Text,\n        grantor_uuid -> Text,\n        grantee_uuid -> Nullable<Text>,\n        email -> Nullable<Text>,\n        key_encrypted -> Nullable<Text>,\n        atype -> Integer,\n        status -> Integer,\n        wait_time_days -> Integer,\n        recovery_initiated_at -> Nullable<Timestamp>,\n        last_notification_at -> Nullable<Timestamp>,\n        updated_at -> Timestamp,\n        created_at -> Timestamp,\n    }\n}\n\ntable! {\n    groups (uuid) {\n        uuid -> Text,\n        organizations_uuid -> Text,\n        name -> Text,\n        access_all -> Bool,\n        external_id -> Nullable<Text>,\n        creation_date -> Timestamp,\n        revision_date -> Timestamp,\n    }\n}\n\ntable! {\n    groups_users (groups_uuid, users_organizations_uuid) {\n        groups_uuid -> Text,\n        users_organizations_uuid -> Text,\n    }\n}\n\ntable! {\n    collections_groups (collections_uuid, groups_uuid) {\n        collections_uuid -> Text,\n        groups_uuid -> Text,\n        read_only -> Bool,\n        hide_passwords -> Bool,\n        manage -> Bool,\n    }\n}\n\ntable! {\n    auth_requests  (uuid) {\n        uuid -> Text,\n        user_uuid -> Text,\n        organization_uuid -> Nullable<Text>,\n        request_device_identifier -> Text,\n        device_type -> Integer,\n        request_ip -> Text,\n        response_device_id -> Nullable<Text>,\n        access_code -> Text,\n        public_key -> Text,\n        enc_key -> Nullable<Text>,\n        master_password_hash -> Nullable<Text>,\n        approved -> Nullable<Bool>,\n        creation_date -> Timestamp,\n        response_date -> Nullable<Timestamp>,\n        authentication_date -> Nullable<Timestamp>,\n    }\n}\n\njoinable!(attachments -> ciphers (cipher_uuid));\njoinable!(ciphers -> organizations (organization_uuid));\njoinable!(ciphers -> users (user_uuid));\njoinable!(ciphers_collections -> ciphers (cipher_uuid));\njoinable!(ciphers_collections -> collections (collection_uuid));\njoinable!(collections -> organizations (org_uuid));\njoinable!(devices -> users (user_uuid));\njoinable!(folders -> users (user_uuid));\njoinable!(folders_ciphers -> ciphers (cipher_uuid));\njoinable!(folders_ciphers -> folders (folder_uuid));\njoinable!(org_policies -> organizations (org_uuid));\njoinable!(sends -> organizations (organization_uuid));\njoinable!(sends -> users (user_uuid));\njoinable!(twofactor -> users (user_uuid));\njoinable!(users_collections -> collections (collection_uuid));\njoinable!(users_collections -> users (user_uuid));\njoinable!(users_organizations -> organizations (org_uuid));\njoinable!(users_organizations -> users (user_uuid));\njoinable!(users_organizations -> ciphers (org_uuid));\njoinable!(organization_api_key -> organizations (org_uuid));\njoinable!(emergency_access -> users (grantor_uuid));\njoinable!(groups -> organizations (organizations_uuid));\njoinable!(groups_users -> users_organizations (users_organizations_uuid));\njoinable!(groups_users -> groups (groups_uuid));\njoinable!(collections_groups -> collections (collections_uuid));\njoinable!(collections_groups -> groups (groups_uuid));\njoinable!(event -> users_organizations (uuid));\njoinable!(auth_requests -> users (user_uuid));\njoinable!(sso_users -> users (user_uuid));\n\nallow_tables_to_appear_in_same_query!(\n    attachments,\n    ciphers,\n    ciphers_collections,\n    collections,\n    devices,\n    folders,\n    folders_ciphers,\n    invitations,\n    org_policies,\n    organizations,\n    sends,\n    sso_users,\n    twofactor,\n    users,\n    users_collections,\n    users_organizations,\n    organization_api_key,\n    emergency_access,\n    groups,\n    groups_users,\n    collections_groups,\n    event,\n    auth_requests,\n);\n"
  },
  {
    "path": "src/error.rs",
    "content": "//\n// Error generator macro\n//\nuse crate::db::models::EventType;\nuse crate::http_client::CustomHttpClientError;\nuse serde::ser::{Serialize, SerializeStruct, Serializer};\nuse std::error::Error as StdError;\n\nmacro_rules! make_error {\n    ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {\n        const BAD_REQUEST: u16 = 400;\n\n        pub enum ErrorKind { $($name( $ty )),+ }\n\n        #[derive(Debug)]\n        pub struct ErrorEvent { pub event: EventType }\n        pub struct Error { message: String, error: ErrorKind, error_code: u16, event: Option<ErrorEvent> }\n\n        $(impl From<$ty> for Error {\n            fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }\n        })+\n        $(impl<S: Into<String>> From<(S, $ty)> for Error {\n            fn from(val: (S, $ty)) -> Self {\n                Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST, event: None }\n            }\n        })+\n        impl StdError for Error {\n            fn source(&self) -> Option<&(dyn StdError + 'static)> {\n                match &self.error {$( ErrorKind::$name(e) => $src_fn(e), )+}\n            }\n        }\n        impl std::fmt::Display for Error {\n            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n                match &self.error {$(\n                   ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)),\n                )+}\n            }\n        }\n    };\n}\n\nuse diesel::r2d2::Error as R2d2Err;\nuse diesel::r2d2::PoolError as R2d2PoolErr;\nuse diesel::result::Error as DieselErr;\nuse diesel::ConnectionError as DieselConErr;\nuse handlebars::RenderError as HbErr;\nuse jsonwebtoken::errors::Error as JwtErr;\nuse lettre::address::AddressError as AddrErr;\nuse lettre::error::Error as LettreErr;\nuse lettre::transport::smtp::Error as SmtpErr;\nuse opendal::Error as OpenDALErr;\nuse openssl::error::ErrorStack as SSLErr;\nuse regex::Error as RegexErr;\nuse reqwest::Error as ReqErr;\nuse rocket::error::Error as RocketErr;\nuse serde_json::{Error as SerdeErr, Value};\nuse std::io::Error as IoErr;\nuse std::time::SystemTimeError as TimeErr;\nuse webauthn_rs::prelude::WebauthnError as WebauthnErr;\nuse yubico::yubicoerror::YubicoError as YubiErr;\n\n#[derive(Serialize)]\npub struct Empty {}\n\npub struct Compact {}\n\n// Error struct\n// Contains a String error message, meant for the user and an enum variant, with an error of different types.\n//\n// After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print).\n// The second one contains the function used to obtain the response sent to the client\nmake_error! {\n    // Just an empty error\n    Empty(Empty):     _no_source, _serialize,\n    // Used to represent err! calls\n    Simple(String):  _no_source,  _api_error,\n    Compact(Compact):  _no_source,  _compact_api_error,\n\n    // Used in our custom http client to handle non-global IPs and blocked domains\n    CustomHttpClient(CustomHttpClientError): _has_source, _api_error,\n\n    // Used for special return values, like 2FA errors\n    Json(Value):           _no_source,  _serialize,\n    Db(DieselErr):         _has_source, _api_error,\n    R2d2(R2d2Err):         _has_source, _api_error,\n    R2d2Pool(R2d2PoolErr): _has_source, _api_error,\n    Serde(SerdeErr):       _has_source, _api_error,\n    JWt(JwtErr):           _has_source, _api_error,\n    Handlebars(HbErr):     _has_source, _api_error,\n\n    Io(IoErr):       _has_source, _api_error,\n    Time(TimeErr):   _has_source, _api_error,\n    Req(ReqErr):     _has_source, _api_error,\n    Regex(RegexErr): _has_source, _api_error,\n    Yubico(YubiErr): _has_source, _api_error,\n\n    Lettre(LettreErr): _has_source, _api_error,\n    Address(AddrErr):  _has_source, _api_error,\n    Smtp(SmtpErr):     _has_source, _api_error,\n    OpenSSL(SSLErr):   _has_source, _api_error,\n    Rocket(RocketErr): _has_source, _api_error,\n\n    DieselCon(DieselConErr): _has_source, _api_error,\n    Webauthn(WebauthnErr):   _has_source, _api_error,\n\n    OpenDAL(OpenDALErr): _has_source, _api_error,\n}\n\nimpl std::fmt::Debug for Error {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self.source() {\n            Some(e) => write!(f, \"{}.\\n[CAUSE] {:#?}\", self.message, e),\n            None => match self.error {\n                ErrorKind::Empty(_) => Ok(()),\n                ErrorKind::Simple(ref s) => {\n                    if &self.message == s {\n                        write!(f, \"{}\", self.message)\n                    } else {\n                        write!(f, \"{}. {}\", self.message, s)\n                    }\n                }\n                ErrorKind::Json(_) => write!(f, \"{}\", self.message),\n                _ => unreachable!(),\n            },\n        }\n    }\n}\n\nimpl Error {\n    pub fn new<M: Into<String>, N: Into<String>>(usr_msg: M, log_msg: N) -> Self {\n        (usr_msg, log_msg.into()).into()\n    }\n\n    pub fn new_msg<M: Into<String> + Clone>(usr_msg: M) -> Self {\n        (usr_msg.clone(), usr_msg.into()).into()\n    }\n\n    pub fn empty() -> Self {\n        Empty {}.into()\n    }\n\n    #[must_use]\n    pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {\n        self.message = msg.into();\n        self\n    }\n\n    #[must_use]\n    pub fn with_kind(mut self, kind: ErrorKind) -> Self {\n        self.error = kind;\n        self\n    }\n\n    #[must_use]\n    pub const fn with_code(mut self, code: u16) -> Self {\n        self.error_code = code;\n        self\n    }\n\n    #[must_use]\n    pub fn with_event(mut self, event: ErrorEvent) -> Self {\n        self.event = Some(event);\n        self\n    }\n\n    pub fn get_event(&self) -> &Option<ErrorEvent> {\n        &self.event\n    }\n\n    pub fn message(&self) -> &str {\n        &self.message\n    }\n}\n\npub trait MapResult<S> {\n    fn map_res(self, msg: &str) -> Result<S, Error>;\n}\n\nimpl<S, E: Into<Error>> MapResult<S> for Result<S, E> {\n    fn map_res(self, msg: &str) -> Result<S, Error> {\n        self.map_err(|e| e.into().with_msg(msg))\n    }\n}\n\nimpl<E: Into<Error>> MapResult<()> for Result<usize, E> {\n    fn map_res(self, msg: &str) -> Result<(), Error> {\n        self.and(Ok(())).map_res(msg)\n    }\n}\n\nimpl<S> MapResult<S> for Option<S> {\n    fn map_res(self, msg: &str) -> Result<S, Error> {\n        self.ok_or_else(|| Error::new(msg, \"\"))\n    }\n}\n\nconst fn _has_source<T>(e: T) -> Option<T> {\n    Some(e)\n}\nfn _no_source<T, S>(_: T) -> Option<S> {\n    None\n}\n\nfn _serialize(e: &impl Serialize, _msg: &str) -> String {\n    serde_json::to_string(e).unwrap()\n}\n\n/// This will serialize the default ApiErrorResponse\n/// It will add the needed fields which are mostly empty or have multiple copies of the message\n/// This is more efficient than having a larger struct and use the Serialize derive\n/// It also prevents using `json!()` calls to create the final output\nimpl Serialize for ApiErrorResponse<'_> {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        #[derive(serde::Serialize)]\n        struct ErrorModel<'a> {\n            message: &'a str,\n            object: &'static str,\n        }\n\n        let mut state = serializer.serialize_struct(\"ApiErrorResponse\", 9)?;\n\n        state.serialize_field(\"message\", self.0.message)?;\n\n        let mut validation_errors = std::collections::HashMap::with_capacity(1);\n        validation_errors.insert(\"\", vec![self.0.message]);\n        state.serialize_field(\"validationErrors\", &validation_errors)?;\n\n        let error_model = ErrorModel {\n            message: self.0.message,\n            object: \"error\",\n        };\n        state.serialize_field(\"errorModel\", &error_model)?;\n\n        state.serialize_field(\"error\", \"\")?;\n        state.serialize_field(\"error_description\", \"\")?;\n        state.serialize_field(\"exceptionMessage\", &None::<()>)?;\n        state.serialize_field(\"exceptionStackTrace\", &None::<()>)?;\n        state.serialize_field(\"innerExceptionMessage\", &None::<()>)?;\n        state.serialize_field(\"object\", \"error\")?;\n\n        state.end()\n    }\n}\n\n/// This will serialize the smaller CompactApiErrorResponse\n/// It will add the needed fields which are mostly empty\n/// This is more efficient than having a larger struct and use the Serialize derive\n/// It also prevents using `json!()` calls to create the final output\nimpl Serialize for CompactApiErrorResponse<'_> {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        let mut state = serializer.serialize_struct(\"CompactApiErrorResponse\", 6)?;\n\n        state.serialize_field(\"message\", self.0.message)?;\n        state.serialize_field(\"validationErrors\", &None::<()>)?;\n        state.serialize_field(\"exceptionMessage\", &None::<()>)?;\n        state.serialize_field(\"exceptionStackTrace\", &None::<()>)?;\n        state.serialize_field(\"innerExceptionMessage\", &None::<()>)?;\n        state.serialize_field(\"object\", \"error\")?;\n\n        state.end()\n    }\n}\n\n/// Main API Error struct template\n/// This struct which we can be used by both ApiErrorResponse and CompactApiErrorResponse\n/// is small and doesn't contain unneeded empty fields. This is more memory efficient, but also less code to compile\nstruct ApiErrorMsg<'a> {\n    message: &'a str,\n}\n/// Default API Error response struct\n/// The custom serialization adds all other needed fields\nstruct ApiErrorResponse<'a>(ApiErrorMsg<'a>);\n/// Compact API Error response struct used for some newer error responses\n/// The custom serialization adds all other needed fields\nstruct CompactApiErrorResponse<'a>(ApiErrorMsg<'a>);\n\nfn _api_error(_: &impl std::any::Any, msg: &str) -> String {\n    let response = ApiErrorMsg {\n        message: msg,\n    };\n    serde_json::to_string(&ApiErrorResponse(response)).unwrap()\n}\n\nfn _compact_api_error(_: &impl std::any::Any, msg: &str) -> String {\n    let response = ApiErrorMsg {\n        message: msg,\n    };\n    serde_json::to_string(&CompactApiErrorResponse(response)).unwrap()\n}\n\n//\n// Rocket responder impl\n//\nuse std::io::Cursor;\n\nuse rocket::http::{ContentType, Status};\nuse rocket::request::Request;\nuse rocket::response::{self, Responder, Response};\n\nimpl Responder<'_, 'static> for Error {\n    fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {\n        match self.error {\n            ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Compact(_) => {} // Don't print the error in this situation\n            _ => error!(target: \"error\", \"{self:#?}\"),\n        };\n\n        let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);\n        let body = self.to_string();\n        Response::build().status(code).header(ContentType::JSON).sized_body(Some(body.len()), Cursor::new(body)).ok()\n    }\n}\n\n//\n// Error return macros\n//\n#[macro_export]\nmacro_rules! err {\n    ($kind:ident, $msg:expr) => {{\n        let msg = $msg;\n        error!(\"{msg}\");\n        return Err($crate::error::Error::new_msg(msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));\n    }};\n    ($msg:expr) => {{\n        let msg = $msg;\n        error!(\"{msg}\");\n        return Err($crate::error::Error::new_msg(msg));\n    }};\n    ($msg:expr, ErrorEvent $err_event:tt) => {{\n        let msg = $msg;\n        error!(\"{msg}\");\n        return Err($crate::error::Error::new_msg(msg).with_event($crate::error::ErrorEvent $err_event));\n    }};\n    ($usr_msg:expr, $log_value:expr) => {{\n        let usr_msg = $usr_msg;\n        let log_value = $log_value;\n        error!(\"{usr_msg}. {log_value}\");\n        return Err($crate::error::Error::new(usr_msg, log_value));\n    }};\n    ($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{\n        let usr_msg = $usr_msg;\n        let log_value = $log_value;\n        error!(\"{usr_msg}. {log_value}\");\n        return Err($crate::error::Error::new(usr_msg, log_value).with_event($crate::error::ErrorEvent $err_event));\n    }};\n}\n\n#[macro_export]\nmacro_rules! err_silent {\n    ($msg:expr) => {{\n        return Err($crate::error::Error::new_msg($msg));\n    }};\n    ($msg:expr, ErrorEvent $err_event:tt) => {{\n        return Err($crate::error::Error::new_msg($msg).with_event($crate::error::ErrorEvent $err_event));\n    }};\n    ($usr_msg:expr, $log_value:expr) => {{\n        return Err($crate::error::Error::new($usr_msg, $log_value));\n    }};\n    ($usr_msg:expr, $log_value:expr, ErrorEvent $err_event:tt) => {{\n        return Err($crate::error::Error::new($usr_msg, $log_value).with_event($crate::error::ErrorEvent $err_event));\n    }};\n}\n\n#[macro_export]\nmacro_rules! err_code {\n    ($msg:expr, $err_code:expr) => {{\n        let msg = $msg;\n        error!(\"{msg}\");\n        return Err($crate::error::Error::new_msg(msg).with_code($err_code));\n    }};\n    ($usr_msg:expr, $log_value:expr, $err_code:expr) => {{\n        let usr_msg = $usr_msg;\n        let log_value = $log_value;\n        error!(\"{usr_msg}. {log_value}\");\n        return Err($crate::error::Error::new(usr_msg, log_value).with_code($err_code));\n    }};\n}\n\n#[macro_export]\nmacro_rules! err_discard {\n    ($msg:expr, $data:expr) => {{\n        std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();\n        return Err($crate::error::Error::new_msg($msg));\n    }};\n    ($usr_msg:expr, $log_value:expr, $data:expr) => {{\n        std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();\n        return Err($crate::error::Error::new($usr_msg, $log_value));\n    }};\n}\n\n#[macro_export]\nmacro_rules! err_json {\n    ($expr:expr, $log_value:expr) => {{\n        return Err(($log_value, $expr).into());\n    }};\n    ($expr:expr, $log_value:expr, $err_event:expr, ErrorEvent) => {{\n        return Err(($log_value, $expr).into().with_event($err_event));\n    }};\n}\n\n#[macro_export]\nmacro_rules! err_handler {\n    ($expr:expr) => {{\n        error!(target: \"auth\", \"Unauthorized Error: {}\", $expr);\n        return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $expr));\n    }};\n    ($usr_msg:expr, $log_value:expr) => {{\n        let usr_msg = $usr_msg;\n        let log_value = $log_value;\n        error!(target: \"auth\", \"Unauthorized Error: {usr_msg}. {log_value}\");\n        return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, usr_msg));\n    }};\n}\n"
  },
  {
    "path": "src/http_client.rs",
    "content": "use std::{\n    fmt,\n    net::{IpAddr, SocketAddr},\n    str::FromStr,\n    sync::{Arc, LazyLock, Mutex},\n    time::Duration,\n};\n\nuse hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};\nuse regex::Regex;\nuse reqwest::{\n    dns::{Name, Resolve, Resolving},\n    header, Client, ClientBuilder,\n};\nuse url::Host;\n\nuse crate::{util::is_global, CONFIG};\n\npub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> {\n    let Ok(url) = url::Url::parse(url) else {\n        err!(\"Invalid URL\");\n    };\n    let Some(host) = url.host() else {\n        err!(\"Invalid host\");\n    };\n\n    should_block_host(&host)?;\n\n    static INSTANCE: LazyLock<Client> =\n        LazyLock::new(|| get_reqwest_client_builder().build().expect(\"Failed to build client\"));\n\n    Ok(INSTANCE.request(method, url))\n}\n\npub fn get_reqwest_client_builder() -> ClientBuilder {\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::USER_AGENT, header::HeaderValue::from_static(\"Vaultwarden\"));\n\n    let redirect_policy = reqwest::redirect::Policy::custom(|attempt| {\n        if attempt.previous().len() >= 5 {\n            return attempt.error(\"Too many redirects\");\n        }\n\n        let Some(host) = attempt.url().host() else {\n            return attempt.error(\"Invalid host\");\n        };\n\n        if let Err(e) = should_block_host(&host) {\n            return attempt.error(e);\n        }\n\n        attempt.follow()\n    });\n\n    Client::builder()\n        .default_headers(headers)\n        .redirect(redirect_policy)\n        .dns_resolver(CustomDnsResolver::instance())\n        .timeout(Duration::from_secs(10))\n}\n\npub fn should_block_address(domain_or_ip: &str) -> bool {\n    if let Ok(ip) = IpAddr::from_str(domain_or_ip) {\n        if should_block_ip(ip) {\n            return true;\n        }\n    }\n\n    should_block_address_regex(domain_or_ip)\n}\n\nfn should_block_ip(ip: IpAddr) -> bool {\n    if !CONFIG.http_request_block_non_global_ips() {\n        return false;\n    }\n\n    !is_global(ip)\n}\n\nfn should_block_address_regex(domain_or_ip: &str) -> bool {\n    let Some(block_regex) = CONFIG.http_request_block_regex() else {\n        return false;\n    };\n\n    static COMPILED_REGEX: Mutex<Option<(String, Regex)>> = Mutex::new(None);\n    let mut guard = COMPILED_REGEX.lock().unwrap();\n\n    // If the stored regex is up to date, use it\n    if let Some((value, regex)) = &*guard {\n        if value == &block_regex {\n            return regex.is_match(domain_or_ip);\n        }\n    }\n\n    // If we don't have a regex stored, or it's not up to date, recreate it\n    let regex = Regex::new(&block_regex).unwrap();\n    let is_match = regex.is_match(domain_or_ip);\n    *guard = Some((block_regex, regex));\n\n    is_match\n}\n\nfn should_block_host(host: &Host<&str>) -> Result<(), CustomHttpClientError> {\n    let (ip, host_str): (Option<IpAddr>, String) = match host {\n        Host::Ipv4(ip) => (Some(IpAddr::V4(*ip)), ip.to_string()),\n        Host::Ipv6(ip) => (Some(IpAddr::V6(*ip)), ip.to_string()),\n        Host::Domain(d) => (None, (*d).to_string()),\n    };\n\n    if let Some(ip) = ip {\n        if should_block_ip(ip) {\n            return Err(CustomHttpClientError::NonGlobalIp {\n                domain: None,\n                ip,\n            });\n        }\n    }\n\n    if should_block_address_regex(&host_str) {\n        return Err(CustomHttpClientError::Blocked {\n            domain: host_str,\n        });\n    }\n\n    Ok(())\n}\n\n#[derive(Debug, Clone)]\npub enum CustomHttpClientError {\n    Blocked {\n        domain: String,\n    },\n    NonGlobalIp {\n        domain: Option<String>,\n        ip: IpAddr,\n    },\n}\n\nimpl CustomHttpClientError {\n    pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> {\n        let mut source = e.source();\n\n        while let Some(err) = source {\n            source = err.source();\n            if let Some(err) = err.downcast_ref::<CustomHttpClientError>() {\n                return Some(err);\n            }\n        }\n        None\n    }\n}\n\nimpl fmt::Display for CustomHttpClientError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Blocked {\n                domain,\n            } => write!(f, \"Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX\"),\n            Self::NonGlobalIp {\n                domain: Some(domain),\n                ip,\n            } => write!(f, \"IP {ip} for domain '{domain}' is not a global IP!\"),\n            Self::NonGlobalIp {\n                domain: None,\n                ip,\n            } => write!(f, \"IP {ip} is not a global IP!\"),\n        }\n    }\n}\n\nimpl std::error::Error for CustomHttpClientError {}\n\n#[derive(Debug, Clone)]\nenum CustomDnsResolver {\n    Default(),\n    Hickory(Arc<TokioResolver>),\n}\ntype BoxError = Box<dyn std::error::Error + Send + Sync>;\n\nimpl CustomDnsResolver {\n    fn instance() -> Arc<Self> {\n        static INSTANCE: LazyLock<Arc<CustomDnsResolver>> = LazyLock::new(CustomDnsResolver::new);\n        Arc::clone(&*INSTANCE)\n    }\n\n    fn new() -> Arc<Self> {\n        match TokioResolver::builder(TokioConnectionProvider::default()) {\n            Ok(mut builder) => {\n                if CONFIG.dns_prefer_ipv6() {\n                    builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv6thenIpv4;\n                }\n                let resolver = builder.build();\n                Arc::new(Self::Hickory(Arc::new(resolver)))\n            }\n            Err(e) => {\n                warn!(\"Error creating Hickory resolver, falling back to default: {e:?}\");\n                Arc::new(Self::Default())\n            }\n        }\n    }\n\n    // Note that we get an iterator of addresses, but we only grab the first one for convenience\n    async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {\n        pre_resolve(name)?;\n\n        let result = match self {\n            Self::Default() => tokio::net::lookup_host(name).await?.next(),\n            Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),\n        };\n\n        if let Some(addr) = &result {\n            post_resolve(name, addr.ip())?;\n        }\n\n        Ok(result)\n    }\n}\n\nfn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> {\n    if should_block_address(name) {\n        return Err(CustomHttpClientError::Blocked {\n            domain: name.to_string(),\n        });\n    }\n\n    Ok(())\n}\n\nfn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomHttpClientError> {\n    if should_block_ip(ip) {\n        Err(CustomHttpClientError::NonGlobalIp {\n            domain: Some(name.to_string()),\n            ip,\n        })\n    } else {\n        Ok(())\n    }\n}\n\nimpl Resolve for CustomDnsResolver {\n    fn resolve(&self, name: Name) -> Resolving {\n        let this = self.clone();\n        Box::pin(async move {\n            let name = name.as_str();\n            let result = this.resolve_domain(name).await?;\n            Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))\n        })\n    }\n}\n\n#[cfg(s3)]\npub(crate) mod aws {\n    use aws_smithy_runtime_api::client::{\n        http::{HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector},\n        orchestrator::HttpResponse,\n        result::ConnectorError,\n        runtime_components::RuntimeComponents,\n    };\n    use reqwest::Client;\n\n    // Adapter that wraps reqwest to be compatible with the AWS SDK\n    #[derive(Debug)]\n    pub(crate) struct AwsReqwestConnector {\n        pub(crate) client: Client,\n    }\n\n    impl HttpConnector for AwsReqwestConnector {\n        fn call(&self, request: aws_smithy_runtime_api::client::orchestrator::HttpRequest) -> HttpConnectorFuture {\n            // Convert the AWS-style request to a reqwest request\n            let client = self.client.clone();\n            let future = async move {\n                let method = reqwest::Method::from_bytes(request.method().as_bytes())\n                    .map_err(|e| ConnectorError::user(Box::new(e)))?;\n                let mut req_builder = client.request(method, request.uri().to_string());\n\n                for (name, value) in request.headers() {\n                    req_builder = req_builder.header(name, value);\n                }\n\n                if let Some(body_bytes) = request.body().bytes() {\n                    req_builder = req_builder.body(body_bytes.to_vec());\n                }\n\n                let response = req_builder.send().await.map_err(|e| ConnectorError::io(Box::new(e)))?;\n\n                let status = response.status().into();\n                let bytes = response.bytes().await.map_err(|e| ConnectorError::io(Box::new(e)))?;\n\n                Ok(HttpResponse::new(status, bytes.into()))\n            };\n\n            HttpConnectorFuture::new(Box::pin(future))\n        }\n    }\n\n    impl HttpClient for AwsReqwestConnector {\n        fn http_connector(\n            &self,\n            _settings: &HttpConnectorSettings,\n            _components: &RuntimeComponents,\n        ) -> SharedHttpConnector {\n            SharedHttpConnector::new(AwsReqwestConnector {\n                client: self.client.clone(),\n            })\n        }\n    }\n}\n"
  },
  {
    "path": "src/mail.rs",
    "content": "use chrono::NaiveDateTime;\nuse percent_encoding::{percent_encode, NON_ALPHANUMERIC};\nuse std::{env::consts::EXE_SUFFIX, str::FromStr};\n\nuse lettre::{\n    message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart},\n    transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism},\n    transport::smtp::client::{Tls, TlsParameters},\n    transport::smtp::extension::ClientId,\n    Address, AsyncSendmailTransport, AsyncSmtpTransport, AsyncTransport, Tokio1Executor,\n};\n\nuse crate::{\n    api::EmptyResult,\n    auth::{\n        encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims,\n        generate_verify_email_claims,\n    },\n    db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId},\n    error::Error,\n    CONFIG,\n};\n\nfn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> {\n    if let Some(command) = CONFIG.sendmail_command() {\n        AsyncSendmailTransport::new_with_command(command)\n    } else {\n        AsyncSendmailTransport::new_with_command(format!(\"sendmail{EXE_SUFFIX}\"))\n    }\n}\n\nfn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {\n    use std::time::Duration;\n    let host = CONFIG.smtp_host().unwrap();\n\n    let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host.as_str())\n        .port(CONFIG.smtp_port())\n        .timeout(Some(Duration::from_secs(CONFIG.smtp_timeout())));\n\n    // Determine security\n    let smtp_client = if CONFIG.smtp_security() != *\"off\" {\n        let mut tls_parameters = TlsParameters::builder(host);\n        if CONFIG.smtp_accept_invalid_hostnames() {\n            tls_parameters = tls_parameters.dangerous_accept_invalid_hostnames(true);\n        }\n        if CONFIG.smtp_accept_invalid_certs() {\n            tls_parameters = tls_parameters.dangerous_accept_invalid_certs(true);\n        }\n        let tls_parameters = tls_parameters.build().unwrap();\n\n        if CONFIG.smtp_security() == *\"force_tls\" {\n            smtp_client.tls(Tls::Wrapper(tls_parameters))\n        } else {\n            smtp_client.tls(Tls::Required(tls_parameters))\n        }\n    } else {\n        smtp_client\n    };\n\n    let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) {\n        (Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)),\n        _ => smtp_client,\n    };\n\n    let smtp_client = match CONFIG.helo_name() {\n        Some(helo_name) => smtp_client.hello_name(ClientId::Domain(helo_name)),\n        None => smtp_client,\n    };\n\n    let smtp_client = match CONFIG.smtp_auth_mechanism() {\n        Some(mechanism) => {\n            let allowed_mechanisms = [SmtpAuthMechanism::Plain, SmtpAuthMechanism::Login, SmtpAuthMechanism::Xoauth2];\n            let mut selected_mechanisms = vec![];\n            for wanted_mechanism in mechanism.split(',') {\n                for m in &allowed_mechanisms {\n                    if m.to_string().to_lowercase()\n                        == wanted_mechanism.trim_matches(|c| c == '\"' || c == '\\'' || c == ' ').to_lowercase()\n                    {\n                        selected_mechanisms.push(*m);\n                    }\n                }\n            }\n\n            if !selected_mechanisms.is_empty() {\n                smtp_client.authentication(selected_mechanisms)\n            } else {\n                // Only show a warning, and return without setting an actual authentication mechanism\n                warn!(\"No valid SMTP Auth mechanism found for '{mechanism}', using default values\");\n                smtp_client\n            }\n        }\n        _ => smtp_client,\n    };\n\n    smtp_client.build()\n}\n\n// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections\nfn sanitize_data(data: &mut serde_json::Value) {\n    use regex::Regex;\n    use std::sync::LazyLock;\n    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r\"<[^>]+>\").unwrap());\n\n    match data {\n        serde_json::Value::String(s) => *s = RE.replace_all(s, \"\").to_string(),\n        serde_json::Value::Object(obj) => {\n            for d in obj.values_mut() {\n                sanitize_data(d);\n            }\n        }\n        serde_json::Value::Array(arr) => {\n            for d in arr.iter_mut() {\n                sanitize_data(d);\n            }\n        }\n        _ => {}\n    }\n}\n\nfn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {\n    let mut data = data;\n    sanitize_data(&mut data);\n    let (subject_html, body_html) = get_template(&format!(\"{template_name}.html\"), &data)?;\n    let (_subject_text, body_text) = get_template(template_name, &data)?;\n    Ok((subject_html, body_html, body_text))\n}\n\nfn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String, String), Error> {\n    let text = CONFIG.render_template(template_name, data)?;\n    let mut text_split = text.split(\"<!---------------->\");\n\n    let subject = match text_split.next() {\n        Some(s) => s.trim().to_string(),\n        None => err!(\"Template doesn't contain subject\"),\n    };\n\n    let body = match text_split.next() {\n        Some(s) => s.trim().to_string(),\n        None => err!(\"Template doesn't contain body\"),\n    };\n\n    if text_split.next().is_some() {\n        err!(\"Template contains more than one body\");\n    }\n\n    Ok((subject, body))\n}\n\npub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {\n    let template_name = if hint.is_some() {\n        \"email/pw_hint_some\"\n    } else {\n        \"email/pw_hint_none\"\n    };\n\n    let (subject, body_html, body_text) = get_text(\n        template_name,\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"hint\": hint,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult {\n    let claims = generate_delete_claims(user_id.to_string());\n    let delete_token = encode_jwt(&claims);\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/delete_account\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"user_id\": user_id,\n            \"email\": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),\n            \"token\": delete_token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {\n    let claims = generate_verify_email_claims(user_id);\n    let verify_email_token = encode_jwt(&claims);\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/verify_email\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"user_id\": user_id,\n            \"email\": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),\n            \"token\": verify_email_token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult {\n    let mut query = url::Url::parse(\"https://query.builder\").unwrap();\n    query.query_pairs_mut().append_pair(\"email\", email).append_pair(\"token\", token);\n    let query_string = match query.query() {\n        None => err!(\"Failed to build verify URL query parameters\"),\n        Some(query) => query,\n    };\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/register_verify_email\",\n        json!({\n            // `url.Url` would place the anchor `#` after the query parameters\n            \"url\": format!(\"{}/#/finish-signup/?{query_string}\", CONFIG.domain()),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"email\": email,\n        }),\n    )?;\n\n    send_email(email, &subject, body_html, body_text).await\n}\n\npub async fn send_welcome(address: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/welcome\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {\n    let claims = generate_verify_email_claims(user_id);\n    let verify_email_token = encode_jwt(&claims);\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/welcome_must_verify\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"user_id\": user_id,\n            \"token\": verify_email_token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/send_2fa_removed_from_org\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"org_name\": org_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_single_org_removed_from_org(address: &str, org_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/send_single_org_removed_from_org\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"org_name\": org_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_invite(\n    user: &User,\n    org_id: OrganizationId,\n    member_id: MembershipId,\n    org_name: &str,\n    invited_by_email: Option<String>,\n) -> EmptyResult {\n    let claims = generate_invite_claims(\n        user.uuid.clone(),\n        user.email.clone(),\n        org_id.clone(),\n        member_id.clone(),\n        invited_by_email,\n    );\n    let invite_token = encode_jwt(&claims);\n    let mut query = url::Url::parse(\"https://query.builder\").unwrap();\n    {\n        let mut query_params = query.query_pairs_mut();\n        query_params\n            .append_pair(\"email\", &user.email)\n            .append_pair(\"organizationName\", org_name)\n            .append_pair(\"organizationId\", &org_id)\n            .append_pair(\"organizationUserId\", &member_id)\n            .append_pair(\"token\", &invite_token);\n\n        if CONFIG.sso_enabled() && CONFIG.sso_only() {\n            query_params.append_pair(\"orgSsoIdentifier\", &org_id);\n        }\n        if user.private_key.is_some() {\n            query_params.append_pair(\"orgUserHasExistingUser\", \"true\");\n        }\n    }\n\n    let Some(query_string) = query.query() else {\n        err!(\"Failed to build invite URL query parameters\")\n    };\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/send_org_invite\",\n        json!({\n            // `url.Url` would place the anchor `#` after the query parameters\n            \"url\": format!(\"{}/#/accept-organization/?{query_string}\", CONFIG.domain()),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"org_name\": org_name,\n        }),\n    )?;\n\n    send_email(&user.email, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_invite(\n    address: &str,\n    user_id: UserId,\n    emer_id: EmergencyAccessId,\n    grantor_name: &str,\n    grantor_email: &str,\n) -> EmptyResult {\n    let claims = generate_emergency_access_invite_claims(\n        user_id,\n        String::from(address),\n        emer_id.clone(),\n        String::from(grantor_name),\n        String::from(grantor_email),\n    );\n\n    // Build the query here to ensure proper escaping\n    let mut query = url::Url::parse(\"https://query.builder\").unwrap();\n    {\n        let mut query_params = query.query_pairs_mut();\n        query_params\n            .append_pair(\"id\", &emer_id.to_string())\n            .append_pair(\"name\", grantor_name)\n            .append_pair(\"email\", address)\n            .append_pair(\"token\", &encode_jwt(&claims));\n    }\n\n    let Some(query_string) = query.query() else {\n        err!(\"Failed to build emergency invite URL query parameters\")\n    };\n\n    let (subject, body_html, body_text) = get_text(\n        \"email/send_emergency_access_invite\",\n        json!({\n            // `url.Url` would place the anchor `#` after the query parameters\n            \"url\": format!(\"{}/#/accept-emergency/?{query_string}\", CONFIG.domain()),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantor_name\": grantor_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_invite_accepted(address: &str, grantee_email: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_invite_accepted\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantee_email\": grantee_email,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_invite_confirmed(address: &str, grantor_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_invite_confirmed\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantor_name\": grantor_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_recovery_approved(address: &str, grantor_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_recovery_approved\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantor_name\": grantor_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_recovery_initiated(\n    address: &str,\n    grantee_name: &str,\n    atype: &str,\n    wait_time_days: &i32,\n) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_recovery_initiated\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantee_name\": grantee_name,\n            \"atype\": atype,\n            \"wait_time_days\": wait_time_days,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_recovery_reminder(\n    address: &str,\n    grantee_name: &str,\n    atype: &str,\n    days_left: &str,\n) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_recovery_reminder\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantee_name\": grantee_name,\n            \"atype\": atype,\n            \"days_left\": days_left,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_recovery_rejected(address: &str, grantor_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_recovery_rejected\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantor_name\": grantor_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_emergency_access_recovery_timed_out(address: &str, grantee_name: &str, atype: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/emergency_access_recovery_timed_out\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"grantee_name\": grantee_name,\n            \"atype\": atype,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/invite_accepted\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"email\": new_user_email,\n            \"org_name\": org_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/invite_confirmed\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"org_name\": org_name,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &Device) -> EmptyResult {\n    use crate::util::upcase_first;\n\n    let fmt = \"%A, %B %_d, %Y at %r %Z\";\n    let (subject, body_html, body_text) = get_text(\n        \"email/new_device_logged_in\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"ip\": ip,\n            \"device_name\": upcase_first(&device.name),\n            \"device_type\": DeviceType::from_i32(device.atype).to_string(),\n            \"datetime\": crate::util::format_naive_datetime_local(dt, fmt),\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_incomplete_2fa_login(\n    address: &str,\n    ip: &str,\n    dt: &NaiveDateTime,\n    device_name: &str,\n    device_type: &str,\n) -> EmptyResult {\n    use crate::util::upcase_first;\n\n    let fmt = \"%A, %B %_d, %Y at %r %Z\";\n    let (subject, body_html, body_text) = get_text(\n        \"email/incomplete_2fa_login\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"ip\": ip,\n            \"device_name\": upcase_first(device_name),\n            \"device_type\": device_type,\n            \"datetime\": crate::util::format_naive_datetime_local(dt, fmt),\n            \"time_limit\": CONFIG.incomplete_2fa_time_limit(),\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_token(address: &str, token: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/twofactor_email\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"token\": token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_change_email(address: &str, token: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/change_email\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"token\": token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_change_email_existing(address: &str, acting_address: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/change_email_existing\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"existing_address\": address,\n            \"acting_address\": acting_address,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_change_email_invited(address: &str, acting_address: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/change_email_invited\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"existing_address\": address,\n            \"acting_address\": acting_address,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_sso_change_email(address: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/sso_change_email\",\n        json!({\n            \"url\": format!(\"{}/#/settings/account\", CONFIG.domain()),\n            \"img_src\": CONFIG._smtp_img_src(),\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_test(address: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/smtp_test\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/admin_reset_password\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"user_name\": user_name,\n            \"org_name\": org_name,\n        }),\n    )?;\n    send_email(address, &subject, body_html, body_text).await\n}\n\npub async fn send_protected_action_token(address: &str, token: &str) -> EmptyResult {\n    let (subject, body_html, body_text) = get_text(\n        \"email/protected_action\",\n        json!({\n            \"url\": CONFIG.domain(),\n            \"img_src\": CONFIG._smtp_img_src(),\n            \"token\": token,\n        }),\n    )?;\n\n    send_email(address, &subject, body_html, body_text).await\n}\n\nasync fn send_with_selected_transport(email: Message) -> EmptyResult {\n    if CONFIG.use_sendmail() {\n        match sendmail_transport().send(email).await {\n            Ok(_) => Ok(()),\n            // Match some common errors and make them more user friendly\n            Err(e) => {\n                if e.is_client() {\n                    debug!(\"Sendmail client error: {e:?}\");\n                    err!(format!(\"Sendmail client error: {e}\"));\n                } else if e.is_response() {\n                    debug!(\"Sendmail response error: {e:?}\");\n                    err!(format!(\"Sendmail response error: {e}\"));\n                } else {\n                    debug!(\"Sendmail error: {e:?}\");\n                    err!(format!(\"Sendmail error: {e}\"));\n                }\n            }\n        }\n    } else {\n        match smtp_transport().send(email).await {\n            Ok(_) => Ok(()),\n            // Match some common errors and make them more user friendly\n            Err(e) => {\n                if e.is_client() {\n                    debug!(\"SMTP client error: {e:#?}\");\n                    err!(format!(\"SMTP client error: {e}\"));\n                } else if e.is_transient() {\n                    debug!(\"SMTP 4xx error: {e:#?}\");\n                    err!(format!(\"SMTP 4xx error: {e}\"));\n                } else if e.is_permanent() {\n                    debug!(\"SMTP 5xx error: {e:#?}\");\n                    let mut msg = e.to_string();\n                    // Add a special check for 535 to add a more descriptive message\n                    if msg.contains(\"(535)\") {\n                        msg = format!(\"{msg} - Authentication credentials invalid\");\n                    }\n                    err!(format!(\"SMTP 5xx error: {msg}\"));\n                } else if e.is_timeout() {\n                    debug!(\"SMTP timeout error: {e:#?}\");\n                    err!(format!(\"SMTP timeout error: {e}\"));\n                } else if e.is_tls() {\n                    debug!(\"SMTP encryption error: {e:#?}\");\n                    err!(format!(\"SMTP encryption error: {e}\"));\n                } else {\n                    debug!(\"SMTP error: {e:#?}\");\n                    err!(format!(\"SMTP error: {e}\"));\n                }\n            }\n        }\n    }\n}\n\nasync fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {\n    let smtp_from = Address::from_str(&CONFIG.smtp_from())?;\n\n    let body = if CONFIG.smtp_embed_images() {\n        let logo_gray_body = Body::new(crate::api::static_files(\"logo-gray.png\").unwrap().1.to_vec());\n        let mail_github_body = Body::new(crate::api::static_files(\"mail-github.png\").unwrap().1.to_vec());\n        MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart(\n            MultiPart::related()\n                .singlepart(SinglePart::html(body_html))\n                .singlepart(\n                    Attachment::new_inline(String::from(\"logo-gray.png\"))\n                        .body(logo_gray_body, \"image/png\".parse().unwrap()),\n                )\n                .singlepart(\n                    Attachment::new_inline(String::from(\"mail-github.png\"))\n                        .body(mail_github_body, \"image/png\".parse().unwrap()),\n                ),\n        )\n    } else {\n        MultiPart::alternative_plain_html(body_text, body_html)\n    };\n\n    let email = Message::builder()\n        .message_id(Some(format!(\"<{}@{}>\", crate::util::get_uuid(), smtp_from.domain())))\n        .to(Mailbox::new(None, Address::from_str(address)?))\n        .from(Mailbox::new(Some(CONFIG.smtp_from_name()), smtp_from))\n        .subject(subject)\n        .multipart(body)?;\n\n    send_with_selected_transport(email).await\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![cfg_attr(feature = \"unstable\", feature(ip))]\n// The recursion_limit is mainly triggered by the json!() macro.\n// The more key/value pairs there are the more recursion occurs.\n// We want to keep this as low as possible!\n#![recursion_limit = \"165\"]\n\n// When enabled use MiMalloc as malloc instead of the default malloc\n#[cfg(feature = \"enable_mimalloc\")]\nuse mimalloc::MiMalloc;\n#[cfg(feature = \"enable_mimalloc\")]\n#[cfg_attr(feature = \"enable_mimalloc\", global_allocator)]\nstatic GLOBAL: MiMalloc = MiMalloc;\n\n#[macro_use]\nextern crate rocket;\n#[macro_use]\nextern crate serde;\n#[macro_use]\nextern crate serde_json;\n#[macro_use]\nextern crate log;\n#[macro_use]\nextern crate diesel;\n#[macro_use]\nextern crate diesel_migrations;\n#[macro_use]\nextern crate diesel_derive_newtype;\n\nuse std::{\n    collections::HashMap,\n    fs::{canonicalize, create_dir_all},\n    panic,\n    path::Path,\n    process::exit,\n    str::FromStr,\n    thread,\n};\n\nuse tokio::{\n    fs::File,\n    io::{AsyncBufReadExt, BufReader},\n};\n\n#[cfg(unix)]\nuse tokio::signal::unix::SignalKind;\n\n#[macro_use]\nmod error;\nmod api;\nmod auth;\nmod config;\nmod crypto;\n#[macro_use]\nmod db;\nmod http_client;\nmod mail;\nmod ratelimit;\nmod sso;\nmod sso_client;\nmod util;\n\nuse crate::api::core::two_factor::duo_oidc::purge_duo_contexts;\nuse crate::api::purge_auth_requests;\nuse crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};\npub use config::{PathType, CONFIG};\npub use error::{Error, MapResult};\nuse rocket::data::{Limits, ToByteUnit};\nuse std::sync::{atomic::Ordering, Arc};\npub use util::is_running_in_container;\n\n#[rocket::main]\nasync fn main() -> Result<(), Error> {\n    parse_args();\n    launch_info();\n\n    let level = init_logging()?;\n\n    check_data_folder().await;\n    auth::initialize_keys().await.unwrap_or_else(|e| {\n        error!(\"Error creating private key '{}'\\n{e:?}\\nExiting Vaultwarden!\", CONFIG.private_rsa_key());\n        exit(1);\n    });\n    check_web_vault();\n\n    create_dir(&CONFIG.tmp_folder(), \"tmp folder\");\n\n    let pool = create_db_pool().await;\n    schedule_jobs(pool.clone());\n    db::models::TwoFactor::migrate_u2f_to_webauthn(&pool.get().await.unwrap()).await.unwrap();\n    db::models::TwoFactor::migrate_credential_to_passkey(&pool.get().await.unwrap()).await.unwrap();\n\n    let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);\n    launch_rocket(pool, extra_debug).await // Blocks until program termination.\n}\n\nconst HELP: &str = \"\\\nAlternative implementation of the Bitwarden server API written in Rust\n\nUSAGE:\n    vaultwarden [FLAGS|COMMAND]\n\nFLAGS:\n    -h, --help       Prints help information\n    -v, --version    Prints the app and web-vault version\n\nCOMMAND:\n    hash [--preset {bitwarden|owasp}]  Generate an Argon2id PHC ADMIN_TOKEN\n    backup                             Create a backup of the SQLite database\n                                       You can also send the USR1 signal to trigger a backup\n\nPRESETS:                  m=         t=          p=\n    bitwarden (default) 64MiB, 3 Iterations, 4 Threads\n    owasp               19MiB, 2 Iterations, 1 Thread\n\n\";\n\npub const VERSION: Option<&str> = option_env!(\"VW_VERSION\");\n\nfn parse_args() {\n    let mut pargs = pico_args::Arguments::from_env();\n    let version = VERSION.unwrap_or(\"(Version info from Git not present)\");\n\n    if pargs.contains([\"-h\", \"--help\"]) {\n        println!(\"Vaultwarden {version}\");\n        print!(\"{HELP}\");\n        exit(0);\n    } else if pargs.contains([\"-v\", \"--version\"]) {\n        config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);\n        let web_vault_version = util::get_active_web_release();\n        println!(\"Vaultwarden {version}\");\n        println!(\"Web-Vault {web_vault_version}\");\n        exit(0);\n    }\n\n    if let Some(command) = pargs.subcommand().unwrap_or_default() {\n        if command == \"hash\" {\n            use argon2::{\n                password_hash::SaltString, Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13,\n            };\n\n            let mut argon2_params = ParamsBuilder::new();\n            let preset: Option<String> = pargs.opt_value_from_str([\"-p\", \"--preset\"]).unwrap_or_default();\n            let selected_preset;\n            match preset.as_deref() {\n                Some(\"owasp\") => {\n                    selected_preset = \"owasp\";\n                    argon2_params.m_cost(19456);\n                    argon2_params.t_cost(2);\n                    argon2_params.p_cost(1);\n                }\n                _ => {\n                    // Bitwarden preset is the default\n                    selected_preset = \"bitwarden\";\n                    argon2_params.m_cost(65540);\n                    argon2_params.t_cost(3);\n                    argon2_params.p_cost(4);\n                }\n            }\n\n            println!(\"Generate an Argon2id PHC string using the '{selected_preset}' preset:\\n\");\n\n            let password = rpassword::prompt_password(\"Password: \").unwrap();\n            if password.len() < 8 {\n                println!(\"\\nPassword must contain at least 8 characters\");\n                exit(1);\n            }\n\n            let password_verify = rpassword::prompt_password(\"Confirm Password: \").unwrap();\n            if password != password_verify {\n                println!(\"\\nPasswords do not match\");\n                exit(1);\n            }\n\n            let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap());\n            let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap();\n\n            let argon2_timer = tokio::time::Instant::now();\n            if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) {\n                println!(\n                    \"\\n\\\n                    ADMIN_TOKEN='{password_hash}'\\n\\n\\\n                    Generation of the Argon2id PHC string took: {:?}\",\n                    argon2_timer.elapsed()\n                );\n            } else {\n                println!(\"Unable to generate Argon2id PHC hash.\");\n                exit(1);\n            }\n        } else if command == \"backup\" {\n            match db::backup_sqlite() {\n                Ok(f) => {\n                    println!(\"Backup to '{f}' was successful\");\n                    exit(0);\n                }\n                Err(e) => {\n                    println!(\"Backup failed. {e:?}\");\n                    exit(1);\n                }\n            }\n        }\n        exit(0);\n    }\n}\n\nfn launch_info() {\n    println!(\n        \"\\\n        /--------------------------------------------------------------------\\\\\\n\\\n        |                        Starting Vaultwarden                        |\"\n    );\n\n    if let Some(version) = VERSION {\n        println!(\"|{:^68}|\", format!(\"Version {version}\"));\n    }\n\n    println!(\n        \"\\\n        |--------------------------------------------------------------------|\\n\\\n        | This is an *unofficial* Bitwarden implementation, DO NOT use the   |\\n\\\n        | official channels to report bugs/features, regardless of client.   |\\n\\\n        | Send usage/configuration questions or feature requests to:         |\\n\\\n        |   https://github.com/dani-garcia/vaultwarden/discussions or        |\\n\\\n        |   https://vaultwarden.discourse.group/                             |\\n\\\n        | Report suspected bugs/issues in the software itself at:            |\\n\\\n        |   https://github.com/dani-garcia/vaultwarden/issues/new            |\\n\\\n        \\\\--------------------------------------------------------------------/\\n\"\n    );\n}\n\nfn init_logging() -> Result<log::LevelFilter, Error> {\n    let levels = log::LevelFilter::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::<Vec<String>>().join(\"|\");\n    let log_level_rgx_str = format!(\"^({levels})((,[^,=]+=({levels}))*)$\");\n    let log_level_rgx = regex::Regex::new(&log_level_rgx_str)?;\n    let config_str = CONFIG.log_level().to_lowercase();\n\n    let (level, levels_override) = if let Some(caps) = log_level_rgx.captures(&config_str) {\n        let level = caps\n            .get(1)\n            .and_then(|m| log::LevelFilter::from_str(m.as_str()).ok())\n            .ok_or(Error::new(\"Failed to parse global log level\".to_string(), \"\"))?;\n\n        let levels_override: Vec<(&str, log::LevelFilter)> = caps\n            .get(2)\n            .map(|m| {\n                m.as_str()\n                    .split(',')\n                    .collect::<Vec<&str>>()\n                    .into_iter()\n                    .flat_map(|s| match s.split_once('=') {\n                        Some((log, lvl_str)) => log::LevelFilter::from_str(lvl_str).ok().map(|lvl| (log, lvl)),\n                        _ => None,\n                    })\n                    .collect()\n            })\n            .ok_or(Error::new(\"Failed to parse overrides\".to_string(), \"\"))?;\n\n        (level, levels_override)\n    } else {\n        err!(format!(\"LOG_LEVEL should follow the format info,vaultwarden::api::icons=debug, invalid: {config_str}\"))\n    };\n\n    // Depending on the main log level we either want to disable or enable logging for hickory.\n    // Else if there are timeouts it will clutter the logs since hickory uses warn for this.\n    let hickory_level = if level >= log::LevelFilter::Debug {\n        level\n    } else {\n        log::LevelFilter::Off\n    };\n\n    // Only show Rocket underscore `_` logs when the level is Debug or higher\n    // Else this will bloat the log output with useless messages.\n    let rocket_underscore_level = if level >= log::LevelFilter::Debug {\n        log::LevelFilter::Warn\n    } else {\n        log::LevelFilter::Off\n    };\n\n    // Only show handlebar logs when the level is Trace\n    let handlebars_level = if level >= log::LevelFilter::Trace {\n        log::LevelFilter::Trace\n    } else {\n        log::LevelFilter::Warn\n    };\n\n    // Enable smtp debug logging only specifically for smtp when need.\n    // This can contain sensitive information we do not want in the default debug/trace logging.\n    let smtp_log_level = if CONFIG.smtp_debug() {\n        log::LevelFilter::Debug\n    } else {\n        log::LevelFilter::Off\n    };\n\n    let mut default_levels = HashMap::from([\n        // Hide unknown certificate errors if using self-signed\n        (\"rustls::session\", log::LevelFilter::Off),\n        // Hide failed to close stream messages\n        (\"hyper::server\", log::LevelFilter::Warn),\n        // Silence Rocket `_` logs\n        (\"_\", rocket_underscore_level),\n        (\"rocket::response::responder::_\", rocket_underscore_level),\n        (\"rocket::server::_\", rocket_underscore_level),\n        (\"vaultwarden::api::admin::_\", rocket_underscore_level),\n        (\"vaultwarden::api::notifications::_\", rocket_underscore_level),\n        // Silence Rocket logs\n        (\"rocket::launch\", log::LevelFilter::Error),\n        (\"rocket::launch_\", log::LevelFilter::Error),\n        (\"rocket::rocket\", log::LevelFilter::Warn),\n        (\"rocket::server\", log::LevelFilter::Warn),\n        (\"rocket::fairing::fairings\", log::LevelFilter::Warn),\n        (\"rocket::shield::shield\", log::LevelFilter::Warn),\n        (\"hyper::proto\", log::LevelFilter::Off),\n        (\"hyper::client\", log::LevelFilter::Off),\n        // Filter handlebars logs\n        (\"handlebars::render\", handlebars_level),\n        // Prevent cookie_store logs\n        (\"cookie_store\", log::LevelFilter::Off),\n        // Variable level for hickory used by reqwest\n        (\"hickory_resolver::name_server::name_server\", hickory_level),\n        (\"hickory_proto::xfer\", hickory_level),\n        // SMTP\n        (\"lettre::transport::smtp\", smtp_log_level),\n        // Set query_logger default to Off, but can be overwritten manually\n        // You can set LOG_LEVEL=info,vaultwarden::db::query_logger=<LEVEL> to overwrite it.\n        // This makes it possible to do the following:\n        // warn = Print slow queries only, 5 seconds or longer\n        // info = Print slow queries only, 1 second or longer\n        // debug = Print all queries\n        (\"vaultwarden::db::query_logger\", log::LevelFilter::Off),\n    ]);\n\n    for (path, level) in levels_override.into_iter() {\n        let _ = default_levels.insert(path, level);\n    }\n\n    if Some(&log::LevelFilter::Debug) == default_levels.get(\"lettre::transport::smtp\") {\n        println!(\n            \"[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!\\n\\\n             [WARNING] Only enable SMTP_DEBUG during troubleshooting!\\n\"\n        );\n    }\n\n    let mut logger = fern::Dispatch::new().level(level).chain(std::io::stdout());\n\n    for (path, level) in default_levels {\n        logger = logger.level_for(path.to_string(), level);\n    }\n\n    if CONFIG.extended_logging() {\n        logger = logger.format(|out, message, record| {\n            out.finish(format_args!(\n                \"[{}][{}][{}] {}\",\n                chrono::Local::now().format(&CONFIG.log_timestamp_format()),\n                record.target(),\n                record.level(),\n                message\n            ))\n        });\n    } else {\n        logger = logger.format(|out, message, _| out.finish(format_args!(\"{message}\")));\n    }\n\n    if let Some(log_file) = CONFIG.log_file() {\n        #[cfg(windows)]\n        {\n            logger = logger.chain(fern::log_file(log_file)?);\n        }\n        #[cfg(unix)]\n        {\n            const SIGHUP: i32 = SignalKind::hangup().as_raw_value();\n            let path = Path::new(&log_file);\n            logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?);\n        }\n    }\n\n    #[cfg(unix)]\n    {\n        if cfg!(feature = \"enable_syslog\") || CONFIG.use_syslog() {\n            logger = chain_syslog(logger);\n        }\n    }\n\n    if let Err(err) = logger.apply() {\n        err!(format!(\"Failed to activate logger: {err}\"))\n    }\n\n    // Catch panics and log them instead of default output to StdErr\n    panic::set_hook(Box::new(|info| {\n        let thread = thread::current();\n        let thread = thread.name().unwrap_or(\"unnamed\");\n\n        let msg = match info.payload().downcast_ref::<&'static str>() {\n            Some(s) => *s,\n            None => match info.payload().downcast_ref::<String>() {\n                Some(s) => &**s,\n                None => \"Box<Any>\",\n            },\n        };\n\n        let backtrace = std::backtrace::Backtrace::force_capture();\n\n        match info.location() {\n            Some(location) => {\n                error!(\n                    target: \"panic\", \"thread '{}' panicked at '{}': {}:{}\\n{:}\",\n                    thread,\n                    msg,\n                    location.file(),\n                    location.line(),\n                    backtrace\n                );\n            }\n            None => error!(\n                target: \"panic\",\n                \"thread '{thread}' panicked at '{msg}'\\n{backtrace:}\"\n            ),\n        }\n    }));\n\n    Ok(level)\n}\n\n#[cfg(unix)]\nfn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {\n    let syslog_fmt = syslog::Formatter3164 {\n        facility: syslog::Facility::LOG_USER,\n        hostname: None,\n        process: \"vaultwarden\".into(),\n        pid: 0,\n    };\n\n    match syslog::unix(syslog_fmt) {\n        Ok(sl) => logger.chain(sl),\n        Err(e) => {\n            error!(\"Unable to connect to syslog: {e:?}\");\n            logger\n        }\n    }\n}\n\nfn create_dir(path: &str, description: &str) {\n    // Try to create the specified dir, if it doesn't already exist.\n    let err_msg = format!(\"Error creating {description} directory '{path}'\");\n    create_dir_all(path).expect(&err_msg);\n}\n\nasync fn check_data_folder() {\n    let data_folder = &CONFIG.data_folder();\n\n    if data_folder.starts_with(\"s3://\") {\n        if let Err(e) = CONFIG\n            .opendal_operator_for_path_type(&PathType::Data)\n            .unwrap_or_else(|e| {\n                error!(\"Failed to create S3 operator for data folder '{data_folder}': {e:?}\");\n                exit(1);\n            })\n            .check()\n            .await\n        {\n            error!(\"Could not access S3 data folder '{data_folder}': {e:?}\");\n            exit(1);\n        }\n\n        return;\n    }\n\n    let path = Path::new(data_folder);\n    if !path.exists() {\n        error!(\"Data folder '{data_folder}' doesn't exist.\");\n        if is_running_in_container() {\n            error!(\"Verify that your data volume is mounted at the correct location.\");\n        } else {\n            error!(\"Create the data folder and try again.\");\n        }\n        exit(1);\n    }\n    if !path.is_dir() {\n        error!(\"Data folder '{data_folder}' is not a directory.\");\n        exit(1);\n    }\n\n    if is_running_in_container()\n        && std::env::var(\"I_REALLY_WANT_VOLATILE_STORAGE\").is_err()\n        && !container_data_folder_is_persistent(data_folder).await\n    {\n        error!(\n            \"No persistent volume!\\n\\\n            ########################################################################################\\n\\\n            # It looks like you did not configure a persistent volume!                             #\\n\\\n            # This will result in permanent data loss when the container is removed or updated!    #\\n\\\n            # If you really want to use volatile storage set `I_REALLY_WANT_VOLATILE_STORAGE=true` #\\n\\\n            ########################################################################################\\n\"\n        );\n        exit(1);\n    }\n}\n\n/// Detect when using Docker or Podman the DATA_FOLDER is either a bind-mount or a volume created manually.\n/// If not created manually, then the data will not be persistent.\n/// A none persistent volume in either Docker or Podman is represented by a 64 alphanumerical string.\n/// If we detect this string, we will alert about not having a persistent self defined volume.\n/// This probably means that someone forgot to add `-v /path/to/vaultwarden_data/:/data`\nasync fn container_data_folder_is_persistent(data_folder: &str) -> bool {\n    if let Ok(mountinfo) = File::open(\"/proc/self/mountinfo\").await {\n        // Since there can only be one mountpoint to the DATA_FOLDER\n        // We do a basic check for this mountpoint surrounded by a space.\n        let data_folder_match = if data_folder.starts_with('/') {\n            format!(\" {data_folder} \")\n        } else {\n            format!(\" /{data_folder} \")\n        };\n        let mut lines = BufReader::new(mountinfo).lines();\n        let re = regex::Regex::new(r\"/volumes/[a-z0-9]{64}/_data /\").unwrap();\n        while let Some(line) = lines.next_line().await.unwrap_or_default() {\n            // Only execute a regex check if we find the base match\n            if line.contains(&data_folder_match) {\n                if re.is_match(&line) {\n                    return false;\n                }\n                // If we did found a match for the mountpoint, but not the regex, then still stop searching.\n                break;\n            }\n        }\n    }\n    // In all other cases, just assume a true.\n    // This is just an informative check to try and prevent data loss.\n    true\n}\n\nfn check_web_vault() {\n    if !CONFIG.web_vault_enabled() {\n        return;\n    }\n\n    let index_path = Path::new(&CONFIG.web_vault_folder()).join(\"index.html\");\n\n    if !index_path.exists() {\n        error!(\n            \"Web vault is not found at '{}'. To install it, please follow the steps in: \",\n            CONFIG.web_vault_folder()\n        );\n        error!(\"https://github.com/dani-garcia/vaultwarden/wiki/Building-binary#install-the-web-vault\");\n        error!(\"You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it\");\n        exit(1);\n    }\n}\n\nasync fn create_db_pool() -> db::DbPool {\n    match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()).await {\n        Ok(p) => p,\n        Err(e) => {\n            error!(\"Error creating database pool: {e:?}\");\n            exit(1);\n        }\n    }\n}\n\nasync fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> {\n    let basepath = &CONFIG.domain_path();\n\n    let mut config = rocket::Config::from(rocket::Config::figment());\n    config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into();\n    config.cli_colors = false; // Make sure Rocket does not color any values for logging.\n    config.limits = Limits::new()\n        .limit(\"json\", 20.megabytes()) // 20MB should be enough for very large imports, something like 5000+ vault entries\n        .limit(\"data-form\", 525.megabytes()) // This needs to match the maximum allowed file size for Send\n        .limit(\"file\", 525.megabytes()); // This needs to match the maximum allowed file size for attachments\n\n    // If adding more paths here, consider also adding them to\n    // crate::utils::LOGGED_ROUTES to make sure they appear in the log\n    let instance = rocket::custom(config)\n        .mount([basepath, \"/\"].concat(), api::web_routes())\n        .mount([basepath, \"/api\"].concat(), api::core_routes())\n        .mount([basepath, \"/admin\"].concat(), api::admin_routes())\n        .mount([basepath, \"/events\"].concat(), api::core_events_routes())\n        .mount([basepath, \"/identity\"].concat(), api::identity_routes())\n        .mount([basepath, \"/icons\"].concat(), api::icons_routes())\n        .mount([basepath, \"/notifications\"].concat(), api::notifications_routes())\n        .register([basepath, \"/\"].concat(), api::web_catchers())\n        .register([basepath, \"/api\"].concat(), api::core_catchers())\n        .register([basepath, \"/admin\"].concat(), api::admin_catchers())\n        .manage(pool)\n        .manage(Arc::clone(&WS_USERS))\n        .manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))\n        .attach(util::AppHeaders())\n        .attach(util::Cors())\n        .attach(util::BetterLogging(extra_debug))\n        .ignite()\n        .await?;\n\n    CONFIG.set_rocket_shutdown_handle(instance.shutdown());\n\n    tokio::spawn(async move {\n        tokio::signal::ctrl_c().await.expect(\"Error setting Ctrl-C handler\");\n        info!(\"Exiting Vaultwarden!\");\n        CONFIG.shutdown();\n    });\n\n    #[cfg(all(unix, sqlite))]\n    {\n        if db::ACTIVE_DB_TYPE.get() != Some(&db::DbConnType::Sqlite) {\n            debug!(\"PostgreSQL and MySQL/MariaDB do not support this backup feature, skip adding USR1 signal.\");\n        } else {\n            tokio::spawn(async move {\n                let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();\n                loop {\n                    // If we need more signals to act upon, we might want to use select! here.\n                    // With only one item to listen for this is enough.\n                    let _ = signal_user1.recv().await;\n                    match db::backup_sqlite() {\n                        Ok(f) => info!(\"Backup to '{f}' was successful\"),\n                        Err(e) => error!(\"Backup failed. {e:?}\"),\n                    }\n                }\n            });\n        }\n    }\n\n    instance.launch().await?;\n\n    info!(\"Vaultwarden process exited!\");\n    Ok(())\n}\n\nfn schedule_jobs(pool: db::DbPool) {\n    if CONFIG.job_poll_interval_ms() == 0 {\n        info!(\"Job scheduler disabled.\");\n        return;\n    }\n\n    let runtime = tokio::runtime::Runtime::new().unwrap();\n\n    thread::Builder::new()\n        .name(\"job-scheduler\".to_string())\n        .spawn(move || {\n            use job_scheduler_ng::{Job, JobScheduler};\n            let _runtime_guard = runtime.enter();\n\n            let mut sched = JobScheduler::new();\n\n            // Purge sends that are past their deletion date.\n            if !CONFIG.send_purge_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.send_purge_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::purge_sends(pool.clone()));\n                }));\n            }\n\n            // Purge trashed items that are old enough to be auto-deleted.\n            if !CONFIG.trash_purge_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.trash_purge_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::purge_trashed_ciphers(pool.clone()));\n                }));\n            }\n\n            // Send email notifications about incomplete 2FA logins, which potentially\n            // indicates that a user's master password has been compromised.\n            if !CONFIG.incomplete_2fa_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.incomplete_2fa_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::send_incomplete_2fa_notifications(pool.clone()));\n                }));\n            }\n\n            // Grant emergency access requests that have met the required wait time.\n            // This job should run before the emergency access reminders job to avoid\n            // sending reminders for requests that are about to be granted anyway.\n            if !CONFIG.emergency_request_timeout_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.emergency_request_timeout_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::emergency_request_timeout_job(pool.clone()));\n                }));\n            }\n\n            // Send reminders to emergency access grantors that there are pending\n            // emergency access requests.\n            if !CONFIG.emergency_notification_reminder_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.emergency_notification_reminder_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::emergency_notification_reminder_job(pool.clone()));\n                }));\n            }\n\n            if !CONFIG.auth_request_purge_schedule().is_empty() {\n                sched.add(Job::new(CONFIG.auth_request_purge_schedule().parse().unwrap(), || {\n                    runtime.spawn(purge_auth_requests(pool.clone()));\n                }));\n            }\n\n            // Clean unused, expired Duo authentication contexts.\n            if !CONFIG.duo_context_purge_schedule().is_empty() && CONFIG._enable_duo() && !CONFIG.duo_use_iframe() {\n                sched.add(Job::new(CONFIG.duo_context_purge_schedule().parse().unwrap(), || {\n                    runtime.spawn(purge_duo_contexts(pool.clone()));\n                }));\n            }\n\n            // Cleanup the event table of records x days old.\n            if CONFIG.org_events_enabled()\n                && !CONFIG.event_cleanup_schedule().is_empty()\n                && CONFIG.events_days_retain().is_some()\n            {\n                sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || {\n                    runtime.spawn(api::event_cleanup_job(pool.clone()));\n                }));\n            }\n\n            // Purge sso auth from incomplete flow (default to daily at 00h20).\n            if !CONFIG.purge_incomplete_sso_auth().is_empty() {\n                sched.add(Job::new(CONFIG.purge_incomplete_sso_auth().parse().unwrap(), || {\n                    runtime.spawn(db::models::SsoAuth::delete_expired(pool.clone()));\n                }));\n            }\n\n            // Periodically check for jobs to run. We probably won't need any\n            // jobs that run more often than once a minute, so a default poll\n            // interval of 30 seconds should be sufficient. Users who want to\n            // schedule jobs to run more frequently for some reason can reduce\n            // the poll interval accordingly.\n            //\n            // Note that the scheduler checks jobs in the order in which they\n            // were added, so if two jobs are both eligible to run at a given\n            // tick, the one that was added earlier will run first.\n            loop {\n                sched.tick();\n                runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(CONFIG.job_poll_interval_ms())));\n            }\n        })\n        .expect(\"Error spawning job scheduler thread\");\n}\n"
  },
  {
    "path": "src/ratelimit.rs",
    "content": "use std::{net::IpAddr, num::NonZeroU32, sync::LazyLock, time::Duration};\n\nuse governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter};\n\nuse crate::{Error, CONFIG};\n\ntype Limiter<T = IpAddr> = RateLimiter<T, DashMapStateStore<T>, DefaultClock>;\n\nstatic LIMITER_LOGIN: LazyLock<Limiter> = LazyLock::new(|| {\n    let seconds = Duration::from_secs(CONFIG.login_ratelimit_seconds());\n    let burst = NonZeroU32::new(CONFIG.login_ratelimit_max_burst()).expect(\"Non-zero login ratelimit burst\");\n    RateLimiter::keyed(Quota::with_period(seconds).expect(\"Non-zero login ratelimit seconds\").allow_burst(burst))\n});\n\nstatic LIMITER_ADMIN: LazyLock<Limiter> = LazyLock::new(|| {\n    let seconds = Duration::from_secs(CONFIG.admin_ratelimit_seconds());\n    let burst = NonZeroU32::new(CONFIG.admin_ratelimit_max_burst()).expect(\"Non-zero admin ratelimit burst\");\n    RateLimiter::keyed(Quota::with_period(seconds).expect(\"Non-zero admin ratelimit seconds\").allow_burst(burst))\n});\n\npub fn check_limit_login(ip: &IpAddr) -> Result<(), Error> {\n    match LIMITER_LOGIN.check_key(ip) {\n        Ok(_) => Ok(()),\n        Err(_e) => {\n            err_code!(\"Too many login requests\", 429);\n        }\n    }\n}\n\npub fn check_limit_admin(ip: &IpAddr) -> Result<(), Error> {\n    match LIMITER_ADMIN.check_key(ip) {\n        Ok(_) => Ok(()),\n        Err(_e) => {\n            err_code!(\"Too many admin requests\", 429);\n        }\n    }\n}\n"
  },
  {
    "path": "src/sso.rs",
    "content": "use std::{sync::LazyLock, time::Duration};\n\nuse chrono::Utc;\nuse derive_more::{AsRef, Deref, Display, From, Into};\nuse regex::Regex;\nuse url::Url;\n\nuse crate::{\n    api::ApiResult,\n    auth,\n    auth::{AuthMethod, AuthTokens, TokenWrapper, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY},\n    db::{\n        models::{Device, OIDCAuthenticatedUser, OIDCCodeWrapper, SsoAuth, SsoUser, User},\n        DbConn,\n    },\n    sso_client::Client,\n    CONFIG,\n};\n\npub static FAKE_IDENTIFIER: &str = \"VW_DUMMY_IDENTIFIER_FOR_OIDC\";\n\nstatic SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!(\"{}|sso\", CONFIG.domain_origin()));\n\npub static SSO_AUTH_EXPIRATION: LazyLock<chrono::Duration> =\n    LazyLock::new(|| chrono::TimeDelta::try_minutes(10).unwrap());\n\n#[derive(\n    Clone,\n    Debug,\n    Default,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    From,\n)]\n#[deref(forward)]\n#[from(forward)]\npub struct OIDCCode(String);\n\n#[derive(\n    Clone,\n    Debug,\n    Default,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    From,\n    Into,\n)]\n#[deref(forward)]\n#[into(owned)]\npub struct OIDCCodeChallenge(String);\n\n#[derive(\n    Clone,\n    Debug,\n    Default,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    Into,\n)]\n#[deref(forward)]\n#[into(owned)]\npub struct OIDCCodeVerifier(String);\n\n#[derive(\n    Clone,\n    Debug,\n    Default,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    From,\n)]\n#[deref(forward)]\n#[from(forward)]\npub struct OIDCState(String);\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct SsoTokenJwtClaims {\n    // Not before\n    pub nbf: i64,\n    // Expiration time\n    pub exp: i64,\n    // Issuer\n    pub iss: String,\n    // Subject\n    pub sub: String,\n}\n\npub fn encode_ssotoken_claims() -> String {\n    let time_now = Utc::now();\n    let claims = SsoTokenJwtClaims {\n        nbf: time_now.timestamp(),\n        exp: (time_now + chrono::TimeDelta::try_minutes(2).unwrap()).timestamp(),\n        iss: SSO_JWT_ISSUER.to_string(),\n        sub: \"vaultwarden\".to_string(),\n    };\n\n    auth::encode_jwt(&claims)\n}\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\nstruct BasicTokenClaims {\n    iat: Option<i64>,\n    nbf: Option<i64>,\n    exp: i64,\n}\n\n#[derive(Deserialize)]\nstruct BasicTokenClaimsValidation {\n    exp: u64,\n    iss: String,\n}\n\nimpl BasicTokenClaims {\n    fn nbf(&self) -> i64 {\n        self.nbf.or(self.iat).unwrap_or_else(|| Utc::now().timestamp())\n    }\n}\n\nfn decode_token_claims(token_name: &str, token: &str) -> ApiResult<BasicTokenClaims> {\n    // We need to manually validate this token, since `insecure_decode` does not do this\n    match jsonwebtoken::dangerous::insecure_decode::<BasicTokenClaimsValidation>(token) {\n        Ok(btcv) => {\n            let now = jsonwebtoken::get_current_timestamp();\n            let validate_claim = btcv.claims;\n            // Validate the exp in the claim with a leeway of 60 seconds, same as jsonwebtoken does\n            if validate_claim.exp < now - 60 {\n                err_silent!(format!(\"Expired Signature for base token claim from {token_name}\"))\n            }\n            if validate_claim.iss.ne(&CONFIG.sso_authority()) {\n                err_silent!(format!(\"Invalid Issuer for base token claim from {token_name}\"))\n            }\n\n            // All is validated and ok, lets decode again using the wanted struct\n            let btc = jsonwebtoken::dangerous::insecure_decode::<BasicTokenClaims>(token).unwrap();\n            Ok(btc.claims)\n        }\n        Err(err) => err_silent!(format!(\"Failed to decode basic token claims from {token_name}: {err}\")),\n    }\n}\n\npub fn decode_state(base64_state: &str) -> ApiResult<OIDCState> {\n    let state = match data_encoding::BASE64.decode(base64_state.as_bytes()) {\n        Ok(vec) => match String::from_utf8(vec) {\n            Ok(valid) => OIDCState(valid),\n            Err(_) => err!(format!(\"Invalid utf8 chars in {base64_state} after base64 decoding\")),\n        },\n        Err(_) => err!(format!(\"Failed to decode {base64_state} using base64\")),\n    };\n\n    Ok(state)\n}\n\n// redirect_uri from: https://github.com/bitwarden/server/blob/main/src/Identity/IdentityServer/ApiClient.cs\npub async fn authorize_url(\n    state: OIDCState,\n    client_challenge: OIDCCodeChallenge,\n    client_id: &str,\n    raw_redirect_uri: &str,\n    conn: DbConn,\n) -> ApiResult<Url> {\n    let redirect_uri = match client_id {\n        \"web\" | \"browser\" => format!(\"{}/sso-connector.html\", CONFIG.domain()),\n        \"desktop\" | \"mobile\" => \"bitwarden://sso-callback\".to_string(),\n        \"cli\" => {\n            let port_regex = Regex::new(r\"^http://localhost:([0-9]{4})$\").unwrap();\n            match port_regex.captures(raw_redirect_uri).and_then(|captures| captures.get(1).map(|c| c.as_str())) {\n                Some(port) => format!(\"http://localhost:{port}\"),\n                None => err!(\"Failed to extract port number\"),\n            }\n        }\n        _ => err!(format!(\"Unsupported client {client_id}\")),\n    };\n\n    let (auth_url, sso_auth) = Client::authorize_url(state, client_challenge, redirect_uri).await?;\n    sso_auth.save(&conn).await?;\n    Ok(auth_url)\n}\n\n#[derive(\n    Clone,\n    Debug,\n    Default,\n    DieselNewType,\n    FromForm,\n    PartialEq,\n    Eq,\n    Hash,\n    Serialize,\n    Deserialize,\n    AsRef,\n    Deref,\n    Display,\n    From,\n)]\n#[deref(forward)]\n#[from(forward)]\npub struct OIDCIdentifier(String);\n\nimpl OIDCIdentifier {\n    fn new(issuer: &str, subject: &str) -> Self {\n        OIDCIdentifier(format!(\"{issuer}/{subject}\"))\n    }\n}\n\n// During the 2FA flow we will\n//  - retrieve the user information and then only discover he needs 2FA.\n//  - second time we will rely on `SsoAuth.auth_response` since the `code` has already been exchanged.\n// The `SsoAuth` will ensure that the user is authorized only once.\npub async fn exchange_code(\n    state: &OIDCState,\n    client_verifier: OIDCCodeVerifier,\n    conn: &DbConn,\n) -> ApiResult<(SsoAuth, OIDCAuthenticatedUser)> {\n    use openidconnect::OAuth2TokenResponse;\n\n    let mut sso_auth = match SsoAuth::find(state, conn).await {\n        None => err!(format!(\"Invalid state cannot retrieve sso auth\")),\n        Some(sso_auth) => sso_auth,\n    };\n\n    if let Some(authenticated_user) = sso_auth.auth_response.clone() {\n        return Ok((sso_auth, authenticated_user));\n    }\n\n    let code = match sso_auth.code_response.clone() {\n        Some(OIDCCodeWrapper::Ok {\n            code,\n        }) => code.clone(),\n        Some(OIDCCodeWrapper::Error {\n            error,\n            error_description,\n        }) => {\n            sso_auth.delete(conn).await?;\n            err!(format!(\"SSO authorization failed: {error}, {}\", error_description.as_ref().unwrap_or(&String::new())))\n        }\n        None => {\n            sso_auth.delete(conn).await?;\n            err!(\"Missing authorization provider return\");\n        }\n    };\n\n    let client = Client::cached().await?;\n    let (token_response, id_claims) = client.exchange_code(code, client_verifier, &sso_auth).await?;\n\n    let user_info = client.user_info(token_response.access_token().to_owned()).await?;\n\n    let email = match id_claims.email().or(user_info.email()) {\n        None => err!(\"Neither id token nor userinfo contained an email\"),\n        Some(e) => e.to_string().to_lowercase(),\n    };\n\n    let email_verified = id_claims.email_verified().or(user_info.email_verified());\n\n    let user_name = id_claims.preferred_username().map(|un| un.to_string());\n\n    let refresh_token = token_response.refresh_token().map(|t| t.secret());\n    if refresh_token.is_none() && CONFIG.sso_scopes_vec().contains(&\"offline_access\".to_string()) {\n        error!(\"Scope offline_access is present but response contain no refresh_token\");\n    }\n\n    let identifier = OIDCIdentifier::new(id_claims.issuer(), id_claims.subject());\n\n    let authenticated_user = OIDCAuthenticatedUser {\n        refresh_token: refresh_token.cloned(),\n        access_token: token_response.access_token().secret().clone(),\n        expires_in: token_response.expires_in(),\n        identifier: identifier.clone(),\n        email: email.clone(),\n        email_verified,\n        user_name: user_name.clone(),\n    };\n\n    debug!(\"Authenticated user {authenticated_user:?}\");\n    sso_auth.auth_response = Some(authenticated_user.clone());\n    sso_auth.updated_at = Utc::now().naive_utc();\n    sso_auth.save(conn).await?;\n\n    Ok((sso_auth, authenticated_user))\n}\n\n// User has passed 2FA flow we can delete auth info from database\npub async fn redeem(\n    device: &Device,\n    user: &User,\n    client_id: Option<String>,\n    sso_user: Option<SsoUser>,\n    sso_auth: SsoAuth,\n    auth_user: OIDCAuthenticatedUser,\n    conn: &DbConn,\n) -> ApiResult<AuthTokens> {\n    sso_auth.delete(conn).await?;\n\n    if sso_user.is_none() {\n        let user_sso = SsoUser {\n            user_uuid: user.uuid.clone(),\n            identifier: auth_user.identifier.clone(),\n        };\n        user_sso.save(conn).await?;\n    }\n\n    if !CONFIG.sso_auth_only_not_session() {\n        let now = Utc::now();\n\n        let (ap_nbf, ap_exp) =\n            match (decode_token_claims(\"access_token\", &auth_user.access_token), auth_user.expires_in) {\n                (Ok(ap), _) => (ap.nbf(), ap.exp),\n                (Err(_), Some(exp)) => (now.timestamp(), (now + exp).timestamp()),\n                _ => err!(\"Non jwt access_token and empty expires_in\"),\n            };\n\n        let access_claims =\n            auth::LoginJwtClaims::new(device, user, ap_nbf, ap_exp, AuthMethod::Sso.scope_vec(), client_id, now);\n\n        _create_auth_tokens(device, auth_user.refresh_token, access_claims, auth_user.access_token)\n    } else {\n        Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id))\n    }\n}\n\n// We always return a refresh_token (with no refresh_token some secrets are not displayed in the web front).\n// If there is no SSO refresh_token, we keep the access_token to be able to call user_info to check for validity\npub fn create_auth_tokens(\n    device: &Device,\n    user: &User,\n    client_id: Option<String>,\n    refresh_token: Option<String>,\n    access_token: String,\n    expires_in: Option<Duration>,\n) -> ApiResult<AuthTokens> {\n    if !CONFIG.sso_auth_only_not_session() {\n        let now = Utc::now();\n\n        let (ap_nbf, ap_exp) = match (decode_token_claims(\"access_token\", &access_token), expires_in) {\n            (Ok(ap), _) => (ap.nbf(), ap.exp),\n            (Err(_), Some(exp)) => (now.timestamp(), (now + exp).timestamp()),\n            _ => err!(\"Non jwt access_token and empty expires_in\"),\n        };\n\n        let access_claims =\n            auth::LoginJwtClaims::new(device, user, ap_nbf, ap_exp, AuthMethod::Sso.scope_vec(), client_id, now);\n\n        _create_auth_tokens(device, refresh_token, access_claims, access_token)\n    } else {\n        Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id))\n    }\n}\n\nfn _create_auth_tokens(\n    device: &Device,\n    refresh_token: Option<String>,\n    access_claims: auth::LoginJwtClaims,\n    access_token: String,\n) -> ApiResult<AuthTokens> {\n    let (nbf, exp, token) = if let Some(rt) = refresh_token {\n        match decode_token_claims(\"refresh_token\", &rt) {\n            Err(_) => {\n                let time_now = Utc::now();\n                let exp = (time_now + *DEFAULT_REFRESH_VALIDITY).timestamp();\n                debug!(\"Non jwt refresh_token (expiration set to {exp})\");\n                (time_now.timestamp(), exp, TokenWrapper::Refresh(rt))\n            }\n            Ok(refresh_payload) => {\n                debug!(\"Refresh_payload: {refresh_payload:?}\");\n                (refresh_payload.nbf(), refresh_payload.exp, TokenWrapper::Refresh(rt))\n            }\n        }\n    } else {\n        debug!(\"No refresh_token present\");\n        (access_claims.nbf, access_claims.exp, TokenWrapper::Access(access_token))\n    };\n\n    let refresh_claims = auth::RefreshJwtClaims {\n        nbf,\n        exp,\n        iss: auth::JWT_LOGIN_ISSUER.to_string(),\n        sub: AuthMethod::Sso,\n        device_token: device.refresh_token.clone(),\n        token: Some(token),\n    };\n\n    Ok(AuthTokens {\n        refresh_claims,\n        access_claims,\n    })\n}\n\n// This endpoint is called in two case\n//  - the session is close to expiration we will try to extend it\n//  - the user is going to make an action and we check that the session is still valid\npub async fn exchange_refresh_token(\n    device: &Device,\n    user: &User,\n    client_id: Option<String>,\n    refresh_claims: auth::RefreshJwtClaims,\n) -> ApiResult<AuthTokens> {\n    let exp = refresh_claims.exp;\n    match refresh_claims.token {\n        Some(TokenWrapper::Refresh(refresh_token)) => {\n            // Use new refresh_token if returned\n            let (new_refresh_token, access_token, expires_in) =\n                Client::exchange_refresh_token(refresh_token.clone()).await?;\n\n            create_auth_tokens(\n                device,\n                user,\n                client_id,\n                new_refresh_token.or(Some(refresh_token)),\n                access_token,\n                expires_in,\n            )\n        }\n        Some(TokenWrapper::Access(access_token)) => {\n            let now = Utc::now();\n            let exp_limit = (now + *BW_EXPIRATION).timestamp();\n\n            if exp < exp_limit {\n                err_silent!(\"Access token is close to expiration but we have no refresh token\")\n            }\n\n            Client::check_validity(access_token.clone()).await?;\n\n            let access_claims = auth::LoginJwtClaims::new(\n                device,\n                user,\n                now.timestamp(),\n                exp,\n                AuthMethod::Sso.scope_vec(),\n                client_id,\n                now,\n            );\n\n            _create_auth_tokens(device, None, access_claims, access_token)\n        }\n        None => err!(\"No token present while in SSO\"),\n    }\n}\n"
  },
  {
    "path": "src/sso_client.rs",
    "content": "use std::{borrow::Cow, sync::LazyLock, time::Duration};\n\nuse openidconnect::{core::*, reqwest, *};\nuse regex::Regex;\nuse url::Url;\n\nuse crate::{\n    api::{ApiResult, EmptyResult},\n    db::models::SsoAuth,\n    sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},\n    CONFIG,\n};\n\nstatic CLIENT_CACHE_KEY: LazyLock<String> = LazyLock::new(|| \"sso-client\".to_string());\nstatic CLIENT_CACHE: LazyLock<moka::sync::Cache<String, Client>> = LazyLock::new(|| {\n    moka::sync::Cache::builder()\n        .max_capacity(1)\n        .time_to_live(Duration::from_secs(CONFIG.sso_client_cache_expiration()))\n        .build()\n});\nstatic REFRESH_CACHE: LazyLock<moka::future::Cache<String, Result<RefreshTokenResponse, String>>> =\n    LazyLock::new(|| moka::future::Cache::builder().max_capacity(1000).time_to_live(Duration::from_secs(30)).build());\n\n/// OpenID Connect Core client.\npub type CustomClient = openidconnect::Client<\n    EmptyAdditionalClaims,\n    CoreAuthDisplay,\n    CoreGenderClaim,\n    CoreJweContentEncryptionAlgorithm,\n    CoreJsonWebKey,\n    CoreAuthPrompt,\n    StandardErrorResponse<CoreErrorResponseType>,\n    CoreTokenResponse,\n    CoreTokenIntrospectionResponse,\n    CoreRevocableToken,\n    CoreRevocationErrorResponse,\n    EndpointSet,\n    EndpointNotSet,\n    EndpointNotSet,\n    EndpointNotSet,\n    EndpointSet,\n    EndpointSet,\n>;\n\npub type RefreshTokenResponse = (Option<String>, String, Option<Duration>);\n\n#[derive(Clone)]\npub struct Client {\n    pub http_client: reqwest::Client,\n    pub core_client: CustomClient,\n}\n\nimpl Client {\n    // Call the OpenId discovery endpoint to retrieve configuration\n    async fn _get_client() -> ApiResult<Self> {\n        let client_id = ClientId::new(CONFIG.sso_client_id());\n        let client_secret = ClientSecret::new(CONFIG.sso_client_secret());\n\n        let issuer_url = CONFIG.sso_issuer_url()?;\n\n        let http_client = match reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build() {\n            Err(err) => err!(format!(\"Failed to build http client: {err}\")),\n            Ok(client) => client,\n        };\n\n        let provider_metadata = match CoreProviderMetadata::discover_async(issuer_url, &http_client).await {\n            Err(err) => err!(format!(\"Failed to discover OpenID provider: {err}\")),\n            Ok(metadata) => metadata,\n        };\n\n        let base_client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret));\n\n        let token_uri = match base_client.token_uri() {\n            Some(uri) => uri.clone(),\n            None => err!(\"Failed to discover token_url, cannot proceed\"),\n        };\n\n        let user_info_url = match base_client.user_info_url() {\n            Some(url) => url.clone(),\n            None => err!(\"Failed to discover user_info url, cannot proceed\"),\n        };\n\n        let core_client = base_client\n            .set_redirect_uri(CONFIG.sso_redirect_url()?)\n            .set_token_uri(token_uri)\n            .set_user_info_url(user_info_url);\n\n        Ok(Client {\n            http_client,\n            core_client,\n        })\n    }\n\n    // Simple cache to prevent recalling the discovery endpoint each time\n    pub async fn cached() -> ApiResult<Self> {\n        if CONFIG.sso_client_cache_expiration() > 0 {\n            match CLIENT_CACHE.get(&*CLIENT_CACHE_KEY) {\n                Some(client) => Ok(client),\n                None => Self::_get_client().await.inspect(|client| {\n                    debug!(\"Inserting new client in cache\");\n                    CLIENT_CACHE.insert(CLIENT_CACHE_KEY.clone(), client.clone());\n                }),\n            }\n        } else {\n            Self::_get_client().await\n        }\n    }\n\n    pub fn invalidate() {\n        if CONFIG.sso_client_cache_expiration() > 0 {\n            CLIENT_CACHE.invalidate(&*CLIENT_CACHE_KEY);\n        }\n    }\n\n    // The `state` is encoded using base64 to ensure no issue with providers (It contains the Organization identifier).\n    pub async fn authorize_url(\n        state: OIDCState,\n        client_challenge: OIDCCodeChallenge,\n        redirect_uri: String,\n    ) -> ApiResult<(Url, SsoAuth)> {\n        let scopes = CONFIG.sso_scopes_vec().into_iter().map(Scope::new);\n        let base64_state = data_encoding::BASE64.encode(state.to_string().as_bytes());\n\n        let client = Self::cached().await?;\n        let mut auth_req = client\n            .core_client\n            .authorize_url(\n                AuthenticationFlow::<CoreResponseType>::AuthorizationCode,\n                || CsrfToken::new(base64_state),\n                Nonce::new_random,\n            )\n            .add_scopes(scopes)\n            .add_extra_params(CONFIG.sso_authorize_extra_params_vec());\n\n        if CONFIG.sso_pkce() {\n            auth_req = auth_req\n                .add_extra_param::<&str, String>(\"code_challenge\", client_challenge.clone().into())\n                .add_extra_param(\"code_challenge_method\", \"S256\");\n        }\n\n        let (auth_url, _, nonce) = auth_req.url();\n        Ok((auth_url, SsoAuth::new(state, client_challenge, nonce.secret().clone(), redirect_uri)))\n    }\n\n    pub async fn exchange_code(\n        &self,\n        code: OIDCCode,\n        client_verifier: OIDCCodeVerifier,\n        sso_auth: &SsoAuth,\n    ) -> ApiResult<(\n        StandardTokenResponse<\n            IdTokenFields<\n                EmptyAdditionalClaims,\n                EmptyExtraTokenFields,\n                CoreGenderClaim,\n                CoreJweContentEncryptionAlgorithm,\n                CoreJwsSigningAlgorithm,\n            >,\n            CoreTokenType,\n        >,\n        IdTokenClaims<EmptyAdditionalClaims, CoreGenderClaim>,\n    )> {\n        let oidc_code = AuthorizationCode::new(code.to_string());\n\n        let mut exchange = self.core_client.exchange_code(oidc_code);\n\n        let verifier = PkceCodeVerifier::new(client_verifier.into());\n        if CONFIG.sso_pkce() {\n            exchange = exchange.set_pkce_verifier(verifier);\n        } else {\n            let challenge = PkceCodeChallenge::from_code_verifier_sha256(&verifier);\n            if challenge.as_str() != String::from(sso_auth.client_challenge.clone()) {\n                err!(format!(\"PKCE client challenge failed\"))\n                // Might need to notify admin ? how ?\n            }\n        }\n\n        match exchange.request_async(&self.http_client).await {\n            Err(err) => err!(format!(\"Failed to contact token endpoint: {:?}\", err)),\n            Ok(token_response) => {\n                let oidc_nonce = Nonce::new(sso_auth.nonce.clone());\n\n                let id_token = match token_response.extra_fields().id_token() {\n                    None => err!(\"Token response did not contain an id_token\"),\n                    Some(token) => token,\n                };\n\n                if CONFIG.sso_debug_tokens() {\n                    debug!(\"Id token: {}\", id_token.to_string());\n                    debug!(\"Access token: {}\", token_response.access_token().secret());\n                    debug!(\"Refresh token: {:?}\", token_response.refresh_token().map(|t| t.secret()));\n                    debug!(\"Expiration time: {:?}\", token_response.expires_in());\n                }\n\n                let id_claims = match id_token.claims(&self.vw_id_token_verifier(), &oidc_nonce) {\n                    Ok(claims) => claims.clone(),\n                    Err(err) => {\n                        Self::invalidate();\n                        err!(format!(\"Could not read id_token claims, {err}\"));\n                    }\n                };\n\n                Ok((token_response, id_claims))\n            }\n        }\n    }\n\n    pub async fn user_info(&self, access_token: AccessToken) -> ApiResult<CoreUserInfoClaims> {\n        match self.core_client.user_info(access_token, None).request_async(&self.http_client).await {\n            Err(err) => err!(format!(\"Request to user_info endpoint failed: {err}\")),\n            Ok(user_info) => Ok(user_info),\n        }\n    }\n\n    pub async fn check_validity(access_token: String) -> EmptyResult {\n        let client = Client::cached().await?;\n        match client.user_info(AccessToken::new(access_token)).await {\n            Err(err) => {\n                err_silent!(format!(\"Failed to retrieve user info, token has probably been invalidated: {err}\"))\n            }\n            Ok(_) => Ok(()),\n        }\n    }\n\n    pub fn vw_id_token_verifier(&self) -> CoreIdTokenVerifier<'_> {\n        let mut verifier = self.core_client.id_token_verifier();\n        if let Some(regex_str) = CONFIG.sso_audience_trusted() {\n            match Regex::new(&regex_str) {\n                Ok(regex) => {\n                    verifier = verifier.set_other_audience_verifier_fn(move |aud| regex.is_match(aud));\n                }\n                Err(err) => {\n                    error!(\"Failed to parse SSO_AUDIENCE_TRUSTED={regex_str} regex: {err}\");\n                }\n            }\n        }\n        verifier\n    }\n\n    pub async fn exchange_refresh_token(refresh_token: String) -> ApiResult<RefreshTokenResponse> {\n        let client = Client::cached().await?;\n\n        REFRESH_CACHE\n            .get_with(refresh_token.clone(), async move { client._exchange_refresh_token(refresh_token).await })\n            .await\n            .map_err(Into::into)\n    }\n\n    async fn _exchange_refresh_token(&self, refresh_token: String) -> Result<RefreshTokenResponse, String> {\n        let rt = RefreshToken::new(refresh_token);\n\n        match self.core_client.exchange_refresh_token(&rt).request_async(&self.http_client).await {\n            Err(err) => {\n                error!(\"Request to exchange_refresh_token endpoint failed: {err}\");\n                Err(format!(\"Request to exchange_refresh_token endpoint failed: {err}\"))\n            }\n            Ok(token_response) => Ok((\n                token_response.refresh_token().map(|token| token.secret().clone()),\n                token_response.access_token().secret().clone(),\n                token_response.expires_in(),\n            )),\n        }\n    }\n}\n\ntrait AuthorizationRequestExt<'a> {\n    fn add_extra_params<N: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(self, params: Vec<(N, V)>) -> Self;\n}\n\nimpl<'a, AD: AuthDisplay, P: AuthPrompt, RT: ResponseType> AuthorizationRequestExt<'a>\n    for AuthorizationRequest<'a, AD, P, RT>\n{\n    fn add_extra_params<N: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>>(mut self, params: Vec<(N, V)>) -> Self {\n        for (key, value) in params {\n            self = self.add_extra_param(key, value);\n        }\n        self\n    }\n}\n"
  },
  {
    "path": "src/static/global_domains.json",
    "content": "[\n  {\n    \"type\": 2,\n    \"domains\": [\n      \"ameritrade.com\",\n      \"tdameritrade.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 3,\n    \"domains\": [\n      \"bankofamerica.com\",\n      \"bofa.com\",\n      \"mbna.com\",\n      \"usecfo.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 4,\n    \"domains\": [\n      \"sprint.com\",\n      \"sprintpcs.com\",\n      \"nextel.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 0,\n    \"domains\": [\n      \"youtube.com\",\n      \"google.com\",\n      \"gmail.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 1,\n    \"domains\": [\n      \"apple.com\",\n      \"icloud.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 5,\n    \"domains\": [\n      \"wellsfargo.com\",\n      \"wf.com\",\n      \"wellsfargoadvisors.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 6,\n    \"domains\": [\n      \"mymerrill.com\",\n      \"ml.com\",\n      \"merrilledge.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 7,\n    \"domains\": [\n      \"accountonline.com\",\n      \"citi.com\",\n      \"citibank.com\",\n      \"citicards.com\",\n      \"citibankonline.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 8,\n    \"domains\": [\n      \"cnet.com\",\n      \"cnettv.com\",\n      \"com.com\",\n      \"download.com\",\n      \"news.com\",\n      \"search.com\",\n      \"upload.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 9,\n    \"domains\": [\n      \"bananarepublic.com\",\n      \"gap.com\",\n      \"oldnavy.com\",\n      \"piperlime.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 10,\n    \"domains\": [\n      \"bing.com\",\n      \"hotmail.com\",\n      \"live.com\",\n      \"microsoft.com\",\n      \"msn.com\",\n      \"passport.net\",\n      \"windows.com\",\n      \"microsoftonline.com\",\n      \"office.com\",\n      \"office365.com\",\n      \"microsoftstore.com\",\n      \"xbox.com\",\n      \"azure.com\",\n      \"windowsazure.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 11,\n    \"domains\": [\n      \"ua2go.com\",\n      \"ual.com\",\n      \"united.com\",\n      \"unitedwifi.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 12,\n    \"domains\": [\n      \"overture.com\",\n      \"yahoo.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 13,\n    \"domains\": [\n      \"zonealarm.com\",\n      \"zonelabs.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 14,\n    \"domains\": [\n      \"paypal.com\",\n      \"paypal-search.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 15,\n    \"domains\": [\n      \"avon.com\",\n      \"youravon.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 16,\n    \"domains\": [\n      \"diapers.com\",\n      \"soap.com\",\n      \"wag.com\",\n      \"yoyo.com\",\n      \"beautybar.com\",\n      \"casa.com\",\n      \"afterschool.com\",\n      \"vine.com\",\n      \"bookworm.com\",\n      \"look.com\",\n      \"vinemarket.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 17,\n    \"domains\": [\n      \"1800contacts.com\",\n      \"800contacts.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 18,\n    \"domains\": [\n      \"amazon.com\",\n      \"amazon.com.be\",\n      \"amazon.ae\",\n      \"amazon.ca\",\n      \"amazon.co.uk\",\n      \"amazon.com.au\",\n      \"amazon.com.br\",\n      \"amazon.com.mx\",\n      \"amazon.com.tr\",\n      \"amazon.de\",\n      \"amazon.es\",\n      \"amazon.fr\",\n      \"amazon.in\",\n      \"amazon.it\",\n      \"amazon.nl\",\n      \"amazon.pl\",\n      \"amazon.sa\",\n      \"amazon.se\",\n      \"amazon.sg\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 19,\n    \"domains\": [\n      \"cox.com\",\n      \"cox.net\",\n      \"coxbusiness.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 20,\n    \"domains\": [\n      \"mynortonaccount.com\",\n      \"norton.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 21,\n    \"domains\": [\n      \"verizon.com\",\n      \"verizon.net\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 22,\n    \"domains\": [\n      \"rakuten.com\",\n      \"buy.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 23,\n    \"domains\": [\n      \"siriusxm.com\",\n      \"sirius.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 24,\n    \"domains\": [\n      \"ea.com\",\n      \"origin.com\",\n      \"play4free.com\",\n      \"tiberiumalliance.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 25,\n    \"domains\": [\n      \"37signals.com\",\n      \"basecamp.com\",\n      \"basecamphq.com\",\n      \"highrisehq.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 26,\n    \"domains\": [\n      \"steampowered.com\",\n      \"steamcommunity.com\",\n      \"steamgames.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 27,\n    \"domains\": [\n      \"chart.io\",\n      \"chartio.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 28,\n    \"domains\": [\n      \"gotomeeting.com\",\n      \"citrixonline.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 29,\n    \"domains\": [\n      \"gogoair.com\",\n      \"gogoinflight.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 30,\n    \"domains\": [\n      \"mysql.com\",\n      \"oracle.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 31,\n    \"domains\": [\n      \"discover.com\",\n      \"discovercard.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 32,\n    \"domains\": [\n      \"dcu.org\",\n      \"dcu-online.org\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 33,\n    \"domains\": [\n      \"healthcare.gov\",\n      \"cuidadodesalud.gov\",\n      \"cms.gov\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 34,\n    \"domains\": [\n      \"pepco.com\",\n      \"pepcoholdings.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 35,\n    \"domains\": [\n      \"century21.com\",\n      \"21online.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 36,\n    \"domains\": [\n      \"comcast.com\",\n      \"comcast.net\",\n      \"xfinity.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 37,\n    \"domains\": [\n      \"cricketwireless.com\",\n      \"aiowireless.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 38,\n    \"domains\": [\n      \"mandtbank.com\",\n      \"mtb.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 39,\n    \"domains\": [\n      \"dropbox.com\",\n      \"getdropbox.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 40,\n    \"domains\": [\n      \"snapfish.com\",\n      \"snapfish.ca\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 41,\n    \"domains\": [\n      \"alibaba.com\",\n      \"aliexpress.com\",\n      \"aliyun.com\",\n      \"net.cn\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 42,\n    \"domains\": [\n      \"playstation.com\",\n      \"sonyentertainmentnetwork.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 43,\n    \"domains\": [\n      \"mercadolivre.com\",\n      \"mercadolivre.com.br\",\n      \"mercadolibre.com\",\n      \"mercadolibre.com.ar\",\n      \"mercadolibre.com.mx\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 44,\n    \"domains\": [\n      \"zendesk.com\",\n      \"zopim.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 45,\n    \"domains\": [\n      \"autodesk.com\",\n      \"tinkercad.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 46,\n    \"domains\": [\n      \"railnation.ru\",\n      \"railnation.de\",\n      \"rail-nation.com\",\n      \"railnation.gr\",\n      \"railnation.us\",\n      \"trucknation.de\",\n      \"traviangames.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 47,\n    \"domains\": [\n      \"wpcu.coop\",\n      \"wpcuonline.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 48,\n    \"domains\": [\n      \"mathletics.com\",\n      \"mathletics.com.au\",\n      \"mathletics.co.uk\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 49,\n    \"domains\": [\n      \"discountbank.co.il\",\n      \"telebank.co.il\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 50,\n    \"domains\": [\n      \"mi.com\",\n      \"xiaomi.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 52,\n    \"domains\": [\n      \"postepay.it\",\n      \"poste.it\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 51,\n    \"domains\": [\n      \"facebook.com\",\n      \"messenger.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 53,\n    \"domains\": [\n      \"skysports.com\",\n      \"skybet.com\",\n      \"skyvegas.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 54,\n    \"domains\": [\n      \"disneymoviesanywhere.com\",\n      \"go.com\",\n      \"disney.com\",\n      \"dadt.com\",\n      \"disneyplus.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 55,\n    \"domains\": [\n      \"pokemon-gl.com\",\n      \"pokemon.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 56,\n    \"domains\": [\n      \"myuv.com\",\n      \"uvvu.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 58,\n    \"domains\": [\n      \"mdsol.com\",\n      \"imedidata.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 57,\n    \"domains\": [\n      \"bank-yahav.co.il\",\n      \"bankhapoalim.co.il\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 59,\n    \"domains\": [\n      \"sears.com\",\n      \"shld.net\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 60,\n    \"domains\": [\n      \"xiami.com\",\n      \"alipay.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 61,\n    \"domains\": [\n      \"belkin.com\",\n      \"seedonk.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 62,\n    \"domains\": [\n      \"turbotax.com\",\n      \"intuit.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 63,\n    \"domains\": [\n      \"shopify.com\",\n      \"myshopify.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 64,\n    \"domains\": [\n      \"ebay.com\",\n      \"ebay.at\",\n      \"ebay.be\",\n      \"ebay.ca\",\n      \"ebay.ch\",\n      \"ebay.cn\",\n      \"ebay.co.jp\",\n      \"ebay.co.th\",\n      \"ebay.co.uk\",\n      \"ebay.com.au\",\n      \"ebay.com.hk\",\n      \"ebay.com.my\",\n      \"ebay.com.sg\",\n      \"ebay.com.tw\",\n      \"ebay.de\",\n      \"ebay.es\",\n      \"ebay.fr\",\n      \"ebay.ie\",\n      \"ebay.in\",\n      \"ebay.it\",\n      \"ebay.nl\",\n      \"ebay.ph\",\n      \"ebay.pl\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 65,\n    \"domains\": [\n      \"techdata.com\",\n      \"techdata.ch\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 66,\n    \"domains\": [\n      \"schwab.com\",\n      \"schwabplan.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 68,\n    \"domains\": [\n      \"tesla.com\",\n      \"teslamotors.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 69,\n    \"domains\": [\n      \"morganstanley.com\",\n      \"morganstanleyclientserv.com\",\n      \"stockplanconnect.com\",\n      \"ms.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 70,\n    \"domains\": [\n      \"taxact.com\",\n      \"taxactonline.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 71,\n    \"domains\": [\n      \"mediawiki.org\",\n      \"wikibooks.org\",\n      \"wikidata.org\",\n      \"wikimedia.org\",\n      \"wikinews.org\",\n      \"wikipedia.org\",\n      \"wikiquote.org\",\n      \"wikisource.org\",\n      \"wikiversity.org\",\n      \"wikivoyage.org\",\n      \"wiktionary.org\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 72,\n    \"domains\": [\n      \"airbnb.at\",\n      \"airbnb.be\",\n      \"airbnb.ca\",\n      \"airbnb.ch\",\n      \"airbnb.cl\",\n      \"airbnb.co.cr\",\n      \"airbnb.co.id\",\n      \"airbnb.co.in\",\n      \"airbnb.co.kr\",\n      \"airbnb.co.nz\",\n      \"airbnb.co.uk\",\n      \"airbnb.co.ve\",\n      \"airbnb.com\",\n      \"airbnb.com.ar\",\n      \"airbnb.com.au\",\n      \"airbnb.com.bo\",\n      \"airbnb.com.br\",\n      \"airbnb.com.bz\",\n      \"airbnb.com.co\",\n      \"airbnb.com.ec\",\n      \"airbnb.com.gt\",\n      \"airbnb.com.hk\",\n      \"airbnb.com.hn\",\n      \"airbnb.com.mt\",\n      \"airbnb.com.my\",\n      \"airbnb.com.ni\",\n      \"airbnb.com.pa\",\n      \"airbnb.com.pe\",\n      \"airbnb.com.py\",\n      \"airbnb.com.sg\",\n      \"airbnb.com.sv\",\n      \"airbnb.com.tr\",\n      \"airbnb.com.tw\",\n      \"airbnb.cz\",\n      \"airbnb.de\",\n      \"airbnb.dk\",\n      \"airbnb.es\",\n      \"airbnb.fi\",\n      \"airbnb.fr\",\n      \"airbnb.gr\",\n      \"airbnb.gy\",\n      \"airbnb.hu\",\n      \"airbnb.ie\",\n      \"airbnb.is\",\n      \"airbnb.it\",\n      \"airbnb.jp\",\n      \"airbnb.mx\",\n      \"airbnb.nl\",\n      \"airbnb.no\",\n      \"airbnb.pl\",\n      \"airbnb.pt\",\n      \"airbnb.ru\",\n      \"airbnb.se\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 73,\n    \"domains\": [\n      \"eventbrite.at\",\n      \"eventbrite.be\",\n      \"eventbrite.ca\",\n      \"eventbrite.ch\",\n      \"eventbrite.cl\",\n      \"eventbrite.co\",\n      \"eventbrite.co.nz\",\n      \"eventbrite.co.uk\",\n      \"eventbrite.com\",\n      \"eventbrite.com.ar\",\n      \"eventbrite.com.au\",\n      \"eventbrite.com.br\",\n      \"eventbrite.com.mx\",\n      \"eventbrite.com.pe\",\n      \"eventbrite.de\",\n      \"eventbrite.dk\",\n      \"eventbrite.es\",\n      \"eventbrite.fi\",\n      \"eventbrite.fr\",\n      \"eventbrite.hk\",\n      \"eventbrite.ie\",\n      \"eventbrite.it\",\n      \"eventbrite.nl\",\n      \"eventbrite.pt\",\n      \"eventbrite.se\",\n      \"eventbrite.sg\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 74,\n    \"domains\": [\n      \"stackexchange.com\",\n      \"superuser.com\",\n      \"stackoverflow.com\",\n      \"serverfault.com\",\n      \"mathoverflow.net\",\n      \"askubuntu.com\",\n      \"stackapps.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 75,\n    \"domains\": [\n      \"docusign.com\",\n      \"docusign.net\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 76,\n    \"domains\": [\n      \"envato.com\",\n      \"themeforest.net\",\n      \"codecanyon.net\",\n      \"videohive.net\",\n      \"audiojungle.net\",\n      \"graphicriver.net\",\n      \"photodune.net\",\n      \"3docean.net\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 77,\n    \"domains\": [\n      \"x10hosting.com\",\n      \"x10premium.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 78,\n    \"domains\": [\n      \"dnsomatic.com\",\n      \"opendns.com\",\n      \"umbrella.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 79,\n    \"domains\": [\n      \"cagreatamerica.com\",\n      \"canadaswonderland.com\",\n      \"carowinds.com\",\n      \"cedarfair.com\",\n      \"cedarpoint.com\",\n      \"dorneypark.com\",\n      \"kingsdominion.com\",\n      \"knotts.com\",\n      \"miadventure.com\",\n      \"schlitterbahn.com\",\n      \"valleyfair.com\",\n      \"visitkingsisland.com\",\n      \"worldsoffun.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 80,\n    \"domains\": [\n      \"ubnt.com\",\n      \"ui.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 81,\n    \"domains\": [\n      \"discordapp.com\",\n      \"discord.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 82,\n    \"domains\": [\n      \"netcup.de\",\n      \"netcup.eu\",\n      \"customercontrolpanel.de\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 83,\n    \"domains\": [\n      \"yandex.com\",\n      \"ya.ru\",\n      \"yandex.az\",\n      \"yandex.by\",\n      \"yandex.co.il\",\n      \"yandex.com.am\",\n      \"yandex.com.ge\",\n      \"yandex.com.tr\",\n      \"yandex.ee\",\n      \"yandex.fi\",\n      \"yandex.fr\",\n      \"yandex.kg\",\n      \"yandex.kz\",\n      \"yandex.lt\",\n      \"yandex.lv\",\n      \"yandex.md\",\n      \"yandex.pl\",\n      \"yandex.ru\",\n      \"yandex.tj\",\n      \"yandex.tm\",\n      \"yandex.ua\",\n      \"yandex.uz\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 84,\n    \"domains\": [\n      \"sonyentertainmentnetwork.com\",\n      \"sony.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 85,\n    \"domains\": [\n      \"proton.me\",\n      \"protonmail.com\",\n      \"protonvpn.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 86,\n    \"domains\": [\n      \"ubisoft.com\",\n      \"ubi.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 87,\n    \"domains\": [\n      \"transferwise.com\",\n      \"wise.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 88,\n    \"domains\": [\n      \"takeaway.com\",\n      \"just-eat.dk\",\n      \"just-eat.no\",\n      \"just-eat.fr\",\n      \"just-eat.ch\",\n      \"lieferando.de\",\n      \"lieferando.at\",\n      \"thuisbezorgd.nl\",\n      \"pyszne.pl\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 89,\n    \"domains\": [\n      \"atlassian.com\",\n      \"bitbucket.org\",\n      \"trello.com\",\n      \"statuspage.io\",\n      \"atlassian.net\",\n      \"jira.com\"\n    ],\n    \"excluded\": false\n  },\n  {\n    \"type\": 90,\n    \"domains\": [\n      \"pinterest.com\",\n      \"pinterest.com.au\",\n      \"pinterest.cl\",\n      \"pinterest.de\",\n      \"pinterest.dk\",\n      \"pinterest.es\",\n      \"pinterest.fr\",\n      \"pinterest.co.uk\",\n      \"pinterest.jp\",\n      \"pinterest.co.kr\",\n      \"pinterest.nz\",\n      \"pinterest.pt\",\n      \"pinterest.se\"\n    ],\n    \"excluded\": false\n  }\n]"
  },
  {
    "path": "src/static/scripts/404.css",
    "content": "body {\n    padding-top: 75px;\n}\n.vaultwarden-icon {\n    width: 48px;\n    height: 48px;\n    height: 32px;\n    width: auto;\n    margin: -5px 0 0 0;\n}\n.footer {\n    padding: 40px 0 40px 0;\n    border-top: 1px solid #dee2e6;\n}\n.container {\n    max-width: 980px;\n}\n.content {\n    padding-top: 20px;\n    padding-bottom: 20px;\n    padding-left: 15px;\n    padding-right: 15px;\n}\n.vw-404 {\n    max-width: 500px; width: 100%;\n}"
  },
  {
    "path": "src/static/scripts/admin.css",
    "content": "body {\n    padding-top: 75px;\n}\nimg {\n    width: 48px;\n    height: 48px;\n}\n.vaultwarden-icon {\n    height: 32px;\n    width: auto;\n    margin: -5px 0 0 0;\n}\n/* Special alert-row class to use Bootstrap v5.2+ variable colors */\n.alert-row {\n    --bs-alert-border: 1px solid var(--bs-alert-border-color);\n    color: var(--bs-alert-color);\n    background-color: var(--bs-alert-bg);\n    border: var(--bs-alert-border);\n}\n\n#users-table .vw-account-details {\n    min-width: 250px;\n}\n#users-table .vw-created-at, #users-table .vw-last-active {\n    min-width: 85px;\n    max-width: 85px;\n}\n#users-table .vw-entries, #orgs-table .vw-users, #orgs-table .vw-entries {\n    min-width: 35px;\n    max-width: 40px;\n}\n#orgs-table .vw-misc {\n    min-width: 65px;\n    max-width: 80px;\n}\n#users-table .vw-attachments, #orgs-table .vw-attachments {\n    min-width: 100px;\n    max-width: 130px;\n}\n#users-table .vw-actions, #orgs-table .vw-actions {\n    min-width: 155px;\n    max-width: 160px;\n}\n#users-table .vw-org-cell {\n    max-height: 120px;\n}\n#orgs-table .vw-org-details {\n    min-width: 285px;\n}\n\n#support-string {\n    height: 16rem;\n}\n.vw-copy-toast {\n    width: 15rem;\n}\n\n.abbr-badge {\n    cursor: help;\n}\n\n.theme-icon,\n.theme-icon-active {\n    display: inline-flex;\n    flex: 0 0 1.75em;\n    justify-content: center;\n}\n\n.theme-icon svg,\n.theme-icon-active svg {\n    width: 1.25em;\n    height: 1.25em;\n    min-width: 1.25em;\n    min-height: 1.25em;\n    display: block;\n    overflow: visible;\n}"
  },
  {
    "path": "src/static/scripts/admin.js",
    "content": "\"use strict\";\n/* eslint-env es2017, browser */\n/* exported BASE_URL, _post _delete */\n\nfunction getBaseUrl() {\n    // If the base URL is `https://vaultwarden.example.com/base/path/admin/`,\n    // `window.location.href` should have one of the following forms:\n    //\n    // - `https://vaultwarden.example.com/base/path/admin`\n    // - `https://vaultwarden.example.com/base/path/admin/#/some/route[?queryParam=...]`\n    //\n    // We want to get to just `https://vaultwarden.example.com/base/path`.\n    const pathname = window.location.pathname;\n    const adminPos = pathname.indexOf(\"/admin\");\n    const newPathname = pathname.substring(0, adminPos != -1 ? adminPos : pathname.length);\n    return `${window.location.origin}${newPathname}`;\n}\nconst BASE_URL = getBaseUrl();\n\nfunction reload() {\n    // Reload the page by setting the exact same href\n    // Using window.location.reload() could cause a repost.\n    window.location = window.location.href;\n}\n\nfunction msg(text, reload_page = true) {\n    text && alert(text);\n    reload_page && reload();\n}\n\nfunction _fetch(method, url, successMsg, errMsg, body, reload_page = true) {\n    let respStatus;\n    let respStatusText;\n    fetch(url, {\n        method: method,\n        body: body,\n        mode: \"same-origin\",\n        credentials: \"same-origin\",\n        headers: { \"Content-Type\": \"application/json\" }\n    }).then(resp => {\n        if (resp.ok) {\n            msg(successMsg, reload_page);\n            // Abuse the catch handler by setting error to false and continue\n            return Promise.reject({ error: false });\n        }\n        respStatus = resp.status;\n        respStatusText = resp.statusText;\n        return resp.text();\n    }).then(respText => {\n        try {\n            const respJson = JSON.parse(respText);\n            if (respJson.errorModel && respJson.errorModel.message) {\n                return respJson.errorModel.message;\n            } else {\n                return Promise.reject({ body: `${respStatus} - ${respStatusText}\\n\\nUnknown error`, error: true });\n            }\n        } catch (e) {\n            return Promise.reject({ body: `${respStatus} - ${respStatusText}\\n\\n[Catch] ${e}`, error: true });\n        }\n    }).then(apiMsg => {\n        msg(`${errMsg}\\n${apiMsg}`, reload_page);\n    }).catch(e => {\n        if (e.error === false) { return true; }\n        else { msg(`${errMsg}\\n${e.body}`, reload_page); }\n    });\n}\n\nfunction _post(url, successMsg, errMsg, body, reload_page = true) {\n    return _fetch(\"POST\", url, successMsg, errMsg, body, reload_page);\n}\n\nfunction _delete(url, successMsg, errMsg, body, reload_page = true) {\n    return _fetch(\"DELETE\", url, successMsg, errMsg, body, reload_page);\n}\n\n// Bootstrap Theme Selector\nconst getStoredTheme = () => localStorage.getItem(\"theme\");\nconst setStoredTheme = theme => localStorage.setItem(\"theme\", theme);\n\nconst getPreferredTheme = () => {\n    const storedTheme = getStoredTheme();\n    if (storedTheme) {\n        return storedTheme;\n    }\n\n    return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n};\n\nconst setTheme = theme => {\n    if (theme === \"auto\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n        document.documentElement.setAttribute(\"data-bs-theme\", \"dark\");\n    } else {\n        document.documentElement.setAttribute(\"data-bs-theme\", theme);\n    }\n};\n\nsetTheme(getPreferredTheme());\n\nconst showActiveTheme = (theme, focus = false) => {\n    const themeSwitcher = document.querySelector(\"#bd-theme\");\n\n    if (!themeSwitcher) {\n        return;\n    }\n\n    const themeSwitcherText = document.querySelector(\"#bd-theme-text\");\n    const activeThemeIcon = document.querySelector(\".theme-icon-active use\");\n    const btnToActive = document.querySelector(`[data-bs-theme-value=\"${theme}\"]`);\n    if (!btnToActive) {\n        return;\n    }\n    const btnIconUse = btnToActive ? btnToActive.querySelector(\"[data-theme-icon-use]\") : null;\n    const iconHref = btnIconUse ? btnIconUse.getAttribute(\"href\") || btnIconUse.getAttribute(\"xlink:href\") : null;\n\n    document.querySelectorAll(\"[data-bs-theme-value]\").forEach(element => {\n        element.classList.remove(\"active\");\n        element.setAttribute(\"aria-pressed\", \"false\");\n    });\n\n    btnToActive.classList.add(\"active\");\n    btnToActive.setAttribute(\"aria-pressed\", \"true\");\n\n    if (iconHref && activeThemeIcon) {\n        activeThemeIcon.setAttribute(\"href\", iconHref);\n        activeThemeIcon.setAttribute(\"xlink:href\", iconHref);\n    }\n\n    const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;\n    themeSwitcher.setAttribute(\"aria-label\", themeSwitcherLabel);\n\n    if (focus) {\n        themeSwitcher.focus();\n    }\n};\n\nwindow.matchMedia(\"(prefers-color-scheme: dark)\").addEventListener(\"change\", () => {\n    const storedTheme = getStoredTheme();\n    if (storedTheme !== \"light\" && storedTheme !== \"dark\") {\n        setTheme(getPreferredTheme());\n    }\n});\n\n\n// onLoad events\ndocument.addEventListener(\"DOMContentLoaded\", (/*event*/) => {\n    showActiveTheme(getPreferredTheme());\n\n    document.querySelectorAll(\"[data-bs-theme-value]\")\n        .forEach(toggle => {\n            toggle.addEventListener(\"click\", () => {\n                const theme = toggle.getAttribute(\"data-bs-theme-value\");\n                setStoredTheme(theme);\n                setTheme(theme);\n                showActiveTheme(theme, true);\n            });\n        });\n\n    // get current URL path and assign \"active\" class to the correct nav-item\n    const pathname = window.location.pathname;\n    if (pathname === \"\") return;\n    const navItem = document.querySelectorAll(`.navbar-nav .nav-item a[href=\"${pathname}\"]`);\n    if (navItem.length === 1) {\n        navItem[0].className = navItem[0].className + \" active\";\n        navItem[0].setAttribute(\"aria-current\", \"page\");\n    }\n});\n"
  },
  {
    "path": "src/static/scripts/admin_diagnostics.js",
    "content": "\"use strict\";\n/* eslint-env es2017, browser */\n/* global BASE_URL:readable, bootstrap:readable */\n\nvar dnsCheck = false;\nvar timeCheck = false;\nvar ntpTimeCheck = false;\nvar domainCheck = false;\nvar httpsCheck = false;\nvar websocketCheck = false;\nvar httpResponseCheck = false;\n\n// ================================\n// Date & Time Check\nconst d = new Date();\nconst year = d.getUTCFullYear();\nconst month = String(d.getUTCMonth()+1).padStart(2, \"0\");\nconst day = String(d.getUTCDate()).padStart(2, \"0\");\nconst hour = String(d.getUTCHours()).padStart(2, \"0\");\nconst minute = String(d.getUTCMinutes()).padStart(2, \"0\");\nconst seconds = String(d.getUTCSeconds()).padStart(2, \"0\");\nconst browserUTC = `${year}-${month}-${day} ${hour}:${minute}:${seconds} UTC`;\n\n// ================================\n// Check if the output is a valid IP\nfunction isValidIp(ip) {\n    const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;\n    const ipv6Regex = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|((?:[a-fA-F0-9]{1,4}:){1,7}:|:(:[a-fA-F0-9]{1,4}){1,7}|[a-fA-F0-9]{1,4}:((:[a-fA-F0-9]{1,4}){1,6}))$/;\n    return ipv4Regex.test(ip) || ipv6Regex.test(ip);\n}\n\nfunction checkVersions(platform, installed, latest, commit=null, compare_order=0) {\n    if (installed === \"-\" || latest === \"-\") {\n        document.getElementById(`${platform}-failed`).classList.remove(\"d-none\");\n        return;\n    }\n\n    // Only check basic versions, no commit revisions\n    if (commit === null || installed.indexOf(\"-\") === -1) {\n        if (platform === \"web\" && compare_order === 1) {\n            document.getElementById(`${platform}-prerelease`).classList.remove(\"d-none\");\n        } else if (installed == latest) {\n            document.getElementById(`${platform}-success`).classList.remove(\"d-none\");\n        } else {\n            document.getElementById(`${platform}-warning`).classList.remove(\"d-none\");\n        }\n    } else {\n        // Check if this is a branched version.\n        const branchRegex = /(?:\\s)\\((.*?)\\)/;\n        const branchMatch = installed.match(branchRegex);\n        if (branchMatch !== null) {\n            document.getElementById(`${platform}-branch`).classList.remove(\"d-none\");\n        }\n\n        // This will remove branch info and check if there is a commit hash\n        const installedRegex = /(\\d+\\.\\d+\\.\\d+)-(\\w+)/;\n        const instMatch = installed.match(installedRegex);\n\n        // It could be that a new tagged version has the same commit hash.\n        // In this case the version is the same but only the number is different\n        if (instMatch !== null) {\n            if (instMatch[2] === commit) {\n                // The commit hashes are the same, so latest version is installed\n                document.getElementById(`${platform}-success`).classList.remove(\"d-none\");\n                return;\n            }\n        }\n\n        if (installed === latest) {\n            document.getElementById(`${platform}-success`).classList.remove(\"d-none\");\n        } else {\n            document.getElementById(`${platform}-warning`).classList.remove(\"d-none\");\n        }\n    }\n}\n\n// ================================\n// Generate support string to be pasted on github or the forum\nasync function generateSupportString(event, dj) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    let supportString = \"### Your environment (Generated via diagnostics page)\\n\\n\";\n\n    supportString += `* Vaultwarden version: v${dj.current_release}\\n`;\n    supportString += `* Web-vault version: v${dj.active_web_release}\\n`;\n    supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\\n`;\n    supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\\n`;\n    supportString += `* Database type: ${dj.db_type}\\n`;\n    supportString += `* Database version: ${dj.db_version}\\n`;\n    supportString += `* Uses config.json: ${dj.overrides !== \"\"}\\n`;\n    supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\\n`;\n    if (dj.ip_header_exists) {\n        supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\\n`;\n    }\n    supportString += `* Internet access: ${dj.has_http_access}\\n`;\n    supportString += `* Internet access via a proxy: ${dj.uses_proxy}\\n`;\n    supportString += `* DNS Check: ${dnsCheck}\\n`;\n    if (dj.tz_env !== \"\") {\n        supportString += `* TZ environment: ${dj.tz_env}\\n`;\n    }\n    supportString += `* Browser/Server Time Check: ${timeCheck}\\n`;\n    supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\\n`;\n    supportString += `* Domain Configuration Check: ${domainCheck}\\n`;\n    supportString += `* HTTPS Check: ${httpsCheck}\\n`;\n    if (dj.enable_websocket) {\n        supportString += `* Websocket Check: ${websocketCheck}\\n`;\n    } else {\n        supportString += \"* Websocket Check: disabled\\n\";\n    }\n    supportString += `* HTTP Response Checks: ${httpResponseCheck}\\n`;\n\n    const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, {\n        \"headers\": { \"Accept\": \"application/json\" }\n    });\n    if (!jsonResponse.ok) {\n        alert(\"Generation failed: \" + jsonResponse.statusText);\n        throw new Error(jsonResponse);\n    }\n    const configJson = await jsonResponse.json();\n\n    // Start Config and Details section within a details block which is collapsed by default\n    supportString += \"\\n### Config & Details (Generated via diagnostics page)\\n\\n\";\n    supportString += \"<details><summary>Show Config & Details</summary>\\n\";\n\n    // Add overrides if they exists\n    if (dj.overrides != \"\") {\n        supportString += `\\n**Environment settings which are overridden:** ${dj.overrides}\\n`;\n    }\n\n    // Add http response check messages if they exists\n    if (httpResponseCheck === false) {\n        supportString += \"\\n**Failed HTTP Checks:**\\n\";\n        // We use `innerText` here since that will convert <br> into new-lines\n        supportString += \"\\n```yaml\\n\" + document.getElementById(\"http-response-errors\").innerText.trim() + \"\\n```\\n\";\n    }\n\n    // Add the current config in json form\n    supportString += \"\\n**Config:**\\n\";\n    supportString += \"\\n```json\\n\" + JSON.stringify(configJson, undefined, 2) + \"\\n```\\n\";\n\n    supportString += \"\\n</details>\\n\";\n\n    // Add the support string to the textbox so it can be viewed and copied\n    document.getElementById(\"support-string\").textContent = supportString;\n    document.getElementById(\"support-string\").classList.remove(\"d-none\");\n    document.getElementById(\"copy-support\").classList.remove(\"d-none\");\n}\n\nfunction copyToClipboard(event) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const supportStr = document.getElementById(\"support-string\").textContent;\n    const tmpCopyEl = document.createElement(\"textarea\");\n\n    tmpCopyEl.setAttribute(\"id\", \"copy-support-string\");\n    tmpCopyEl.setAttribute(\"readonly\", \"\");\n    tmpCopyEl.value = supportStr;\n    tmpCopyEl.style.position = \"absolute\";\n    tmpCopyEl.style.left = \"-9999px\";\n    document.body.appendChild(tmpCopyEl);\n    tmpCopyEl.select();\n    document.execCommand(\"copy\");\n    tmpCopyEl.remove();\n\n    new bootstrap.Toast(\"#toastClipboardCopy\").show();\n}\n\nfunction checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) {\n    const timeDrift = (\n        Date.parse(utcTimeA.replace(\" \", \"T\").replace(\" UTC\", \"\")) -\n        Date.parse(utcTimeB.replace(\" \", \"T\").replace(\" UTC\", \"\"))\n    ) / 1000;\n    if (timeDrift > 15 || timeDrift < -15) {\n        document.getElementById(`${statusPrefix}-warning`).classList.remove(\"d-none\");\n        return false;\n    } else {\n        document.getElementById(`${statusPrefix}-success`).classList.remove(\"d-none\");\n        return true;\n    }\n}\n\nfunction checkDomain(browserURL, serverURL) {\n    if (serverURL == browserURL) {\n        document.getElementById(\"domain-success\").classList.remove(\"d-none\");\n        domainCheck = true;\n    } else {\n        document.getElementById(\"domain-warning\").classList.remove(\"d-none\");\n    }\n\n    // Check for HTTPS at domain-server-string\n    if (serverURL.startsWith(\"https://\") ) {\n        document.getElementById(\"https-success\").classList.remove(\"d-none\");\n        httpsCheck = true;\n    } else {\n        document.getElementById(\"https-warning\").classList.remove(\"d-none\");\n    }\n}\n\nfunction initVersionCheck(dj) {\n    const serverInstalled = dj.current_release;\n    const serverLatest = dj.latest_release;\n    const serverLatestCommit = dj.latest_commit;\n\n    if (serverInstalled.indexOf(\"-\") !== -1 && serverLatest !== \"-\" && serverLatestCommit !== \"-\") {\n        document.getElementById(\"server-latest-commit\").classList.remove(\"d-none\");\n    }\n    checkVersions(\"server\", serverInstalled, serverLatest, serverLatestCommit);\n\n    const webInstalled = dj.active_web_release;\n    const webLatest = dj.latest_web_release;\n    checkVersions(\"web\", webInstalled, webLatest, null, dj.web_vault_compare);\n}\n\nfunction checkDns(dns_resolved) {\n    if (isValidIp(dns_resolved)) {\n        document.getElementById(\"dns-success\").classList.remove(\"d-none\");\n        dnsCheck = true;\n    } else {\n        document.getElementById(\"dns-warning\").classList.remove(\"d-none\");\n    }\n}\n\nasync function fetchCheckUrl(url) {\n    try {\n        const response = await fetch(url);\n        return { headers: response.headers, status: response.status, text: await response.text() };\n    } catch (error) {\n        console.error(`Error fetching ${url}: ${error}`);\n        return { error };\n    }\n}\n\nfunction checkSecurityHeaders(headers, omit) {\n    let securityHeaders = {\n        \"x-frame-options\": [\"SAMEORIGIN\"],\n        \"x-content-type-options\": [\"nosniff\"],\n        \"referrer-policy\": [\"same-origin\"],\n        \"x-xss-protection\": [\"0\"],\n        \"x-robots-tag\": [\"noindex\", \"nofollow\"],\n        \"cross-origin-resource-policy\": [\"same-origin\"],\n        \"content-security-policy\": [\n            \"default-src 'none'\",\n            \"font-src 'self'\",\n            \"manifest-src 'self'\",\n            \"base-uri 'self'\",\n            \"form-action 'self'\",\n            \"object-src 'self' blob:\",\n            \"script-src 'self' 'wasm-unsafe-eval'\",\n            \"style-src 'self' 'unsafe-inline'\",\n            \"child-src 'self' https://*.duosecurity.com https://*.duofederal.com\",\n            \"frame-src 'self' https://*.duosecurity.com https://*.duofederal.com\",\n            \"frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://*\",\n            \"img-src 'self' data: https://haveibeenpwned.com\",\n            \"connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory https://app.simplelogin.io/api/ https://app.addy.io/api/ https://api.fastmail.com/ https://api.forwardemail.net\",\n        ]\n    };\n\n    let messages = [];\n    for (let header in securityHeaders) {\n        // Skip some headers for specific endpoints if needed\n        if (typeof omit === \"object\" && omit.includes(header) === true) {\n            continue;\n        }\n        // If the header exists, check if the contents matches what we expect it to be\n        let headerValue = headers.get(header);\n        if (headerValue !== null) {\n            securityHeaders[header].forEach((expectedValue) => {\n                if (headerValue.indexOf(expectedValue) === -1) {\n                    messages.push(`'${header}' does not contain '${expectedValue}'`);\n                }\n            });\n        } else {\n            messages.push(`'${header}' is missing!`);\n        }\n    }\n    return messages;\n}\n\nasync function checkHttpResponse() {\n    const [apiConfig, webauthnConnector, notFound, notFoundApi, badRequest, unauthorized, forbidden] = await Promise.all([\n        fetchCheckUrl(`${BASE_URL}/api/config`),\n        fetchCheckUrl(`${BASE_URL}/webauthn-connector.html`),\n        fetchCheckUrl(`${BASE_URL}/admin/does-not-exist`),\n        fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=404`),\n        fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=400`),\n        fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=401`),\n        fetchCheckUrl(`${BASE_URL}/admin/diagnostics/http?code=403`),\n    ]);\n\n    const respErrorElm = document.getElementById(\"http-response-errors\");\n\n    // Check and validate the default API header responses\n    let apiErrors = checkSecurityHeaders(apiConfig.headers);\n    if (apiErrors.length >= 1) {\n        respErrorElm.innerHTML += \"<b>API calls:</b><br>\";\n        apiErrors.forEach((errMsg) => {\n            respErrorElm.innerHTML += `<b>Header:</b> ${errMsg}<br>`;\n        });\n    }\n\n    // Check the special `-connector.html` headers, these should have some headers omitted.\n    const omitConnectorHeaders = [\"x-frame-options\", \"content-security-policy\"];\n    let connectorErrors = checkSecurityHeaders(webauthnConnector.headers, omitConnectorHeaders);\n    omitConnectorHeaders.forEach((header) => {\n        if (webauthnConnector.headers.get(header) !== null) {\n            connectorErrors.push(`'${header}' is present while it should not`);\n        }\n    });\n    if (connectorErrors.length >= 1) {\n        respErrorElm.innerHTML += \"<b>2FA Connector calls:</b><br>\";\n        connectorErrors.forEach((errMsg) => {\n            respErrorElm.innerHTML += `<b>Header:</b> ${errMsg}<br>`;\n        });\n    }\n\n    // Check specific error code responses if they are not re-written by a reverse proxy\n    let responseErrors = [];\n    if (notFound.status !== 404 || notFound.text.indexOf(\"return to the web-vault\") === -1) {\n        responseErrors.push(\"404 (Not Found) HTML is invalid\");\n    }\n\n    if (notFoundApi.status !== 404 || notFoundApi.text.indexOf(\"\\\"message\\\":\\\"Testing error 404 response\\\",\") === -1) {\n        responseErrors.push(\"404 (Not Found) JSON is invalid\");\n    }\n\n    if (badRequest.status !== 400 || badRequest.text.indexOf(\"\\\"message\\\":\\\"Testing error 400 response\\\",\") === -1) {\n        responseErrors.push(\"400 (Bad Request) is invalid\");\n    }\n\n    if (unauthorized.status !== 401 || unauthorized.text.indexOf(\"\\\"message\\\":\\\"Testing error 401 response\\\",\") === -1) {\n        responseErrors.push(\"401 (Unauthorized) is invalid\");\n    }\n\n    if (forbidden.status !== 403 || forbidden.text.indexOf(\"\\\"message\\\":\\\"Testing error 403 response\\\",\") === -1) {\n        responseErrors.push(\"403 (Forbidden) is invalid\");\n    }\n\n    if (responseErrors.length >= 1) {\n        respErrorElm.innerHTML += \"<b>HTTP error responses:</b><br>\";\n        responseErrors.forEach((errMsg) => {\n            respErrorElm.innerHTML += `<b>Response to:</b> ${errMsg}<br>`;\n        });\n    }\n\n    if (responseErrors.length >= 1 || connectorErrors.length >= 1 || apiErrors.length >= 1) {\n        document.getElementById(\"http-response-warning\").classList.remove(\"d-none\");\n    } else {\n        httpResponseCheck = true;\n        document.getElementById(\"http-response-success\").classList.remove(\"d-none\");\n    }\n}\n\nasync function fetchWsUrl(wsUrl) {\n    return new Promise((resolve, reject) => {\n        try {\n            const ws = new WebSocket(wsUrl);\n            ws.onopen = () => {\n                ws.close();\n                resolve(true);\n            };\n\n            ws.onerror = () => {\n                reject(false);\n            };\n        } catch (_) {\n            reject(false);\n        }\n    });\n}\n\nasync function checkWebsocketConnection() {\n    // Test Websocket connections via the anonymous (login with device) connection\n    const isConnected = await fetchWsUrl(`${BASE_URL}/notifications/anonymous-hub?token=admin-diagnostics`).catch(() => false);\n    if (isConnected) {\n        websocketCheck = true;\n        document.getElementById(\"websocket-success\").classList.remove(\"d-none\");\n    } else {\n        document.getElementById(\"websocket-error\").classList.remove(\"d-none\");\n    }\n}\n\nfunction init(dj) {\n    // Time check\n    document.getElementById(\"time-browser-string\").textContent = browserUTC;\n\n    // Check if we were able to fetch a valid NTP Time\n    // If so, compare both browser and server with NTP\n    // Else, compare browser and server.\n    if (dj.ntp_time.indexOf(\"UTC\") !== -1) {\n        timeCheck = checkTimeDrift(dj.server_time, browserUTC, \"time\");\n        checkTimeDrift(dj.ntp_time, browserUTC, \"ntp-browser\");\n        ntpTimeCheck = checkTimeDrift(dj.ntp_time, dj.server_time, \"ntp-server\");\n    } else {\n        timeCheck = checkTimeDrift(dj.server_time, browserUTC, \"time\");\n        ntpTimeCheck = \"n/a\";\n    }\n\n    // Domain check\n    const browserURL = location.href.toLowerCase();\n    document.getElementById(\"domain-browser-string\").textContent = browserURL;\n    checkDomain(browserURL, dj.admin_url.toLowerCase());\n\n    // Version check\n    initVersionCheck(dj);\n\n    // DNS Check\n    checkDns(dj.dns_resolved);\n\n    checkHttpResponse();\n\n    if (dj.enable_websocket) {\n        checkWebsocketConnection();\n    }\n}\n\n// onLoad events\ndocument.addEventListener(\"DOMContentLoaded\", (event) => {\n    const diag_json = JSON.parse(document.getElementById(\"diagnostics_json\").textContent);\n    init(diag_json);\n\n    const btnGenSupport = document.getElementById(\"gen-support\");\n    if (btnGenSupport) {\n        btnGenSupport.addEventListener(\"click\", () => {\n            generateSupportString(event, diag_json);\n        });\n    }\n    const btnCopySupport = document.getElementById(\"copy-support\");\n    if (btnCopySupport) {\n        btnCopySupport.addEventListener(\"click\", copyToClipboard);\n    }\n});"
  },
  {
    "path": "src/static/scripts/admin_organizations.js",
    "content": "\"use strict\";\n/* eslint-env es2017, browser, jquery */\n/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */\n\nfunction deleteOrganization(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const org_uuid = event.target.dataset.vwOrgUuid;\n    const org_name = event.target.dataset.vwOrgName;\n    const billing_email = event.target.dataset.vwBillingEmail;\n    if (!org_uuid) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n\n    // First make sure the user wants to delete this organization\n    const continueDelete = confirm(`WARNING: All data of this organization (${org_name}) will be lost!\\nMake sure you have a backup, this cannot be undone!`);\n    if (continueDelete == true) {\n        const input_org_uuid = prompt(`To delete the organization \"${org_name} (${billing_email})\", please type the organization uuid below.`);\n        if (input_org_uuid != null) {\n            if (input_org_uuid == org_uuid) {\n                _post(`${BASE_URL}/admin/organizations/${org_uuid}/delete`,\n                    \"Organization deleted correctly\",\n                    \"Error deleting organization\"\n                );\n            } else {\n                alert(\"Wrong organization uuid, please try again\");\n            }\n        }\n    }\n}\n\nfunction initActions() {\n    document.querySelectorAll(\"button[vw-delete-organization]\").forEach(btn => {\n        btn.addEventListener(\"click\", deleteOrganization);\n    });\n\n    if (jdenticon) {\n        jdenticon();\n    }\n}\n\n// onLoad events\ndocument.addEventListener(\"DOMContentLoaded\", (/*event*/) => {\n    jQuery(\"#orgs-table\").DataTable({\n        \"drawCallback\": function() {\n            initActions();\n        },\n        \"stateSave\": true,\n        \"responsive\": true,\n        \"lengthMenu\": [\n            [-1, 5, 10, 25, 50],\n            [\"All\", 5, 10, 25, 50]\n        ],\n        \"pageLength\": -1, // Default show all\n        \"columnDefs\": [{\n            \"targets\": [4,5],\n            \"searchable\": false,\n            \"orderable\": false\n        }]\n    });\n\n    // Add click events for organization actions\n    initActions();\n\n    const btnReload = document.getElementById(\"reload\");\n    if (btnReload) {\n        btnReload.addEventListener(\"click\", reload);\n    }\n});"
  },
  {
    "path": "src/static/scripts/admin_settings.js",
    "content": "\"use strict\";\n/* eslint-env es2017, browser */\n/* global _post:readable, BASE_URL:readable */\n\nfunction smtpTest(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    if (formHasChanges(config_form)) {\n        alert(\"Config has been changed but not yet saved.\\nPlease save the changes first before sending a test email.\");\n        return false;\n    }\n\n    const test_email = document.getElementById(\"smtp-test-email\");\n\n    // Do a very very basic email address check.\n    if (test_email.value.match(/\\S+@\\S+/i) === null) {\n        test_email.parentElement.classList.add(\"was-validated\");\n        return false;\n    }\n\n    const data = JSON.stringify({ \"email\": test_email.value });\n    _post(`${BASE_URL}/admin/test/smtp`,\n        \"SMTP Test email sent correctly\",\n        \"Error sending SMTP test email\",\n        data, false\n    );\n}\n\nfunction getFormData() {\n    let data = {};\n\n    document.querySelectorAll(\".conf-checkbox\").forEach(function (e) {\n        data[e.name] = e.checked;\n    });\n\n    document.querySelectorAll(\".conf-number\").forEach(function (e) {\n        data[e.name] = e.value ? +e.value : null;\n    });\n\n    document.querySelectorAll(\".conf-text, .conf-password\").forEach(function (e) {\n        data[e.name] = e.value || null;\n    });\n    return data;\n}\n\nfunction saveConfig(event) {\n    const data = JSON.stringify(getFormData());\n    _post(`${BASE_URL}/admin/config`,\n        \"Config saved correctly\",\n        \"Error saving config\",\n        data\n    );\n    event.preventDefault();\n}\n\nfunction deleteConf(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const input = prompt(\n        \"This will remove all user configurations, and restore the defaults and the \" +\n        \"values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:\"\n    );\n    if (input === \"DELETE\") {\n        _post(`${BASE_URL}/admin/config/delete`,\n            \"Config deleted correctly\",\n            \"Error deleting config\"\n        );\n    } else {\n        alert(\"Wrong input, please try again\");\n    }\n}\n\nfunction backupDatabase(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    _post(`${BASE_URL}/admin/config/backup_db`,\n        \"Backup created successfully\",\n        \"Error creating backup\", null, false\n    );\n}\n\n// Two functions to help check if there were changes to the form fields\n// Useful for example during the smtp test to prevent people from clicking save before testing there new settings\nfunction initChangeDetection(form) {\n    const ignore_fields = [\"smtp-test-email\"];\n    Array.from(form).forEach((el) => {\n        if (! ignore_fields.includes(el.id)) {\n            el.dataset.origValue = el.value;\n        }\n    });\n}\n\nfunction formHasChanges(form) {\n    return Array.from(form).some(el => \"origValue\" in el.dataset && ( el.dataset.origValue !== el.value));\n}\n\n// This function will prevent submitting a from when someone presses enter.\nfunction preventFormSubmitOnEnter(form) {\n    if (form) {\n        form.addEventListener(\"keypress\", (event) => {\n            if (event.key == \"Enter\") {\n                event.preventDefault();\n            }\n        });\n    }\n}\n\n// This function will hook into the smtp-test-email input field and will call the smtpTest() function when enter is pressed.\nfunction submitTestEmailOnEnter() {\n    const smtp_test_email_input = document.getElementById(\"smtp-test-email\");\n    if (smtp_test_email_input) {\n        smtp_test_email_input.addEventListener(\"keypress\", (event) => {\n            if (event.key == \"Enter\") {\n                event.preventDefault();\n                smtpTest(event);\n            }\n        });\n    }\n}\n\n// Colorize some settings which are high risk\nfunction colorRiskSettings() {\n    const risk_items = document.getElementsByClassName(\"col-form-label\");\n    Array.from(risk_items).forEach((el) => {\n        if (el.textContent.toLowerCase().includes(\"risks\") ) {\n            el.parentElement.className += \" alert-danger\";\n        }\n    });\n}\n\nfunction toggleVis(event) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const elem = document.getElementById(event.target.dataset.vwPwToggle);\n    const type = elem.getAttribute(\"type\");\n    if (type === \"text\") {\n        elem.setAttribute(\"type\", \"password\");\n    } else {\n        elem.setAttribute(\"type\", \"text\");\n    }\n}\n\nfunction masterCheck(check_id, inputs_query) {\n    function onChanged(checkbox, inputs_query) {\n        return function _fn() {\n            document.querySelectorAll(inputs_query).forEach(function (e) { e.disabled = !checkbox.checked; });\n            checkbox.disabled = false;\n        };\n    }\n\n    const checkbox = document.getElementById(check_id);\n    if (checkbox) {\n        const onChange = onChanged(checkbox, inputs_query);\n        onChange(); // Trigger the event initially\n        checkbox.addEventListener(\"change\", onChange);\n    }\n}\n\n// This will check if the ADMIN_TOKEN is not a Argon2 hashed value.\n// Else it will show a warning, unless someone has closed it.\n// Then it will not show this warning for 30 days.\nfunction checkAdminToken() {\n    const admin_token = document.getElementById(\"input_admin_token\");\n    const disable_admin_token = document.getElementById(\"input_disable_admin_token\");\n    if (!disable_admin_token.checked && !admin_token.value.startsWith(\"$argon2\")) {\n        // Check if the warning has been closed before and 30 days have passed\n        const admin_token_warning_closed = localStorage.getItem(\"admin_token_warning_closed\");\n        if (admin_token_warning_closed !== null) {\n            const closed_date = new Date(parseInt(admin_token_warning_closed));\n            const current_date = new Date();\n            const thirtyDays = 1000*60*60*24*30;\n            if (current_date - closed_date < thirtyDays) {\n                return;\n            }\n        }\n\n        // When closing the alert, store the current date/time in the browser\n        const admin_token_warning = document.getElementById(\"admin_token_warning\");\n        admin_token_warning.addEventListener(\"closed.bs.alert\", function() {\n            const d = new Date();\n            localStorage.setItem(\"admin_token_warning_closed\", d.getTime());\n        });\n\n        // Display the warning\n        admin_token_warning.classList.remove(\"d-none\");\n    }\n}\n\n// This will check for specific configured values, and when needed will show a warning div\nfunction showWarnings() {\n    checkAdminToken();\n}\n\nconst config_form = document.getElementById(\"config-form\");\n\n// onLoad events\ndocument.addEventListener(\"DOMContentLoaded\", (/*event*/) => {\n    initChangeDetection(config_form);\n    // Prevent enter to submitting the form and save the config.\n    // Users need to really click on save, this also to prevent accidental submits.\n    preventFormSubmitOnEnter(config_form);\n\n    submitTestEmailOnEnter();\n    colorRiskSettings();\n\n    document.querySelectorAll(\"input[id^='input__enable_']\").forEach(group_toggle => {\n        const input_id = group_toggle.id.replace(\"input__enable_\", \"#g_\");\n        masterCheck(group_toggle.id, `${input_id} input`);\n    });\n\n    document.querySelectorAll(\"button[data-vw-pw-toggle]\").forEach(password_toggle_btn => {\n        password_toggle_btn.addEventListener(\"click\", toggleVis);\n    });\n\n    const btnBackupDatabase = document.getElementById(\"backupDatabase\");\n    if (btnBackupDatabase) {\n        btnBackupDatabase.addEventListener(\"click\", backupDatabase);\n    }\n    const btnDeleteConf = document.getElementById(\"deleteConf\");\n    if (btnDeleteConf) {\n        btnDeleteConf.addEventListener(\"click\", deleteConf);\n    }\n    const btnSmtpTest = document.getElementById(\"smtpTest\");\n    if (btnSmtpTest) {\n        btnSmtpTest.addEventListener(\"click\", smtpTest);\n    }\n\n    config_form.addEventListener(\"submit\", saveConfig);\n\n    showWarnings();\n});"
  },
  {
    "path": "src/static/scripts/admin_users.js",
    "content": "\"use strict\";\n/* eslint-env es2017, browser, jquery */\n/* global _post:readable, _delete:readable BASE_URL:readable, reload:readable, jdenticon:readable */\n\nfunction deleteUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const input_email = prompt(`To delete user \"${email}\", please type the email below`);\n    if (input_email != null) {\n        if (input_email == email) {\n            _post(`${BASE_URL}/admin/users/${id}/delete`,\n                \"User deleted correctly\",\n                \"Error deleting user\"\n            );\n        } else {\n            alert(\"Wrong email, please try again\");\n        }\n    }\n}\n\nfunction deleteSSOUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const input_email = prompt(`To delete user \"${email}\" SSO association, please type the email below`);\n    if (input_email != null) {\n        if (input_email == email) {\n            _delete(`${BASE_URL}/admin/users/${id}/sso`,\n                \"User SSO association deleted correctly\",\n                \"Error deleting user SSO association\"\n            );\n        } else {\n            alert(\"Wrong email, please try again\");\n        }\n    }\n}\n\nfunction remove2fa(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const confirmed = confirm(`Are you sure you want to remove 2FA for \"${email}\"?`);\n    if (confirmed) {\n        _post(`${BASE_URL}/admin/users/${id}/remove-2fa`,\n            \"2FA removed correctly\",\n            \"Error removing 2FA\"\n        );\n    }\n}\n\nfunction deauthUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const confirmed = confirm(`Are you sure you want to deauthorize sessions for \"${email}\"?`);\n    if (confirmed) {\n        _post(`${BASE_URL}/admin/users/${id}/deauth`,\n            \"Sessions deauthorized correctly\",\n            \"Error deauthorizing sessions\"\n        );\n    }\n}\n\nfunction disableUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const confirmed = confirm(`Are you sure you want to disable user \"${email}\"? This will also deauthorize their sessions.`);\n    if (confirmed) {\n        _post(`${BASE_URL}/admin/users/${id}/disable`,\n            \"User disabled successfully\",\n            \"Error disabling user\"\n        );\n    }\n}\n\nfunction enableUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const confirmed = confirm(`Are you sure you want to enable user \"${email}\"?`);\n    if (confirmed) {\n        _post(`${BASE_URL}/admin/users/${id}/enable`,\n            \"User enabled successfully\",\n            \"Error enabling user\"\n        );\n    }\n}\n\nfunction updateRevisions(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    _post(`${BASE_URL}/admin/users/update_revision`,\n        \"Success, clients will sync next time they connect\",\n        \"Error forcing clients to sync\"\n    );\n}\n\nfunction inviteUser(event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const email = document.getElementById(\"inviteEmail\");\n    const data = JSON.stringify({\n        \"email\": email.value\n    });\n    email.value = \"\";\n    _post(`${BASE_URL}/admin/invite`,\n        \"User invited correctly\",\n        \"Error inviting user\",\n        data\n    );\n}\n\nfunction resendUserInvite (event) {\n    event.preventDefault();\n    event.stopPropagation();\n    const id = event.target.parentNode.dataset.vwUserUuid;\n    const email = event.target.parentNode.dataset.vwUserEmail;\n    if (!id || !email) {\n        alert(\"Required parameters not found!\");\n        return false;\n    }\n    const confirmed = confirm(`Are you sure you want to resend invitation for \"${email}\"?`);\n    if (confirmed) {\n        _post(`${BASE_URL}/admin/users/${id}/invite/resend`,\n            \"Invite sent successfully\",\n            \"Error resend invite\"\n        );\n    }\n}\n\nconst ORG_TYPES = {\n    \"0\": {\n        \"name\": \"Owner\",\n        \"bg\": \"orange\",\n        \"font\": \"black\"\n    },\n    \"1\": {\n        \"name\": \"Admin\",\n        \"bg\": \"blueviolet\"\n    },\n    \"2\": {\n        \"name\": \"User\",\n        \"bg\": \"blue\"\n    },\n    \"4\": {\n        \"name\": \"Manager\",\n        \"bg\": \"green\"\n    },\n};\n\n// Special sort function to sort dates in ISO format\njQuery.extend(jQuery.fn.dataTableExt.oSort, {\n    \"date-iso-pre\": function(a) {\n        let x;\n        const sortDate = a.replace(/(<([^>]+)>)/gi, \"\").trim();\n        if (sortDate !== \"\") {\n            const dtParts = sortDate.split(\" \");\n            const timeParts = (undefined != dtParts[1]) ? dtParts[1].split(\":\") : [\"00\", \"00\", \"00\"];\n            const dateParts = dtParts[0].split(\"-\");\n            x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1;\n            if (isNaN(x)) {\n                x = 0;\n            }\n        } else {\n            x = Infinity;\n        }\n        return x;\n    },\n\n    \"date-iso-asc\": function(a, b) {\n        return a - b;\n    },\n\n    \"date-iso-desc\": function(a, b) {\n        return b - a;\n    }\n});\n\nconst userOrgTypeDialog = document.getElementById(\"userOrgTypeDialog\");\n// Fill the form and title\nuserOrgTypeDialog.addEventListener(\"show.bs.modal\", function(event) {\n    // Get shared values\n    const userEmail = event.relatedTarget.parentNode.dataset.vwUserEmail;\n    const userUuid = event.relatedTarget.parentNode.dataset.vwUserUuid;\n    // Get org specific values\n    const userOrgType = event.relatedTarget.dataset.vwOrgType;\n    const userOrgTypeName = ORG_TYPES[userOrgType][\"name\"];\n    const orgName = event.relatedTarget.dataset.vwOrgName;\n    const orgUuid = event.relatedTarget.dataset.vwOrgUuid;\n\n    document.getElementById(\"userOrgTypeDialogOrgName\").textContent = orgName;\n    document.getElementById(\"userOrgTypeDialogUserEmail\").textContent = userEmail;\n    document.getElementById(\"userOrgTypeUserUuid\").value = userUuid;\n    document.getElementById(\"userOrgTypeOrgUuid\").value = orgUuid;\n    document.getElementById(`userOrgType${userOrgTypeName}`).checked = true;\n}, false);\n\n// Prevent accidental submission of the form with valid elements after the modal has been hidden.\nuserOrgTypeDialog.addEventListener(\"hide.bs.modal\", function() {\n    document.getElementById(\"userOrgTypeDialogOrgName\").textContent = \"\";\n    document.getElementById(\"userOrgTypeDialogUserEmail\").textContent = \"\";\n    document.getElementById(\"userOrgTypeUserUuid\").value = \"\";\n    document.getElementById(\"userOrgTypeOrgUuid\").value = \"\";\n}, false);\n\nfunction updateUserOrgType(event) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const data = JSON.stringify(Object.fromEntries(new FormData(event.target).entries()));\n\n    _post(`${BASE_URL}/admin/users/org_type`,\n        \"Updated organization type of the user successfully\",\n        \"Error updating organization type of the user\",\n        data\n    );\n}\n\nfunction initUserTable() {\n    // Color all the org buttons per type\n    document.querySelectorAll(\"button[data-vw-org-type]\").forEach(function(e) {\n        const orgType = ORG_TYPES[e.dataset.vwOrgType];\n        e.style.backgroundColor = orgType.bg;\n        if (orgType.font !== undefined) {\n            e.style.color = orgType.font;\n        }\n        e.title = orgType.name;\n    });\n\n    document.querySelectorAll(\"button[vw-remove2fa]\").forEach(btn => {\n        btn.addEventListener(\"click\", remove2fa);\n    });\n    document.querySelectorAll(\"button[vw-deauth-user]\").forEach(btn => {\n        btn.addEventListener(\"click\", deauthUser);\n    });\n    document.querySelectorAll(\"button[vw-delete-user]\").forEach(btn => {\n        btn.addEventListener(\"click\", deleteUser);\n    });\n    document.querySelectorAll(\"button[vw-delete-sso-user]\").forEach(btn => {\n        btn.addEventListener(\"click\", deleteSSOUser);\n    });\n    document.querySelectorAll(\"button[vw-disable-user]\").forEach(btn => {\n        btn.addEventListener(\"click\", disableUser);\n    });\n    document.querySelectorAll(\"button[vw-enable-user]\").forEach(btn => {\n        btn.addEventListener(\"click\", enableUser);\n    });\n    document.querySelectorAll(\"button[vw-resend-user-invite]\").forEach(btn => {\n        btn.addEventListener(\"click\", resendUserInvite);\n    });\n\n    if (jdenticon) {\n        jdenticon();\n    }\n}\n\n// onLoad events\ndocument.addEventListener(\"DOMContentLoaded\", (/*event*/) => {\n    const size = jQuery(\"#users-table > thead th\").length;\n    const ssoOffset = size-7;\n    jQuery(\"#users-table\").DataTable({\n        \"drawCallback\": function() {\n            initUserTable();\n        },\n        \"stateSave\": true,\n        \"responsive\": true,\n        \"lengthMenu\": [\n            [-1, 2, 5, 10, 25, 50],\n            [\"All\", 2, 5, 10, 25, 50]\n        ],\n        \"pageLength\": -1, // Default show all\n        \"columnDefs\": [{\n            \"targets\": [1 + ssoOffset, 2 + ssoOffset],\n            \"type\": \"date-iso\"\n        }, {\n            \"targets\": size-1,\n            \"searchable\": false,\n            \"orderable\": false\n        }]\n    });\n\n    // Add click events for user actions\n    initUserTable();\n\n    const btnUpdateRevisions = document.getElementById(\"updateRevisions\");\n    if (btnUpdateRevisions) {\n        btnUpdateRevisions.addEventListener(\"click\", updateRevisions);\n    }\n    const btnReload = document.getElementById(\"reload\");\n    if (btnReload) {\n        btnReload.addEventListener(\"click\", reload);\n    }\n    const btnUserOrgTypeForm = document.getElementById(\"userOrgTypeForm\");\n    if (btnUserOrgTypeForm) {\n        btnUserOrgTypeForm.addEventListener(\"submit\", updateUserOrgType);\n    }\n    const btnInviteUserForm = document.getElementById(\"inviteUserForm\");\n    if (btnInviteUserForm) {\n        btnInviteUserForm.addEventListener(\"submit\", inviteUser);\n    }\n});\n"
  },
  {
    "path": "src/static/scripts/bootstrap.bundle.js",
    "content": "/*!\n  * Bootstrap v5.3.8 (https://getbootstrap.com/)\n  * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bootstrap = factory());\n})(this, (function () { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/data.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  /**\n   * Constants\n   */\n\n  const elementMap = new Map();\n  const Data = {\n    set(element, key, instance) {\n      if (!elementMap.has(element)) {\n        elementMap.set(element, new Map());\n      }\n      const instanceMap = elementMap.get(element);\n\n      // make it clear we only want one instance per element\n      // can be removed later when multiple key/instances are fine to be used\n      if (!instanceMap.has(key) && instanceMap.size !== 0) {\n        // eslint-disable-next-line no-console\n        console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n        return;\n      }\n      instanceMap.set(key, instance);\n    },\n    get(element, key) {\n      if (elementMap.has(element)) {\n        return elementMap.get(element).get(key) || null;\n      }\n      return null;\n    },\n    remove(element, key) {\n      if (!elementMap.has(element)) {\n        return;\n      }\n      const instanceMap = elementMap.get(element);\n      instanceMap.delete(key);\n\n      // free up element references if there are no instances left for an element\n      if (instanceMap.size === 0) {\n        elementMap.delete(element);\n      }\n    }\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/index.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const MAX_UID = 1000000;\n  const MILLISECONDS_MULTIPLIER = 1000;\n  const TRANSITION_END = 'transitionend';\n\n  /**\n   * Properly escape IDs selectors to handle weird IDs\n   * @param {string} selector\n   * @returns {string}\n   */\n  const parseSelector = selector => {\n    if (selector && window.CSS && window.CSS.escape) {\n      // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n      selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n    }\n    return selector;\n  };\n\n  // Shout-out Angus Croll (https://goo.gl/pxwQGp)\n  const toType = object => {\n    if (object === null || object === undefined) {\n      return `${object}`;\n    }\n    return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n  };\n\n  /**\n   * Public Util API\n   */\n\n  const getUID = prefix => {\n    do {\n      prefix += Math.floor(Math.random() * MAX_UID);\n    } while (document.getElementById(prefix));\n    return prefix;\n  };\n  const getTransitionDurationFromElement = element => {\n    if (!element) {\n      return 0;\n    }\n\n    // Get transition-duration of the element\n    let {\n      transitionDuration,\n      transitionDelay\n    } = window.getComputedStyle(element);\n    const floatTransitionDuration = Number.parseFloat(transitionDuration);\n    const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n    // Return 0 if element or transition duration is not found\n    if (!floatTransitionDuration && !floatTransitionDelay) {\n      return 0;\n    }\n\n    // If multiple durations are defined, take the first\n    transitionDuration = transitionDuration.split(',')[0];\n    transitionDelay = transitionDelay.split(',')[0];\n    return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n  };\n  const triggerTransitionEnd = element => {\n    element.dispatchEvent(new Event(TRANSITION_END));\n  };\n  const isElement$1 = object => {\n    if (!object || typeof object !== 'object') {\n      return false;\n    }\n    if (typeof object.jquery !== 'undefined') {\n      object = object[0];\n    }\n    return typeof object.nodeType !== 'undefined';\n  };\n  const getElement = object => {\n    // it's a jQuery object or a node element\n    if (isElement$1(object)) {\n      return object.jquery ? object[0] : object;\n    }\n    if (typeof object === 'string' && object.length > 0) {\n      return document.querySelector(parseSelector(object));\n    }\n    return null;\n  };\n  const isVisible = element => {\n    if (!isElement$1(element) || element.getClientRects().length === 0) {\n      return false;\n    }\n    const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n    // Handle `details` element as its content may falsie appear visible when it is closed\n    const closedDetails = element.closest('details:not([open])');\n    if (!closedDetails) {\n      return elementIsVisible;\n    }\n    if (closedDetails !== element) {\n      const summary = element.closest('summary');\n      if (summary && summary.parentNode !== closedDetails) {\n        return false;\n      }\n      if (summary === null) {\n        return false;\n      }\n    }\n    return elementIsVisible;\n  };\n  const isDisabled = element => {\n    if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n      return true;\n    }\n    if (element.classList.contains('disabled')) {\n      return true;\n    }\n    if (typeof element.disabled !== 'undefined') {\n      return element.disabled;\n    }\n    return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n  };\n  const findShadowRoot = element => {\n    if (!document.documentElement.attachShadow) {\n      return null;\n    }\n\n    // Can find the shadow root otherwise it'll return the document\n    if (typeof element.getRootNode === 'function') {\n      const root = element.getRootNode();\n      return root instanceof ShadowRoot ? root : null;\n    }\n    if (element instanceof ShadowRoot) {\n      return element;\n    }\n\n    // when we don't find a shadow root\n    if (!element.parentNode) {\n      return null;\n    }\n    return findShadowRoot(element.parentNode);\n  };\n  const noop = () => {};\n\n  /**\n   * Trick to restart an element's animation\n   *\n   * @param {HTMLElement} element\n   * @return void\n   *\n   * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n   */\n  const reflow = element => {\n    element.offsetHeight; // eslint-disable-line no-unused-expressions\n  };\n  const getjQuery = () => {\n    if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n      return window.jQuery;\n    }\n    return null;\n  };\n  const DOMContentLoadedCallbacks = [];\n  const onDOMContentLoaded = callback => {\n    if (document.readyState === 'loading') {\n      // add listener on the first call when the document is in loading state\n      if (!DOMContentLoadedCallbacks.length) {\n        document.addEventListener('DOMContentLoaded', () => {\n          for (const callback of DOMContentLoadedCallbacks) {\n            callback();\n          }\n        });\n      }\n      DOMContentLoadedCallbacks.push(callback);\n    } else {\n      callback();\n    }\n  };\n  const isRTL = () => document.documentElement.dir === 'rtl';\n  const defineJQueryPlugin = plugin => {\n    onDOMContentLoaded(() => {\n      const $ = getjQuery();\n      /* istanbul ignore if */\n      if ($) {\n        const name = plugin.NAME;\n        const JQUERY_NO_CONFLICT = $.fn[name];\n        $.fn[name] = plugin.jQueryInterface;\n        $.fn[name].Constructor = plugin;\n        $.fn[name].noConflict = () => {\n          $.fn[name] = JQUERY_NO_CONFLICT;\n          return plugin.jQueryInterface;\n        };\n      }\n    });\n  };\n  const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n    return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;\n  };\n  const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n    if (!waitForTransition) {\n      execute(callback);\n      return;\n    }\n    const durationPadding = 5;\n    const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n    let called = false;\n    const handler = ({\n      target\n    }) => {\n      if (target !== transitionElement) {\n        return;\n      }\n      called = true;\n      transitionElement.removeEventListener(TRANSITION_END, handler);\n      execute(callback);\n    };\n    transitionElement.addEventListener(TRANSITION_END, handler);\n    setTimeout(() => {\n      if (!called) {\n        triggerTransitionEnd(transitionElement);\n      }\n    }, emulatedDuration);\n  };\n\n  /**\n   * Return the previous/next element of a list.\n   *\n   * @param {array} list    The list of elements\n   * @param activeElement   The active element\n   * @param shouldGetNext   Choose to get next or previous element\n   * @param isCycleAllowed\n   * @return {Element|elem} The proper element\n   */\n  const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n    const listLength = list.length;\n    let index = list.indexOf(activeElement);\n\n    // if the element does not exist in the list return an element\n    // depending on the direction and if cycle is allowed\n    if (index === -1) {\n      return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n    }\n    index += shouldGetNext ? 1 : -1;\n    if (isCycleAllowed) {\n      index = (index + listLength) % listLength;\n    }\n    return list[Math.max(0, Math.min(index, listLength - 1))];\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/event-handler.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\n  const stripNameRegex = /\\..*/;\n  const stripUidRegex = /::\\d+$/;\n  const eventRegistry = {}; // Events storage\n  let uidEvent = 1;\n  const customEvents = {\n    mouseenter: 'mouseover',\n    mouseleave: 'mouseout'\n  };\n  const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n  /**\n   * Private methods\n   */\n\n  function makeEventUid(element, uid) {\n    return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n  }\n  function getElementEvents(element) {\n    const uid = makeEventUid(element);\n    element.uidEvent = uid;\n    eventRegistry[uid] = eventRegistry[uid] || {};\n    return eventRegistry[uid];\n  }\n  function bootstrapHandler(element, fn) {\n    return function handler(event) {\n      hydrateObj(event, {\n        delegateTarget: element\n      });\n      if (handler.oneOff) {\n        EventHandler.off(element, event.type, fn);\n      }\n      return fn.apply(element, [event]);\n    };\n  }\n  function bootstrapDelegationHandler(element, selector, fn) {\n    return function handler(event) {\n      const domElements = element.querySelectorAll(selector);\n      for (let {\n        target\n      } = event; target && target !== this; target = target.parentNode) {\n        for (const domElement of domElements) {\n          if (domElement !== target) {\n            continue;\n          }\n          hydrateObj(event, {\n            delegateTarget: target\n          });\n          if (handler.oneOff) {\n            EventHandler.off(element, event.type, selector, fn);\n          }\n          return fn.apply(target, [event]);\n        }\n      }\n    };\n  }\n  function findHandler(events, callable, delegationSelector = null) {\n    return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n  }\n  function normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n    const isDelegated = typeof handler === 'string';\n    // TODO: tooltip passes `false` instead of selector, so we need to check\n    const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n    let typeEvent = getTypeEvent(originalTypeEvent);\n    if (!nativeEvents.has(typeEvent)) {\n      typeEvent = originalTypeEvent;\n    }\n    return [isDelegated, callable, typeEvent];\n  }\n  function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n    if (typeof originalTypeEvent !== 'string' || !element) {\n      return;\n    }\n    let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n    // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n    // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n    if (originalTypeEvent in customEvents) {\n      const wrapFunction = fn => {\n        return function (event) {\n          if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n            return fn.call(this, event);\n          }\n        };\n      };\n      callable = wrapFunction(callable);\n    }\n    const events = getElementEvents(element);\n    const handlers = events[typeEvent] || (events[typeEvent] = {});\n    const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n    if (previousFunction) {\n      previousFunction.oneOff = previousFunction.oneOff && oneOff;\n      return;\n    }\n    const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n    const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n    fn.delegationSelector = isDelegated ? handler : null;\n    fn.callable = callable;\n    fn.oneOff = oneOff;\n    fn.uidEvent = uid;\n    handlers[uid] = fn;\n    element.addEventListener(typeEvent, fn, isDelegated);\n  }\n  function removeHandler(element, events, typeEvent, handler, delegationSelector) {\n    const fn = findHandler(events[typeEvent], handler, delegationSelector);\n    if (!fn) {\n      return;\n    }\n    element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n    delete events[typeEvent][fn.uidEvent];\n  }\n  function removeNamespacedHandlers(element, events, typeEvent, namespace) {\n    const storeElementEvent = events[typeEvent] || {};\n    for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n      if (handlerKey.includes(namespace)) {\n        removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n      }\n    }\n  }\n  function getTypeEvent(event) {\n    // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n    event = event.replace(stripNameRegex, '');\n    return customEvents[event] || event;\n  }\n  const EventHandler = {\n    on(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, false);\n    },\n    one(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, true);\n    },\n    off(element, originalTypeEvent, handler, delegationFunction) {\n      if (typeof originalTypeEvent !== 'string' || !element) {\n        return;\n      }\n      const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n      const inNamespace = typeEvent !== originalTypeEvent;\n      const events = getElementEvents(element);\n      const storeElementEvent = events[typeEvent] || {};\n      const isNamespace = originalTypeEvent.startsWith('.');\n      if (typeof callable !== 'undefined') {\n        // Simplest case: handler is passed, remove that listener ONLY.\n        if (!Object.keys(storeElementEvent).length) {\n          return;\n        }\n        removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n        return;\n      }\n      if (isNamespace) {\n        for (const elementEvent of Object.keys(events)) {\n          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n        }\n      }\n      for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n        const handlerKey = keyHandlers.replace(stripUidRegex, '');\n        if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n          removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n        }\n      }\n    },\n    trigger(element, event, args) {\n      if (typeof event !== 'string' || !element) {\n        return null;\n      }\n      const $ = getjQuery();\n      const typeEvent = getTypeEvent(event);\n      const inNamespace = event !== typeEvent;\n      let jQueryEvent = null;\n      let bubbles = true;\n      let nativeDispatch = true;\n      let defaultPrevented = false;\n      if (inNamespace && $) {\n        jQueryEvent = $.Event(event, args);\n        $(element).trigger(jQueryEvent);\n        bubbles = !jQueryEvent.isPropagationStopped();\n        nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n        defaultPrevented = jQueryEvent.isDefaultPrevented();\n      }\n      const evt = hydrateObj(new Event(event, {\n        bubbles,\n        cancelable: true\n      }), args);\n      if (defaultPrevented) {\n        evt.preventDefault();\n      }\n      if (nativeDispatch) {\n        element.dispatchEvent(evt);\n      }\n      if (evt.defaultPrevented && jQueryEvent) {\n        jQueryEvent.preventDefault();\n      }\n      return evt;\n    }\n  };\n  function hydrateObj(obj, meta = {}) {\n    for (const [key, value] of Object.entries(meta)) {\n      try {\n        obj[key] = value;\n      } catch (_unused) {\n        Object.defineProperty(obj, key, {\n          configurable: true,\n          get() {\n            return value;\n          }\n        });\n      }\n    }\n    return obj;\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/manipulator.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  function normalizeData(value) {\n    if (value === 'true') {\n      return true;\n    }\n    if (value === 'false') {\n      return false;\n    }\n    if (value === Number(value).toString()) {\n      return Number(value);\n    }\n    if (value === '' || value === 'null') {\n      return null;\n    }\n    if (typeof value !== 'string') {\n      return value;\n    }\n    try {\n      return JSON.parse(decodeURIComponent(value));\n    } catch (_unused) {\n      return value;\n    }\n  }\n  function normalizeDataKey(key) {\n    return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n  }\n  const Manipulator = {\n    setDataAttribute(element, key, value) {\n      element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n    },\n    removeDataAttribute(element, key) {\n      element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n    },\n    getDataAttributes(element) {\n      if (!element) {\n        return {};\n      }\n      const attributes = {};\n      const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n      for (const key of bsKeys) {\n        let pureKey = key.replace(/^bs/, '');\n        pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);\n        attributes[pureKey] = normalizeData(element.dataset[key]);\n      }\n      return attributes;\n    },\n    getDataAttribute(element, key) {\n      return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n    }\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/config.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Class definition\n   */\n\n  class Config {\n    // Getters\n    static get Default() {\n      return {};\n    }\n    static get DefaultType() {\n      return {};\n    }\n    static get NAME() {\n      throw new Error('You have to implement the static method \"NAME\", for each component!');\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      return config;\n    }\n    _mergeConfigObj(config, element) {\n      const jsonConfig = isElement$1(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n      return {\n        ...this.constructor.Default,\n        ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n        ...(isElement$1(element) ? Manipulator.getDataAttributes(element) : {}),\n        ...(typeof config === 'object' ? config : {})\n      };\n    }\n    _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n      for (const [property, expectedTypes] of Object.entries(configTypes)) {\n        const value = config[property];\n        const valueType = isElement$1(value) ? 'element' : toType(value);\n        if (!new RegExp(expectedTypes).test(valueType)) {\n          throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n        }\n      }\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap base-component.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const VERSION = '5.3.8';\n\n  /**\n   * Class definition\n   */\n\n  class BaseComponent extends Config {\n    constructor(element, config) {\n      super();\n      element = getElement(element);\n      if (!element) {\n        return;\n      }\n      this._element = element;\n      this._config = this._getConfig(config);\n      Data.set(this._element, this.constructor.DATA_KEY, this);\n    }\n\n    // Public\n    dispose() {\n      Data.remove(this._element, this.constructor.DATA_KEY);\n      EventHandler.off(this._element, this.constructor.EVENT_KEY);\n      for (const propertyName of Object.getOwnPropertyNames(this)) {\n        this[propertyName] = null;\n      }\n    }\n\n    // Private\n    _queueCallback(callback, element, isAnimated = true) {\n      executeAfterTransition(callback, element, isAnimated);\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config, this._element);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n\n    // Static\n    static getInstance(element) {\n      return Data.get(getElement(element), this.DATA_KEY);\n    }\n    static getOrCreateInstance(element, config = {}) {\n      return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n    }\n    static get VERSION() {\n      return VERSION;\n    }\n    static get DATA_KEY() {\n      return `bs.${this.NAME}`;\n    }\n    static get EVENT_KEY() {\n      return `.${this.DATA_KEY}`;\n    }\n    static eventName(name) {\n      return `${name}${this.EVENT_KEY}`;\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/selector-engine.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const getSelector = element => {\n    let selector = element.getAttribute('data-bs-target');\n    if (!selector || selector === '#') {\n      let hrefAttribute = element.getAttribute('href');\n\n      // The only valid content that could double as a selector are IDs or classes,\n      // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n      // `document.querySelector` will rightfully complain it is invalid.\n      // See https://github.com/twbs/bootstrap/issues/32273\n      if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n        return null;\n      }\n\n      // Just in case some CMS puts out a full URL with the anchor appended\n      if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n        hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n      }\n      selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n    }\n    return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n  };\n  const SelectorEngine = {\n    find(selector, element = document.documentElement) {\n      return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n    },\n    findOne(selector, element = document.documentElement) {\n      return Element.prototype.querySelector.call(element, selector);\n    },\n    children(element, selector) {\n      return [].concat(...element.children).filter(child => child.matches(selector));\n    },\n    parents(element, selector) {\n      const parents = [];\n      let ancestor = element.parentNode.closest(selector);\n      while (ancestor) {\n        parents.push(ancestor);\n        ancestor = ancestor.parentNode.closest(selector);\n      }\n      return parents;\n    },\n    prev(element, selector) {\n      let previous = element.previousElementSibling;\n      while (previous) {\n        if (previous.matches(selector)) {\n          return [previous];\n        }\n        previous = previous.previousElementSibling;\n      }\n      return [];\n    },\n    // TODO: this is now unused; remove later along with prev()\n    next(element, selector) {\n      let next = element.nextElementSibling;\n      while (next) {\n        if (next.matches(selector)) {\n          return [next];\n        }\n        next = next.nextElementSibling;\n      }\n      return [];\n    },\n    focusableChildren(element) {\n      const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n      return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n    },\n    getSelectorFromElement(element) {\n      const selector = getSelector(element);\n      if (selector) {\n        return SelectorEngine.findOne(selector) ? selector : null;\n      }\n      return null;\n    },\n    getElementFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.findOne(selector) : null;\n    },\n    getMultipleElementsFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.find(selector) : [];\n    }\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/component-functions.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const enableDismissTrigger = (component, method = 'hide') => {\n    const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n    const name = component.NAME;\n    EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n      if (['A', 'AREA'].includes(this.tagName)) {\n        event.preventDefault();\n      }\n      if (isDisabled(this)) {\n        return;\n      }\n      const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n      const instance = component.getOrCreateInstance(target);\n\n      // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n      instance[method]();\n    });\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap alert.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$f = 'alert';\n  const DATA_KEY$a = 'bs.alert';\n  const EVENT_KEY$b = `.${DATA_KEY$a}`;\n  const EVENT_CLOSE = `close${EVENT_KEY$b}`;\n  const EVENT_CLOSED = `closed${EVENT_KEY$b}`;\n  const CLASS_NAME_FADE$5 = 'fade';\n  const CLASS_NAME_SHOW$8 = 'show';\n\n  /**\n   * Class definition\n   */\n\n  class Alert extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME$f;\n    }\n\n    // Public\n    close() {\n      const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n      if (closeEvent.defaultPrevented) {\n        return;\n      }\n      this._element.classList.remove(CLASS_NAME_SHOW$8);\n      const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n      this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n    }\n\n    // Private\n    _destroyElement() {\n      this._element.remove();\n      EventHandler.trigger(this._element, EVENT_CLOSED);\n      this.dispose();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Alert.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  enableDismissTrigger(Alert, 'close');\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Alert);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap button.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$e = 'button';\n  const DATA_KEY$9 = 'bs.button';\n  const EVENT_KEY$a = `.${DATA_KEY$9}`;\n  const DATA_API_KEY$6 = '.data-api';\n  const CLASS_NAME_ACTIVE$3 = 'active';\n  const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\n  const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n  /**\n   * Class definition\n   */\n\n  class Button extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME$e;\n    }\n\n    // Public\n    toggle() {\n      // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n      this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Button.getOrCreateInstance(this);\n        if (config === 'toggle') {\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n    event.preventDefault();\n    const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n    const data = Button.getOrCreateInstance(button);\n    data.toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Button);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/swipe.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$d = 'swipe';\n  const EVENT_KEY$9 = '.bs.swipe';\n  const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\n  const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\n  const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\n  const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\n  const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\n  const POINTER_TYPE_TOUCH = 'touch';\n  const POINTER_TYPE_PEN = 'pen';\n  const CLASS_NAME_POINTER_EVENT = 'pointer-event';\n  const SWIPE_THRESHOLD = 40;\n  const Default$c = {\n    endCallback: null,\n    leftCallback: null,\n    rightCallback: null\n  };\n  const DefaultType$c = {\n    endCallback: '(function|null)',\n    leftCallback: '(function|null)',\n    rightCallback: '(function|null)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Swipe extends Config {\n    constructor(element, config) {\n      super();\n      this._element = element;\n      if (!element || !Swipe.isSupported()) {\n        return;\n      }\n      this._config = this._getConfig(config);\n      this._deltaX = 0;\n      this._supportPointerEvents = Boolean(window.PointerEvent);\n      this._initEvents();\n    }\n\n    // Getters\n    static get Default() {\n      return Default$c;\n    }\n    static get DefaultType() {\n      return DefaultType$c;\n    }\n    static get NAME() {\n      return NAME$d;\n    }\n\n    // Public\n    dispose() {\n      EventHandler.off(this._element, EVENT_KEY$9);\n    }\n\n    // Private\n    _start(event) {\n      if (!this._supportPointerEvents) {\n        this._deltaX = event.touches[0].clientX;\n        return;\n      }\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX;\n      }\n    }\n    _end(event) {\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX - this._deltaX;\n      }\n      this._handleSwipe();\n      execute(this._config.endCallback);\n    }\n    _move(event) {\n      this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n    }\n    _handleSwipe() {\n      const absDeltaX = Math.abs(this._deltaX);\n      if (absDeltaX <= SWIPE_THRESHOLD) {\n        return;\n      }\n      const direction = absDeltaX / this._deltaX;\n      this._deltaX = 0;\n      if (!direction) {\n        return;\n      }\n      execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n    }\n    _initEvents() {\n      if (this._supportPointerEvents) {\n        EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n        EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n        this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n      } else {\n        EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n        EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n        EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n      }\n    }\n    _eventIsPointerPenTouch(event) {\n      return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n    }\n\n    // Static\n    static isSupported() {\n      return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap carousel.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$c = 'carousel';\n  const DATA_KEY$8 = 'bs.carousel';\n  const EVENT_KEY$8 = `.${DATA_KEY$8}`;\n  const DATA_API_KEY$5 = '.data-api';\n  const ARROW_LEFT_KEY$1 = 'ArrowLeft';\n  const ARROW_RIGHT_KEY$1 = 'ArrowRight';\n  const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\n  const ORDER_NEXT = 'next';\n  const ORDER_PREV = 'prev';\n  const DIRECTION_LEFT = 'left';\n  const DIRECTION_RIGHT = 'right';\n  const EVENT_SLIDE = `slide${EVENT_KEY$8}`;\n  const EVENT_SLID = `slid${EVENT_KEY$8}`;\n  const EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\n  const EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\n  const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\n  const EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\n  const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\n  const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\n  const CLASS_NAME_CAROUSEL = 'carousel';\n  const CLASS_NAME_ACTIVE$2 = 'active';\n  const CLASS_NAME_SLIDE = 'slide';\n  const CLASS_NAME_END = 'carousel-item-end';\n  const CLASS_NAME_START = 'carousel-item-start';\n  const CLASS_NAME_NEXT = 'carousel-item-next';\n  const CLASS_NAME_PREV = 'carousel-item-prev';\n  const SELECTOR_ACTIVE = '.active';\n  const SELECTOR_ITEM = '.carousel-item';\n  const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\n  const SELECTOR_ITEM_IMG = '.carousel-item img';\n  const SELECTOR_INDICATORS = '.carousel-indicators';\n  const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\n  const SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\n  const KEY_TO_DIRECTION = {\n    [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n    [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n  };\n  const Default$b = {\n    interval: 5000,\n    keyboard: true,\n    pause: 'hover',\n    ride: false,\n    touch: true,\n    wrap: true\n  };\n  const DefaultType$b = {\n    interval: '(number|boolean)',\n    // TODO:v6 remove boolean support\n    keyboard: 'boolean',\n    pause: '(string|boolean)',\n    ride: '(boolean|string)',\n    touch: 'boolean',\n    wrap: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Carousel extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._interval = null;\n      this._activeElement = null;\n      this._isSliding = false;\n      this.touchTimeout = null;\n      this._swipeHelper = null;\n      this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n      this._addEventListeners();\n      if (this._config.ride === CLASS_NAME_CAROUSEL) {\n        this.cycle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default$b;\n    }\n    static get DefaultType() {\n      return DefaultType$b;\n    }\n    static get NAME() {\n      return NAME$c;\n    }\n\n    // Public\n    next() {\n      this._slide(ORDER_NEXT);\n    }\n    nextWhenVisible() {\n      // FIXME TODO use `document.visibilityState`\n      // Don't call next when the page isn't visible\n      // or the carousel or its parent isn't visible\n      if (!document.hidden && isVisible(this._element)) {\n        this.next();\n      }\n    }\n    prev() {\n      this._slide(ORDER_PREV);\n    }\n    pause() {\n      if (this._isSliding) {\n        triggerTransitionEnd(this._element);\n      }\n      this._clearInterval();\n    }\n    cycle() {\n      this._clearInterval();\n      this._updateInterval();\n      this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n    }\n    _maybeEnableCycle() {\n      if (!this._config.ride) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n        return;\n      }\n      this.cycle();\n    }\n    to(index) {\n      const items = this._getItems();\n      if (index > items.length - 1 || index < 0) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n        return;\n      }\n      const activeIndex = this._getItemIndex(this._getActive());\n      if (activeIndex === index) {\n        return;\n      }\n      const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n      this._slide(order, items[index]);\n    }\n    dispose() {\n      if (this._swipeHelper) {\n        this._swipeHelper.dispose();\n      }\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      config.defaultInterval = config.interval;\n      return config;\n    }\n    _addEventListeners() {\n      if (this._config.keyboard) {\n        EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n      }\n      if (this._config.pause === 'hover') {\n        EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n        EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n      }\n      if (this._config.touch && Swipe.isSupported()) {\n        this._addTouchEventListeners();\n      }\n    }\n    _addTouchEventListeners() {\n      for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n        EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n      }\n      const endCallBack = () => {\n        if (this._config.pause !== 'hover') {\n          return;\n        }\n\n        // If it's a touch-enabled device, mouseenter/leave are fired as\n        // part of the mouse compatibility events on first tap - the carousel\n        // would stop cycling until user tapped out of it;\n        // here, we listen for touchend, explicitly pause the carousel\n        // (as if it's the second time we tap on it, mouseenter compat event\n        // is NOT fired) and after a timeout (to allow for mouse compatibility\n        // events to fire) we explicitly restart cycling\n\n        this.pause();\n        if (this.touchTimeout) {\n          clearTimeout(this.touchTimeout);\n        }\n        this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n      };\n      const swipeConfig = {\n        leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n        rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n        endCallback: endCallBack\n      };\n      this._swipeHelper = new Swipe(this._element, swipeConfig);\n    }\n    _keydown(event) {\n      if (/input|textarea/i.test(event.target.tagName)) {\n        return;\n      }\n      const direction = KEY_TO_DIRECTION[event.key];\n      if (direction) {\n        event.preventDefault();\n        this._slide(this._directionToOrder(direction));\n      }\n    }\n    _getItemIndex(element) {\n      return this._getItems().indexOf(element);\n    }\n    _setActiveIndicatorElement(index) {\n      if (!this._indicatorsElement) {\n        return;\n      }\n      const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n      activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n      activeIndicator.removeAttribute('aria-current');\n      const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n      if (newActiveIndicator) {\n        newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n        newActiveIndicator.setAttribute('aria-current', 'true');\n      }\n    }\n    _updateInterval() {\n      const element = this._activeElement || this._getActive();\n      if (!element) {\n        return;\n      }\n      const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n      this._config.interval = elementInterval || this._config.defaultInterval;\n    }\n    _slide(order, element = null) {\n      if (this._isSliding) {\n        return;\n      }\n      const activeElement = this._getActive();\n      const isNext = order === ORDER_NEXT;\n      const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n      if (nextElement === activeElement) {\n        return;\n      }\n      const nextElementIndex = this._getItemIndex(nextElement);\n      const triggerEvent = eventName => {\n        return EventHandler.trigger(this._element, eventName, {\n          relatedTarget: nextElement,\n          direction: this._orderToDirection(order),\n          from: this._getItemIndex(activeElement),\n          to: nextElementIndex\n        });\n      };\n      const slideEvent = triggerEvent(EVENT_SLIDE);\n      if (slideEvent.defaultPrevented) {\n        return;\n      }\n      if (!activeElement || !nextElement) {\n        // Some weirdness is happening, so we bail\n        // TODO: change tests that use empty divs to avoid this check\n        return;\n      }\n      const isCycling = Boolean(this._interval);\n      this.pause();\n      this._isSliding = true;\n      this._setActiveIndicatorElement(nextElementIndex);\n      this._activeElement = nextElement;\n      const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n      const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n      nextElement.classList.add(orderClassName);\n      reflow(nextElement);\n      activeElement.classList.add(directionalClassName);\n      nextElement.classList.add(directionalClassName);\n      const completeCallBack = () => {\n        nextElement.classList.remove(directionalClassName, orderClassName);\n        nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n        activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n        this._isSliding = false;\n        triggerEvent(EVENT_SLID);\n      };\n      this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n      if (isCycling) {\n        this.cycle();\n      }\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_SLIDE);\n    }\n    _getActive() {\n      return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n    }\n    _getItems() {\n      return SelectorEngine.find(SELECTOR_ITEM, this._element);\n    }\n    _clearInterval() {\n      if (this._interval) {\n        clearInterval(this._interval);\n        this._interval = null;\n      }\n    }\n    _directionToOrder(direction) {\n      if (isRTL()) {\n        return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n      }\n      return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n    }\n    _orderToDirection(order) {\n      if (isRTL()) {\n        return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n      }\n      return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Carousel.getOrCreateInstance(this, config);\n        if (typeof config === 'number') {\n          data.to(config);\n          return;\n        }\n        if (typeof config === 'string') {\n          if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n      return;\n    }\n    event.preventDefault();\n    const carousel = Carousel.getOrCreateInstance(target);\n    const slideIndex = this.getAttribute('data-bs-slide-to');\n    if (slideIndex) {\n      carousel.to(slideIndex);\n      carousel._maybeEnableCycle();\n      return;\n    }\n    if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n      carousel.next();\n      carousel._maybeEnableCycle();\n      return;\n    }\n    carousel.prev();\n    carousel._maybeEnableCycle();\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n    const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n    for (const carousel of carousels) {\n      Carousel.getOrCreateInstance(carousel);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Carousel);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap collapse.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$b = 'collapse';\n  const DATA_KEY$7 = 'bs.collapse';\n  const EVENT_KEY$7 = `.${DATA_KEY$7}`;\n  const DATA_API_KEY$4 = '.data-api';\n  const EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\n  const EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\n  const EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\n  const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\n  const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\n  const CLASS_NAME_SHOW$7 = 'show';\n  const CLASS_NAME_COLLAPSE = 'collapse';\n  const CLASS_NAME_COLLAPSING = 'collapsing';\n  const CLASS_NAME_COLLAPSED = 'collapsed';\n  const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\n  const CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\n  const WIDTH = 'width';\n  const HEIGHT = 'height';\n  const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\n  const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\n  const Default$a = {\n    parent: null,\n    toggle: true\n  };\n  const DefaultType$a = {\n    parent: '(null|element)',\n    toggle: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Collapse extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isTransitioning = false;\n      this._triggerArray = [];\n      const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n      for (const elem of toggleList) {\n        const selector = SelectorEngine.getSelectorFromElement(elem);\n        const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n        if (selector !== null && filterElement.length) {\n          this._triggerArray.push(elem);\n        }\n      }\n      this._initializeChildren();\n      if (!this._config.parent) {\n        this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n      }\n      if (this._config.toggle) {\n        this.toggle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default$a;\n    }\n    static get DefaultType() {\n      return DefaultType$a;\n    }\n    static get NAME() {\n      return NAME$b;\n    }\n\n    // Public\n    toggle() {\n      if (this._isShown()) {\n        this.hide();\n      } else {\n        this.show();\n      }\n    }\n    show() {\n      if (this._isTransitioning || this._isShown()) {\n        return;\n      }\n      let activeChildren = [];\n\n      // find active children\n      if (this._config.parent) {\n        activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n          toggle: false\n        }));\n      }\n      if (activeChildren.length && activeChildren[0]._isTransitioning) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      for (const activeInstance of activeChildren) {\n        activeInstance.hide();\n      }\n      const dimension = this._getDimension();\n      this._element.classList.remove(CLASS_NAME_COLLAPSE);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.style[dimension] = 0;\n      this._addAriaAndCollapsedClass(this._triggerArray, true);\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n        this._element.style[dimension] = '';\n        EventHandler.trigger(this._element, EVENT_SHOWN$6);\n      };\n      const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n      const scrollSize = `scroll${capitalizedDimension}`;\n      this._queueCallback(complete, this._element, true);\n      this._element.style[dimension] = `${this._element[scrollSize]}px`;\n    }\n    hide() {\n      if (this._isTransitioning || !this._isShown()) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      const dimension = this._getDimension();\n      this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n      reflow(this._element);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n      for (const trigger of this._triggerArray) {\n        const element = SelectorEngine.getElementFromSelector(trigger);\n        if (element && !this._isShown(element)) {\n          this._addAriaAndCollapsedClass([trigger], false);\n        }\n      }\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE);\n        EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n      };\n      this._element.style[dimension] = '';\n      this._queueCallback(complete, this._element, true);\n    }\n\n    // Private\n    _isShown(element = this._element) {\n      return element.classList.contains(CLASS_NAME_SHOW$7);\n    }\n    _configAfterMerge(config) {\n      config.toggle = Boolean(config.toggle); // Coerce string values\n      config.parent = getElement(config.parent);\n      return config;\n    }\n    _getDimension() {\n      return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n    }\n    _initializeChildren() {\n      if (!this._config.parent) {\n        return;\n      }\n      const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n      for (const element of children) {\n        const selected = SelectorEngine.getElementFromSelector(element);\n        if (selected) {\n          this._addAriaAndCollapsedClass([element], this._isShown(selected));\n        }\n      }\n    }\n    _getFirstLevelChildren(selector) {\n      const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n      // remove children if greater depth\n      return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n    }\n    _addAriaAndCollapsedClass(triggerArray, isOpen) {\n      if (!triggerArray.length) {\n        return;\n      }\n      for (const element of triggerArray) {\n        element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n        element.setAttribute('aria-expanded', isOpen);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      const _config = {};\n      if (typeof config === 'string' && /show|hide/.test(config)) {\n        _config.toggle = false;\n      }\n      return this.each(function () {\n        const data = Collapse.getOrCreateInstance(this, _config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element\n    if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n      event.preventDefault();\n    }\n    for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n      Collapse.getOrCreateInstance(element, {\n        toggle: false\n      }).toggle();\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Collapse);\n\n  var top = 'top';\n  var bottom = 'bottom';\n  var right = 'right';\n  var left = 'left';\n  var auto = 'auto';\n  var basePlacements = [top, bottom, right, left];\n  var start = 'start';\n  var end = 'end';\n  var clippingParents = 'clippingParents';\n  var viewport = 'viewport';\n  var popper = 'popper';\n  var reference = 'reference';\n  var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n    return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n  }, []);\n  var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n    return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n  }, []); // modifiers that need to read the DOM\n\n  var beforeRead = 'beforeRead';\n  var read = 'read';\n  var afterRead = 'afterRead'; // pure-logic modifiers\n\n  var beforeMain = 'beforeMain';\n  var main = 'main';\n  var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\n  var beforeWrite = 'beforeWrite';\n  var write = 'write';\n  var afterWrite = 'afterWrite';\n  var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];\n\n  function getNodeName(element) {\n    return element ? (element.nodeName || '').toLowerCase() : null;\n  }\n\n  function getWindow(node) {\n    if (node == null) {\n      return window;\n    }\n\n    if (node.toString() !== '[object Window]') {\n      var ownerDocument = node.ownerDocument;\n      return ownerDocument ? ownerDocument.defaultView || window : window;\n    }\n\n    return node;\n  }\n\n  function isElement(node) {\n    var OwnElement = getWindow(node).Element;\n    return node instanceof OwnElement || node instanceof Element;\n  }\n\n  function isHTMLElement(node) {\n    var OwnElement = getWindow(node).HTMLElement;\n    return node instanceof OwnElement || node instanceof HTMLElement;\n  }\n\n  function isShadowRoot(node) {\n    // IE 11 has no ShadowRoot\n    if (typeof ShadowRoot === 'undefined') {\n      return false;\n    }\n\n    var OwnElement = getWindow(node).ShadowRoot;\n    return node instanceof OwnElement || node instanceof ShadowRoot;\n  }\n\n  // and applies them to the HTMLElements such as popper and arrow\n\n  function applyStyles(_ref) {\n    var state = _ref.state;\n    Object.keys(state.elements).forEach(function (name) {\n      var style = state.styles[name] || {};\n      var attributes = state.attributes[name] || {};\n      var element = state.elements[name]; // arrow is optional + virtual elements\n\n      if (!isHTMLElement(element) || !getNodeName(element)) {\n        return;\n      } // Flow doesn't support to extend this property, but it's the most\n      // effective way to apply styles to an HTMLElement\n      // $FlowFixMe[cannot-write]\n\n\n      Object.assign(element.style, style);\n      Object.keys(attributes).forEach(function (name) {\n        var value = attributes[name];\n\n        if (value === false) {\n          element.removeAttribute(name);\n        } else {\n          element.setAttribute(name, value === true ? '' : value);\n        }\n      });\n    });\n  }\n\n  function effect$2(_ref2) {\n    var state = _ref2.state;\n    var initialStyles = {\n      popper: {\n        position: state.options.strategy,\n        left: '0',\n        top: '0',\n        margin: '0'\n      },\n      arrow: {\n        position: 'absolute'\n      },\n      reference: {}\n    };\n    Object.assign(state.elements.popper.style, initialStyles.popper);\n    state.styles = initialStyles;\n\n    if (state.elements.arrow) {\n      Object.assign(state.elements.arrow.style, initialStyles.arrow);\n    }\n\n    return function () {\n      Object.keys(state.elements).forEach(function (name) {\n        var element = state.elements[name];\n        var attributes = state.attributes[name] || {};\n        var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n        var style = styleProperties.reduce(function (style, property) {\n          style[property] = '';\n          return style;\n        }, {}); // arrow is optional + virtual elements\n\n        if (!isHTMLElement(element) || !getNodeName(element)) {\n          return;\n        }\n\n        Object.assign(element.style, style);\n        Object.keys(attributes).forEach(function (attribute) {\n          element.removeAttribute(attribute);\n        });\n      });\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const applyStyles$1 = {\n    name: 'applyStyles',\n    enabled: true,\n    phase: 'write',\n    fn: applyStyles,\n    effect: effect$2,\n    requires: ['computeStyles']\n  };\n\n  function getBasePlacement(placement) {\n    return placement.split('-')[0];\n  }\n\n  var max = Math.max;\n  var min = Math.min;\n  var round = Math.round;\n\n  function getUAString() {\n    var uaData = navigator.userAgentData;\n\n    if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n      return uaData.brands.map(function (item) {\n        return item.brand + \"/\" + item.version;\n      }).join(' ');\n    }\n\n    return navigator.userAgent;\n  }\n\n  function isLayoutViewport() {\n    return !/^((?!chrome|android).)*safari/i.test(getUAString());\n  }\n\n  function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n    if (includeScale === void 0) {\n      includeScale = false;\n    }\n\n    if (isFixedStrategy === void 0) {\n      isFixedStrategy = false;\n    }\n\n    var clientRect = element.getBoundingClientRect();\n    var scaleX = 1;\n    var scaleY = 1;\n\n    if (includeScale && isHTMLElement(element)) {\n      scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n      scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n    }\n\n    var _ref = isElement(element) ? getWindow(element) : window,\n        visualViewport = _ref.visualViewport;\n\n    var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n    var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n    var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n    var width = clientRect.width / scaleX;\n    var height = clientRect.height / scaleY;\n    return {\n      width: width,\n      height: height,\n      top: y,\n      right: x + width,\n      bottom: y + height,\n      left: x,\n      x: x,\n      y: y\n    };\n  }\n\n  // means it doesn't take into account transforms.\n\n  function getLayoutRect(element) {\n    var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n    // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n    var width = element.offsetWidth;\n    var height = element.offsetHeight;\n\n    if (Math.abs(clientRect.width - width) <= 1) {\n      width = clientRect.width;\n    }\n\n    if (Math.abs(clientRect.height - height) <= 1) {\n      height = clientRect.height;\n    }\n\n    return {\n      x: element.offsetLeft,\n      y: element.offsetTop,\n      width: width,\n      height: height\n    };\n  }\n\n  function contains(parent, child) {\n    var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n    if (parent.contains(child)) {\n      return true;\n    } // then fallback to custom implementation with Shadow DOM support\n    else if (rootNode && isShadowRoot(rootNode)) {\n        var next = child;\n\n        do {\n          if (next && parent.isSameNode(next)) {\n            return true;\n          } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n          next = next.parentNode || next.host;\n        } while (next);\n      } // Give up, the result is false\n\n\n    return false;\n  }\n\n  function getComputedStyle$1(element) {\n    return getWindow(element).getComputedStyle(element);\n  }\n\n  function isTableElement(element) {\n    return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n  }\n\n  function getDocumentElement(element) {\n    // $FlowFixMe[incompatible-return]: assume body is always available\n    return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n    element.document) || window.document).documentElement;\n  }\n\n  function getParentNode(element) {\n    if (getNodeName(element) === 'html') {\n      return element;\n    }\n\n    return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n      // $FlowFixMe[incompatible-return]\n      // $FlowFixMe[prop-missing]\n      element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n      element.parentNode || ( // DOM Element detected\n      isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n      // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n      getDocumentElement(element) // fallback\n\n    );\n  }\n\n  function getTrueOffsetParent(element) {\n    if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n    getComputedStyle$1(element).position === 'fixed') {\n      return null;\n    }\n\n    return element.offsetParent;\n  } // `.offsetParent` reports `null` for fixed elements, while absolute elements\n  // return the containing block\n\n\n  function getContainingBlock(element) {\n    var isFirefox = /firefox/i.test(getUAString());\n    var isIE = /Trident/i.test(getUAString());\n\n    if (isIE && isHTMLElement(element)) {\n      // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n      var elementCss = getComputedStyle$1(element);\n\n      if (elementCss.position === 'fixed') {\n        return null;\n      }\n    }\n\n    var currentNode = getParentNode(element);\n\n    if (isShadowRoot(currentNode)) {\n      currentNode = currentNode.host;\n    }\n\n    while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n      var css = getComputedStyle$1(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n      // create a containing block.\n      // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n      if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n        return currentNode;\n      } else {\n        currentNode = currentNode.parentNode;\n      }\n    }\n\n    return null;\n  } // Gets the closest ancestor positioned element. Handles some edge cases,\n  // such as table ancestors and cross browser bugs.\n\n\n  function getOffsetParent(element) {\n    var window = getWindow(element);\n    var offsetParent = getTrueOffsetParent(element);\n\n    while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') {\n      offsetParent = getTrueOffsetParent(offsetParent);\n    }\n\n    if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static')) {\n      return window;\n    }\n\n    return offsetParent || getContainingBlock(element) || window;\n  }\n\n  function getMainAxisFromPlacement(placement) {\n    return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n  }\n\n  function within(min$1, value, max$1) {\n    return max(min$1, min(value, max$1));\n  }\n  function withinMaxClamp(min, value, max) {\n    var v = within(min, value, max);\n    return v > max ? max : v;\n  }\n\n  function getFreshSideObject() {\n    return {\n      top: 0,\n      right: 0,\n      bottom: 0,\n      left: 0\n    };\n  }\n\n  function mergePaddingObject(paddingObject) {\n    return Object.assign({}, getFreshSideObject(), paddingObject);\n  }\n\n  function expandToHashMap(value, keys) {\n    return keys.reduce(function (hashMap, key) {\n      hashMap[key] = value;\n      return hashMap;\n    }, {});\n  }\n\n  var toPaddingObject = function toPaddingObject(padding, state) {\n    padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : padding;\n    return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n  };\n\n  function arrow(_ref) {\n    var _state$modifiersData$;\n\n    var state = _ref.state,\n        name = _ref.name,\n        options = _ref.options;\n    var arrowElement = state.elements.arrow;\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var basePlacement = getBasePlacement(state.placement);\n    var axis = getMainAxisFromPlacement(basePlacement);\n    var isVertical = [left, right].indexOf(basePlacement) >= 0;\n    var len = isVertical ? 'height' : 'width';\n\n    if (!arrowElement || !popperOffsets) {\n      return;\n    }\n\n    var paddingObject = toPaddingObject(options.padding, state);\n    var arrowRect = getLayoutRect(arrowElement);\n    var minProp = axis === 'y' ? top : left;\n    var maxProp = axis === 'y' ? bottom : right;\n    var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n    var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n    var arrowOffsetParent = getOffsetParent(arrowElement);\n    var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n    var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n    // outside of the popper bounds\n\n    var min = paddingObject[minProp];\n    var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n    var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n    var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n    var axisProp = axis;\n    state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n  }\n\n  function effect$1(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options;\n    var _options$element = options.element,\n        arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n    if (arrowElement == null) {\n      return;\n    } // CSS selector\n\n\n    if (typeof arrowElement === 'string') {\n      arrowElement = state.elements.popper.querySelector(arrowElement);\n\n      if (!arrowElement) {\n        return;\n      }\n    }\n\n    if (!contains(state.elements.popper, arrowElement)) {\n      return;\n    }\n\n    state.elements.arrow = arrowElement;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const arrow$1 = {\n    name: 'arrow',\n    enabled: true,\n    phase: 'main',\n    fn: arrow,\n    effect: effect$1,\n    requires: ['popperOffsets'],\n    requiresIfExists: ['preventOverflow']\n  };\n\n  function getVariation(placement) {\n    return placement.split('-')[1];\n  }\n\n  var unsetSides = {\n    top: 'auto',\n    right: 'auto',\n    bottom: 'auto',\n    left: 'auto'\n  }; // Round the offsets to the nearest suitable subpixel based on the DPR.\n  // Zooming can change the DPR, but it seems to report a value that will\n  // cleanly divide the values into the appropriate subpixels.\n\n  function roundOffsetsByDPR(_ref, win) {\n    var x = _ref.x,\n        y = _ref.y;\n    var dpr = win.devicePixelRatio || 1;\n    return {\n      x: round(x * dpr) / dpr || 0,\n      y: round(y * dpr) / dpr || 0\n    };\n  }\n\n  function mapToStyles(_ref2) {\n    var _Object$assign2;\n\n    var popper = _ref2.popper,\n        popperRect = _ref2.popperRect,\n        placement = _ref2.placement,\n        variation = _ref2.variation,\n        offsets = _ref2.offsets,\n        position = _ref2.position,\n        gpuAcceleration = _ref2.gpuAcceleration,\n        adaptive = _ref2.adaptive,\n        roundOffsets = _ref2.roundOffsets,\n        isFixed = _ref2.isFixed;\n    var _offsets$x = offsets.x,\n        x = _offsets$x === void 0 ? 0 : _offsets$x,\n        _offsets$y = offsets.y,\n        y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n    var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n      x: x,\n      y: y\n    }) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref3.x;\n    y = _ref3.y;\n    var hasX = offsets.hasOwnProperty('x');\n    var hasY = offsets.hasOwnProperty('y');\n    var sideX = left;\n    var sideY = top;\n    var win = window;\n\n    if (adaptive) {\n      var offsetParent = getOffsetParent(popper);\n      var heightProp = 'clientHeight';\n      var widthProp = 'clientWidth';\n\n      if (offsetParent === getWindow(popper)) {\n        offsetParent = getDocumentElement(popper);\n\n        if (getComputedStyle$1(offsetParent).position !== 'static' && position === 'absolute') {\n          heightProp = 'scrollHeight';\n          widthProp = 'scrollWidth';\n        }\n      } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n      offsetParent = offsetParent;\n\n      if (placement === top || (placement === left || placement === right) && variation === end) {\n        sideY = bottom;\n        var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n        offsetParent[heightProp];\n        y -= offsetY - popperRect.height;\n        y *= gpuAcceleration ? 1 : -1;\n      }\n\n      if (placement === left || (placement === top || placement === bottom) && variation === end) {\n        sideX = right;\n        var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n        offsetParent[widthProp];\n        x -= offsetX - popperRect.width;\n        x *= gpuAcceleration ? 1 : -1;\n      }\n    }\n\n    var commonStyles = Object.assign({\n      position: position\n    }, adaptive && unsetSides);\n\n    var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n      x: x,\n      y: y\n    }, getWindow(popper)) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref4.x;\n    y = _ref4.y;\n\n    if (gpuAcceleration) {\n      var _Object$assign;\n\n      return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n    }\n\n    return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n  }\n\n  function computeStyles(_ref5) {\n    var state = _ref5.state,\n        options = _ref5.options;\n    var _options$gpuAccelerat = options.gpuAcceleration,\n        gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n        _options$adaptive = options.adaptive,\n        adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n        _options$roundOffsets = options.roundOffsets,\n        roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n    var commonStyles = {\n      placement: getBasePlacement(state.placement),\n      variation: getVariation(state.placement),\n      popper: state.elements.popper,\n      popperRect: state.rects.popper,\n      gpuAcceleration: gpuAcceleration,\n      isFixed: state.options.strategy === 'fixed'\n    };\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.popperOffsets,\n        position: state.options.strategy,\n        adaptive: adaptive,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    if (state.modifiersData.arrow != null) {\n      state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.arrow,\n        position: 'absolute',\n        adaptive: false,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-placement': state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const computeStyles$1 = {\n    name: 'computeStyles',\n    enabled: true,\n    phase: 'beforeWrite',\n    fn: computeStyles,\n    data: {}\n  };\n\n  var passive = {\n    passive: true\n  };\n\n  function effect(_ref) {\n    var state = _ref.state,\n        instance = _ref.instance,\n        options = _ref.options;\n    var _options$scroll = options.scroll,\n        scroll = _options$scroll === void 0 ? true : _options$scroll,\n        _options$resize = options.resize,\n        resize = _options$resize === void 0 ? true : _options$resize;\n    var window = getWindow(state.elements.popper);\n    var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n    if (scroll) {\n      scrollParents.forEach(function (scrollParent) {\n        scrollParent.addEventListener('scroll', instance.update, passive);\n      });\n    }\n\n    if (resize) {\n      window.addEventListener('resize', instance.update, passive);\n    }\n\n    return function () {\n      if (scroll) {\n        scrollParents.forEach(function (scrollParent) {\n          scrollParent.removeEventListener('scroll', instance.update, passive);\n        });\n      }\n\n      if (resize) {\n        window.removeEventListener('resize', instance.update, passive);\n      }\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const eventListeners = {\n    name: 'eventListeners',\n    enabled: true,\n    phase: 'write',\n    fn: function fn() {},\n    effect: effect,\n    data: {}\n  };\n\n  var hash$1 = {\n    left: 'right',\n    right: 'left',\n    bottom: 'top',\n    top: 'bottom'\n  };\n  function getOppositePlacement(placement) {\n    return placement.replace(/left|right|bottom|top/g, function (matched) {\n      return hash$1[matched];\n    });\n  }\n\n  var hash = {\n    start: 'end',\n    end: 'start'\n  };\n  function getOppositeVariationPlacement(placement) {\n    return placement.replace(/start|end/g, function (matched) {\n      return hash[matched];\n    });\n  }\n\n  function getWindowScroll(node) {\n    var win = getWindow(node);\n    var scrollLeft = win.pageXOffset;\n    var scrollTop = win.pageYOffset;\n    return {\n      scrollLeft: scrollLeft,\n      scrollTop: scrollTop\n    };\n  }\n\n  function getWindowScrollBarX(element) {\n    // If <html> has a CSS width greater than the viewport, then this will be\n    // incorrect for RTL.\n    // Popper 1 is broken in this case and never had a bug report so let's assume\n    // it's not an issue. I don't think anyone ever specifies width on <html>\n    // anyway.\n    // Browsers where the left scrollbar doesn't cause an issue report `0` for\n    // this (e.g. Edge 2019, IE11, Safari)\n    return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n  }\n\n  function getViewportRect(element, strategy) {\n    var win = getWindow(element);\n    var html = getDocumentElement(element);\n    var visualViewport = win.visualViewport;\n    var width = html.clientWidth;\n    var height = html.clientHeight;\n    var x = 0;\n    var y = 0;\n\n    if (visualViewport) {\n      width = visualViewport.width;\n      height = visualViewport.height;\n      var layoutViewport = isLayoutViewport();\n\n      if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n        x = visualViewport.offsetLeft;\n        y = visualViewport.offsetTop;\n      }\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x + getWindowScrollBarX(element),\n      y: y\n    };\n  }\n\n  // of the `<html>` and `<body>` rect bounds if horizontally scrollable\n\n  function getDocumentRect(element) {\n    var _element$ownerDocumen;\n\n    var html = getDocumentElement(element);\n    var winScroll = getWindowScroll(element);\n    var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n    var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n    var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n    var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n    var y = -winScroll.scrollTop;\n\n    if (getComputedStyle$1(body || html).direction === 'rtl') {\n      x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x,\n      y: y\n    };\n  }\n\n  function isScrollParent(element) {\n    // Firefox wants us to check `-x` and `-y` variations as well\n    var _getComputedStyle = getComputedStyle$1(element),\n        overflow = _getComputedStyle.overflow,\n        overflowX = _getComputedStyle.overflowX,\n        overflowY = _getComputedStyle.overflowY;\n\n    return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n  }\n\n  function getScrollParent(node) {\n    if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n      // $FlowFixMe[incompatible-return]: assume body is always available\n      return node.ownerDocument.body;\n    }\n\n    if (isHTMLElement(node) && isScrollParent(node)) {\n      return node;\n    }\n\n    return getScrollParent(getParentNode(node));\n  }\n\n  /*\n  given a DOM element, return the list of all scroll parents, up the list of ancesors\n  until we get to the top window object. This list is what we attach scroll listeners\n  to, because if any of these parent elements scroll, we'll need to re-calculate the\n  reference element's position.\n  */\n\n  function listScrollParents(element, list) {\n    var _element$ownerDocumen;\n\n    if (list === void 0) {\n      list = [];\n    }\n\n    var scrollParent = getScrollParent(element);\n    var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n    var win = getWindow(scrollParent);\n    var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n    var updatedList = list.concat(target);\n    return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n    updatedList.concat(listScrollParents(getParentNode(target)));\n  }\n\n  function rectToClientRect(rect) {\n    return Object.assign({}, rect, {\n      left: rect.x,\n      top: rect.y,\n      right: rect.x + rect.width,\n      bottom: rect.y + rect.height\n    });\n  }\n\n  function getInnerBoundingClientRect(element, strategy) {\n    var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n    rect.top = rect.top + element.clientTop;\n    rect.left = rect.left + element.clientLeft;\n    rect.bottom = rect.top + element.clientHeight;\n    rect.right = rect.left + element.clientWidth;\n    rect.width = element.clientWidth;\n    rect.height = element.clientHeight;\n    rect.x = rect.left;\n    rect.y = rect.top;\n    return rect;\n  }\n\n  function getClientRectFromMixedType(element, clippingParent, strategy) {\n    return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n  } // A \"clipping parent\" is an overflowable container with the characteristic of\n  // clipping (or hiding) overflowing elements with a position different from\n  // `initial`\n\n\n  function getClippingParents(element) {\n    var clippingParents = listScrollParents(getParentNode(element));\n    var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle$1(element).position) >= 0;\n    var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n    if (!isElement(clipperElement)) {\n      return [];\n    } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n    return clippingParents.filter(function (clippingParent) {\n      return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n    });\n  } // Gets the maximum area that the element is visible in due to any number of\n  // clipping parents\n\n\n  function getClippingRect(element, boundary, rootBoundary, strategy) {\n    var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n    var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n    var firstClippingParent = clippingParents[0];\n    var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n      var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n      accRect.top = max(rect.top, accRect.top);\n      accRect.right = min(rect.right, accRect.right);\n      accRect.bottom = min(rect.bottom, accRect.bottom);\n      accRect.left = max(rect.left, accRect.left);\n      return accRect;\n    }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n    clippingRect.width = clippingRect.right - clippingRect.left;\n    clippingRect.height = clippingRect.bottom - clippingRect.top;\n    clippingRect.x = clippingRect.left;\n    clippingRect.y = clippingRect.top;\n    return clippingRect;\n  }\n\n  function computeOffsets(_ref) {\n    var reference = _ref.reference,\n        element = _ref.element,\n        placement = _ref.placement;\n    var basePlacement = placement ? getBasePlacement(placement) : null;\n    var variation = placement ? getVariation(placement) : null;\n    var commonX = reference.x + reference.width / 2 - element.width / 2;\n    var commonY = reference.y + reference.height / 2 - element.height / 2;\n    var offsets;\n\n    switch (basePlacement) {\n      case top:\n        offsets = {\n          x: commonX,\n          y: reference.y - element.height\n        };\n        break;\n\n      case bottom:\n        offsets = {\n          x: commonX,\n          y: reference.y + reference.height\n        };\n        break;\n\n      case right:\n        offsets = {\n          x: reference.x + reference.width,\n          y: commonY\n        };\n        break;\n\n      case left:\n        offsets = {\n          x: reference.x - element.width,\n          y: commonY\n        };\n        break;\n\n      default:\n        offsets = {\n          x: reference.x,\n          y: reference.y\n        };\n    }\n\n    var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n    if (mainAxis != null) {\n      var len = mainAxis === 'y' ? 'height' : 'width';\n\n      switch (variation) {\n        case start:\n          offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n          break;\n\n        case end:\n          offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n          break;\n      }\n    }\n\n    return offsets;\n  }\n\n  function detectOverflow(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        _options$placement = _options.placement,\n        placement = _options$placement === void 0 ? state.placement : _options$placement,\n        _options$strategy = _options.strategy,\n        strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n        _options$boundary = _options.boundary,\n        boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n        _options$rootBoundary = _options.rootBoundary,\n        rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n        _options$elementConte = _options.elementContext,\n        elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n        _options$altBoundary = _options.altBoundary,\n        altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n        _options$padding = _options.padding,\n        padding = _options$padding === void 0 ? 0 : _options$padding;\n    var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n    var altContext = elementContext === popper ? reference : popper;\n    var popperRect = state.rects.popper;\n    var element = state.elements[altBoundary ? altContext : elementContext];\n    var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n    var referenceClientRect = getBoundingClientRect(state.elements.reference);\n    var popperOffsets = computeOffsets({\n      reference: referenceClientRect,\n      element: popperRect,\n      placement: placement\n    });\n    var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n    var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n    // 0 or negative = within the clipping rect\n\n    var overflowOffsets = {\n      top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n      bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n      left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n      right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n    };\n    var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n    if (elementContext === popper && offsetData) {\n      var offset = offsetData[placement];\n      Object.keys(overflowOffsets).forEach(function (key) {\n        var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n        var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n        overflowOffsets[key] += offset[axis] * multiply;\n      });\n    }\n\n    return overflowOffsets;\n  }\n\n  function computeAutoPlacement(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        placement = _options.placement,\n        boundary = _options.boundary,\n        rootBoundary = _options.rootBoundary,\n        padding = _options.padding,\n        flipVariations = _options.flipVariations,\n        _options$allowedAutoP = _options.allowedAutoPlacements,\n        allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP;\n    var variation = getVariation(placement);\n    var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n      return getVariation(placement) === variation;\n    }) : basePlacements;\n    var allowedPlacements = placements$1.filter(function (placement) {\n      return allowedAutoPlacements.indexOf(placement) >= 0;\n    });\n\n    if (allowedPlacements.length === 0) {\n      allowedPlacements = placements$1;\n    } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n    var overflows = allowedPlacements.reduce(function (acc, placement) {\n      acc[placement] = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding\n      })[getBasePlacement(placement)];\n      return acc;\n    }, {});\n    return Object.keys(overflows).sort(function (a, b) {\n      return overflows[a] - overflows[b];\n    });\n  }\n\n  function getExpandedFallbackPlacements(placement) {\n    if (getBasePlacement(placement) === auto) {\n      return [];\n    }\n\n    var oppositePlacement = getOppositePlacement(placement);\n    return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n  }\n\n  function flip(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n\n    if (state.modifiersData[name]._skip) {\n      return;\n    }\n\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n        specifiedFallbackPlacements = options.fallbackPlacements,\n        padding = options.padding,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        _options$flipVariatio = options.flipVariations,\n        flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n        allowedAutoPlacements = options.allowedAutoPlacements;\n    var preferredPlacement = state.options.placement;\n    var basePlacement = getBasePlacement(preferredPlacement);\n    var isBasePlacement = basePlacement === preferredPlacement;\n    var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n    var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n      return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding,\n        flipVariations: flipVariations,\n        allowedAutoPlacements: allowedAutoPlacements\n      }) : placement);\n    }, []);\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var checksMap = new Map();\n    var makeFallbackChecks = true;\n    var firstFittingPlacement = placements[0];\n\n    for (var i = 0; i < placements.length; i++) {\n      var placement = placements[i];\n\n      var _basePlacement = getBasePlacement(placement);\n\n      var isStartVariation = getVariation(placement) === start;\n      var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n      var len = isVertical ? 'width' : 'height';\n      var overflow = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        altBoundary: altBoundary,\n        padding: padding\n      });\n      var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n      if (referenceRect[len] > popperRect[len]) {\n        mainVariationSide = getOppositePlacement(mainVariationSide);\n      }\n\n      var altVariationSide = getOppositePlacement(mainVariationSide);\n      var checks = [];\n\n      if (checkMainAxis) {\n        checks.push(overflow[_basePlacement] <= 0);\n      }\n\n      if (checkAltAxis) {\n        checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n      }\n\n      if (checks.every(function (check) {\n        return check;\n      })) {\n        firstFittingPlacement = placement;\n        makeFallbackChecks = false;\n        break;\n      }\n\n      checksMap.set(placement, checks);\n    }\n\n    if (makeFallbackChecks) {\n      // `2` may be desired in some cases – research later\n      var numberOfChecks = flipVariations ? 3 : 1;\n\n      var _loop = function _loop(_i) {\n        var fittingPlacement = placements.find(function (placement) {\n          var checks = checksMap.get(placement);\n\n          if (checks) {\n            return checks.slice(0, _i).every(function (check) {\n              return check;\n            });\n          }\n        });\n\n        if (fittingPlacement) {\n          firstFittingPlacement = fittingPlacement;\n          return \"break\";\n        }\n      };\n\n      for (var _i = numberOfChecks; _i > 0; _i--) {\n        var _ret = _loop(_i);\n\n        if (_ret === \"break\") break;\n      }\n    }\n\n    if (state.placement !== firstFittingPlacement) {\n      state.modifiersData[name]._skip = true;\n      state.placement = firstFittingPlacement;\n      state.reset = true;\n    }\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const flip$1 = {\n    name: 'flip',\n    enabled: true,\n    phase: 'main',\n    fn: flip,\n    requiresIfExists: ['offset'],\n    data: {\n      _skip: false\n    }\n  };\n\n  function getSideOffsets(overflow, rect, preventedOffsets) {\n    if (preventedOffsets === void 0) {\n      preventedOffsets = {\n        x: 0,\n        y: 0\n      };\n    }\n\n    return {\n      top: overflow.top - rect.height - preventedOffsets.y,\n      right: overflow.right - rect.width + preventedOffsets.x,\n      bottom: overflow.bottom - rect.height + preventedOffsets.y,\n      left: overflow.left - rect.width - preventedOffsets.x\n    };\n  }\n\n  function isAnySideFullyClipped(overflow) {\n    return [top, right, bottom, left].some(function (side) {\n      return overflow[side] >= 0;\n    });\n  }\n\n  function hide(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var preventedOffsets = state.modifiersData.preventOverflow;\n    var referenceOverflow = detectOverflow(state, {\n      elementContext: 'reference'\n    });\n    var popperAltOverflow = detectOverflow(state, {\n      altBoundary: true\n    });\n    var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n    var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n    var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n    var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n    state.modifiersData[name] = {\n      referenceClippingOffsets: referenceClippingOffsets,\n      popperEscapeOffsets: popperEscapeOffsets,\n      isReferenceHidden: isReferenceHidden,\n      hasPopperEscaped: hasPopperEscaped\n    };\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-reference-hidden': isReferenceHidden,\n      'data-popper-escaped': hasPopperEscaped\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const hide$1 = {\n    name: 'hide',\n    enabled: true,\n    phase: 'main',\n    requiresIfExists: ['preventOverflow'],\n    fn: hide\n  };\n\n  function distanceAndSkiddingToXY(placement, rects, offset) {\n    var basePlacement = getBasePlacement(placement);\n    var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n    var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n      placement: placement\n    })) : offset,\n        skidding = _ref[0],\n        distance = _ref[1];\n\n    skidding = skidding || 0;\n    distance = (distance || 0) * invertDistance;\n    return [left, right].indexOf(basePlacement) >= 0 ? {\n      x: distance,\n      y: skidding\n    } : {\n      x: skidding,\n      y: distance\n    };\n  }\n\n  function offset(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options,\n        name = _ref2.name;\n    var _options$offset = options.offset,\n        offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n    var data = placements.reduce(function (acc, placement) {\n      acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n      return acc;\n    }, {});\n    var _data$state$placement = data[state.placement],\n        x = _data$state$placement.x,\n        y = _data$state$placement.y;\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.modifiersData.popperOffsets.x += x;\n      state.modifiersData.popperOffsets.y += y;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const offset$1 = {\n    name: 'offset',\n    enabled: true,\n    phase: 'main',\n    requires: ['popperOffsets'],\n    fn: offset\n  };\n\n  function popperOffsets(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    // Offsets are the actual position the popper needs to have to be\n    // properly positioned near its reference element\n    // This is the most basic placement, and will be adjusted by\n    // the modifiers in the next step\n    state.modifiersData[name] = computeOffsets({\n      reference: state.rects.reference,\n      element: state.rects.popper,\n      placement: state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const popperOffsets$1 = {\n    name: 'popperOffsets',\n    enabled: true,\n    phase: 'read',\n    fn: popperOffsets,\n    data: {}\n  };\n\n  function getAltAxis(axis) {\n    return axis === 'x' ? 'y' : 'x';\n  }\n\n  function preventOverflow(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        padding = options.padding,\n        _options$tether = options.tether,\n        tether = _options$tether === void 0 ? true : _options$tether,\n        _options$tetherOffset = options.tetherOffset,\n        tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n    var overflow = detectOverflow(state, {\n      boundary: boundary,\n      rootBoundary: rootBoundary,\n      padding: padding,\n      altBoundary: altBoundary\n    });\n    var basePlacement = getBasePlacement(state.placement);\n    var variation = getVariation(state.placement);\n    var isBasePlacement = !variation;\n    var mainAxis = getMainAxisFromPlacement(basePlacement);\n    var altAxis = getAltAxis(mainAxis);\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : tetherOffset;\n    var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n      mainAxis: tetherOffsetValue,\n      altAxis: tetherOffsetValue\n    } : Object.assign({\n      mainAxis: 0,\n      altAxis: 0\n    }, tetherOffsetValue);\n    var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n    var data = {\n      x: 0,\n      y: 0\n    };\n\n    if (!popperOffsets) {\n      return;\n    }\n\n    if (checkMainAxis) {\n      var _offsetModifierState$;\n\n      var mainSide = mainAxis === 'y' ? top : left;\n      var altSide = mainAxis === 'y' ? bottom : right;\n      var len = mainAxis === 'y' ? 'height' : 'width';\n      var offset = popperOffsets[mainAxis];\n      var min$1 = offset + overflow[mainSide];\n      var max$1 = offset - overflow[altSide];\n      var additive = tether ? -popperRect[len] / 2 : 0;\n      var minLen = variation === start ? referenceRect[len] : popperRect[len];\n      var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n      // outside the reference bounds\n\n      var arrowElement = state.elements.arrow;\n      var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n        width: 0,\n        height: 0\n      };\n      var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n      var arrowPaddingMin = arrowPaddingObject[mainSide];\n      var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n      // to include its full size in the calculation. If the reference is small\n      // and near the edge of a boundary, the popper can overflow even if the\n      // reference is not overflowing as well (e.g. virtual elements with no\n      // width or height)\n\n      var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n      var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n      var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n      var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n      var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n      var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n      var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n      var tetherMax = offset + maxOffset - offsetModifierValue;\n      var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1);\n      popperOffsets[mainAxis] = preventedOffset;\n      data[mainAxis] = preventedOffset - offset;\n    }\n\n    if (checkAltAxis) {\n      var _offsetModifierState$2;\n\n      var _mainSide = mainAxis === 'x' ? top : left;\n\n      var _altSide = mainAxis === 'x' ? bottom : right;\n\n      var _offset = popperOffsets[altAxis];\n\n      var _len = altAxis === 'y' ? 'height' : 'width';\n\n      var _min = _offset + overflow[_mainSide];\n\n      var _max = _offset - overflow[_altSide];\n\n      var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n      var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n      var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n      var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n      var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n      popperOffsets[altAxis] = _preventedOffset;\n      data[altAxis] = _preventedOffset - _offset;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  const preventOverflow$1 = {\n    name: 'preventOverflow',\n    enabled: true,\n    phase: 'main',\n    fn: preventOverflow,\n    requiresIfExists: ['offset']\n  };\n\n  function getHTMLElementScroll(element) {\n    return {\n      scrollLeft: element.scrollLeft,\n      scrollTop: element.scrollTop\n    };\n  }\n\n  function getNodeScroll(node) {\n    if (node === getWindow(node) || !isHTMLElement(node)) {\n      return getWindowScroll(node);\n    } else {\n      return getHTMLElementScroll(node);\n    }\n  }\n\n  function isElementScaled(element) {\n    var rect = element.getBoundingClientRect();\n    var scaleX = round(rect.width) / element.offsetWidth || 1;\n    var scaleY = round(rect.height) / element.offsetHeight || 1;\n    return scaleX !== 1 || scaleY !== 1;\n  } // Returns the composite rect of an element relative to its offsetParent.\n  // Composite means it takes into account transforms as well as layout.\n\n\n  function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n    if (isFixed === void 0) {\n      isFixed = false;\n    }\n\n    var isOffsetParentAnElement = isHTMLElement(offsetParent);\n    var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n    var documentElement = getDocumentElement(offsetParent);\n    var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n    var scroll = {\n      scrollLeft: 0,\n      scrollTop: 0\n    };\n    var offsets = {\n      x: 0,\n      y: 0\n    };\n\n    if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n      if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n      isScrollParent(documentElement)) {\n        scroll = getNodeScroll(offsetParent);\n      }\n\n      if (isHTMLElement(offsetParent)) {\n        offsets = getBoundingClientRect(offsetParent, true);\n        offsets.x += offsetParent.clientLeft;\n        offsets.y += offsetParent.clientTop;\n      } else if (documentElement) {\n        offsets.x = getWindowScrollBarX(documentElement);\n      }\n    }\n\n    return {\n      x: rect.left + scroll.scrollLeft - offsets.x,\n      y: rect.top + scroll.scrollTop - offsets.y,\n      width: rect.width,\n      height: rect.height\n    };\n  }\n\n  function order(modifiers) {\n    var map = new Map();\n    var visited = new Set();\n    var result = [];\n    modifiers.forEach(function (modifier) {\n      map.set(modifier.name, modifier);\n    }); // On visiting object, check for its dependencies and visit them recursively\n\n    function sort(modifier) {\n      visited.add(modifier.name);\n      var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n      requires.forEach(function (dep) {\n        if (!visited.has(dep)) {\n          var depModifier = map.get(dep);\n\n          if (depModifier) {\n            sort(depModifier);\n          }\n        }\n      });\n      result.push(modifier);\n    }\n\n    modifiers.forEach(function (modifier) {\n      if (!visited.has(modifier.name)) {\n        // check for visited object\n        sort(modifier);\n      }\n    });\n    return result;\n  }\n\n  function orderModifiers(modifiers) {\n    // order based on dependencies\n    var orderedModifiers = order(modifiers); // order based on phase\n\n    return modifierPhases.reduce(function (acc, phase) {\n      return acc.concat(orderedModifiers.filter(function (modifier) {\n        return modifier.phase === phase;\n      }));\n    }, []);\n  }\n\n  function debounce(fn) {\n    var pending;\n    return function () {\n      if (!pending) {\n        pending = new Promise(function (resolve) {\n          Promise.resolve().then(function () {\n            pending = undefined;\n            resolve(fn());\n          });\n        });\n      }\n\n      return pending;\n    };\n  }\n\n  function mergeByName(modifiers) {\n    var merged = modifiers.reduce(function (merged, current) {\n      var existing = merged[current.name];\n      merged[current.name] = existing ? Object.assign({}, existing, current, {\n        options: Object.assign({}, existing.options, current.options),\n        data: Object.assign({}, existing.data, current.data)\n      }) : current;\n      return merged;\n    }, {}); // IE11 does not support Object.values\n\n    return Object.keys(merged).map(function (key) {\n      return merged[key];\n    });\n  }\n\n  var DEFAULT_OPTIONS = {\n    placement: 'bottom',\n    modifiers: [],\n    strategy: 'absolute'\n  };\n\n  function areValidElements() {\n    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n      args[_key] = arguments[_key];\n    }\n\n    return !args.some(function (element) {\n      return !(element && typeof element.getBoundingClientRect === 'function');\n    });\n  }\n\n  function popperGenerator(generatorOptions) {\n    if (generatorOptions === void 0) {\n      generatorOptions = {};\n    }\n\n    var _generatorOptions = generatorOptions,\n        _generatorOptions$def = _generatorOptions.defaultModifiers,\n        defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n        _generatorOptions$def2 = _generatorOptions.defaultOptions,\n        defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n    return function createPopper(reference, popper, options) {\n      if (options === void 0) {\n        options = defaultOptions;\n      }\n\n      var state = {\n        placement: 'bottom',\n        orderedModifiers: [],\n        options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n        modifiersData: {},\n        elements: {\n          reference: reference,\n          popper: popper\n        },\n        attributes: {},\n        styles: {}\n      };\n      var effectCleanupFns = [];\n      var isDestroyed = false;\n      var instance = {\n        state: state,\n        setOptions: function setOptions(setOptionsAction) {\n          var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n          cleanupModifierEffects();\n          state.options = Object.assign({}, defaultOptions, state.options, options);\n          state.scrollParents = {\n            reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n            popper: listScrollParents(popper)\n          }; // Orders the modifiers based on their dependencies and `phase`\n          // properties\n\n          var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n          state.orderedModifiers = orderedModifiers.filter(function (m) {\n            return m.enabled;\n          });\n          runModifierEffects();\n          return instance.update();\n        },\n        // Sync update – it will always be executed, even if not necessary. This\n        // is useful for low frequency updates where sync behavior simplifies the\n        // logic.\n        // For high frequency updates (e.g. `resize` and `scroll` events), always\n        // prefer the async Popper#update method\n        forceUpdate: function forceUpdate() {\n          if (isDestroyed) {\n            return;\n          }\n\n          var _state$elements = state.elements,\n              reference = _state$elements.reference,\n              popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n          // anymore\n\n          if (!areValidElements(reference, popper)) {\n            return;\n          } // Store the reference and popper rects to be read by modifiers\n\n\n          state.rects = {\n            reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n            popper: getLayoutRect(popper)\n          }; // Modifiers have the ability to reset the current update cycle. The\n          // most common use case for this is the `flip` modifier changing the\n          // placement, which then needs to re-run all the modifiers, because the\n          // logic was previously ran for the previous placement and is therefore\n          // stale/incorrect\n\n          state.reset = false;\n          state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n          // is filled with the initial data specified by the modifier. This means\n          // it doesn't persist and is fresh on each update.\n          // To ensure persistent data, use `${name}#persistent`\n\n          state.orderedModifiers.forEach(function (modifier) {\n            return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n          });\n\n          for (var index = 0; index < state.orderedModifiers.length; index++) {\n            if (state.reset === true) {\n              state.reset = false;\n              index = -1;\n              continue;\n            }\n\n            var _state$orderedModifie = state.orderedModifiers[index],\n                fn = _state$orderedModifie.fn,\n                _state$orderedModifie2 = _state$orderedModifie.options,\n                _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n                name = _state$orderedModifie.name;\n\n            if (typeof fn === 'function') {\n              state = fn({\n                state: state,\n                options: _options,\n                name: name,\n                instance: instance\n              }) || state;\n            }\n          }\n        },\n        // Async and optimistically optimized update – it will not be executed if\n        // not necessary (debounced to run at most once-per-tick)\n        update: debounce(function () {\n          return new Promise(function (resolve) {\n            instance.forceUpdate();\n            resolve(state);\n          });\n        }),\n        destroy: function destroy() {\n          cleanupModifierEffects();\n          isDestroyed = true;\n        }\n      };\n\n      if (!areValidElements(reference, popper)) {\n        return instance;\n      }\n\n      instance.setOptions(options).then(function (state) {\n        if (!isDestroyed && options.onFirstUpdate) {\n          options.onFirstUpdate(state);\n        }\n      }); // Modifiers have the ability to execute arbitrary code before the first\n      // update cycle runs. They will be executed in the same order as the update\n      // cycle. This is useful when a modifier adds some persistent data that\n      // other modifiers need to use, but the modifier is run after the dependent\n      // one.\n\n      function runModifierEffects() {\n        state.orderedModifiers.forEach(function (_ref) {\n          var name = _ref.name,\n              _ref$options = _ref.options,\n              options = _ref$options === void 0 ? {} : _ref$options,\n              effect = _ref.effect;\n\n          if (typeof effect === 'function') {\n            var cleanupFn = effect({\n              state: state,\n              name: name,\n              instance: instance,\n              options: options\n            });\n\n            var noopFn = function noopFn() {};\n\n            effectCleanupFns.push(cleanupFn || noopFn);\n          }\n        });\n      }\n\n      function cleanupModifierEffects() {\n        effectCleanupFns.forEach(function (fn) {\n          return fn();\n        });\n        effectCleanupFns = [];\n      }\n\n      return instance;\n    };\n  }\n  var createPopper$2 = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\n  var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1];\n  var createPopper$1 = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers$1\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1];\n  var createPopper = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  const Popper = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({\n    __proto__: null,\n    afterMain,\n    afterRead,\n    afterWrite,\n    applyStyles: applyStyles$1,\n    arrow: arrow$1,\n    auto,\n    basePlacements,\n    beforeMain,\n    beforeRead,\n    beforeWrite,\n    bottom,\n    clippingParents,\n    computeStyles: computeStyles$1,\n    createPopper,\n    createPopperBase: createPopper$2,\n    createPopperLite: createPopper$1,\n    detectOverflow,\n    end,\n    eventListeners,\n    flip: flip$1,\n    hide: hide$1,\n    left,\n    main,\n    modifierPhases,\n    offset: offset$1,\n    placements,\n    popper,\n    popperGenerator,\n    popperOffsets: popperOffsets$1,\n    preventOverflow: preventOverflow$1,\n    read,\n    reference,\n    right,\n    start,\n    top,\n    variationPlacements,\n    viewport,\n    write\n  }, Symbol.toStringTag, { value: 'Module' }));\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dropdown.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$a = 'dropdown';\n  const DATA_KEY$6 = 'bs.dropdown';\n  const EVENT_KEY$6 = `.${DATA_KEY$6}`;\n  const DATA_API_KEY$3 = '.data-api';\n  const ESCAPE_KEY$2 = 'Escape';\n  const TAB_KEY$1 = 'Tab';\n  const ARROW_UP_KEY$1 = 'ArrowUp';\n  const ARROW_DOWN_KEY$1 = 'ArrowDown';\n  const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\n  const EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\n  const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\n  const EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\n  const EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\n  const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\n  const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\n  const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\n  const CLASS_NAME_SHOW$6 = 'show';\n  const CLASS_NAME_DROPUP = 'dropup';\n  const CLASS_NAME_DROPEND = 'dropend';\n  const CLASS_NAME_DROPSTART = 'dropstart';\n  const CLASS_NAME_DROPUP_CENTER = 'dropup-center';\n  const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\n  const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\n  const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\n  const SELECTOR_MENU = '.dropdown-menu';\n  const SELECTOR_NAVBAR = '.navbar';\n  const SELECTOR_NAVBAR_NAV = '.navbar-nav';\n  const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\n  const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\n  const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\n  const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\n  const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\n  const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\n  const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\n  const PLACEMENT_TOPCENTER = 'top';\n  const PLACEMENT_BOTTOMCENTER = 'bottom';\n  const Default$9 = {\n    autoClose: true,\n    boundary: 'clippingParents',\n    display: 'dynamic',\n    offset: [0, 2],\n    popperConfig: null,\n    reference: 'toggle'\n  };\n  const DefaultType$9 = {\n    autoClose: '(boolean|string)',\n    boundary: '(string|element)',\n    display: 'string',\n    offset: '(array|string|function)',\n    popperConfig: '(null|object|function)',\n    reference: '(string|element|object)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Dropdown extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._popper = null;\n      this._parent = this._element.parentNode; // dropdown wrapper\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n      this._inNavbar = this._detectNavbar();\n    }\n\n    // Getters\n    static get Default() {\n      return Default$9;\n    }\n    static get DefaultType() {\n      return DefaultType$9;\n    }\n    static get NAME() {\n      return NAME$a;\n    }\n\n    // Public\n    toggle() {\n      return this._isShown() ? this.hide() : this.show();\n    }\n    show() {\n      if (isDisabled(this._element) || this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._createPopper();\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', noop);\n        }\n      }\n      this._element.focus();\n      this._element.setAttribute('aria-expanded', true);\n      this._menu.classList.add(CLASS_NAME_SHOW$6);\n      this._element.classList.add(CLASS_NAME_SHOW$6);\n      EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n    }\n    hide() {\n      if (isDisabled(this._element) || !this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      this._completeHide(relatedTarget);\n    }\n    dispose() {\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      super.dispose();\n    }\n    update() {\n      this._inNavbar = this._detectNavbar();\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Private\n    _completeHide(relatedTarget) {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', noop);\n        }\n      }\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      this._menu.classList.remove(CLASS_NAME_SHOW$6);\n      this._element.classList.remove(CLASS_NAME_SHOW$6);\n      this._element.setAttribute('aria-expanded', 'false');\n      Manipulator.removeDataAttribute(this._menu, 'popper');\n      EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n    }\n    _getConfig(config) {\n      config = super._getConfig(config);\n      if (typeof config.reference === 'object' && !isElement$1(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n        // Popper virtual elements require a getBoundingClientRect method\n        throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n      }\n      return config;\n    }\n    _createPopper() {\n      if (typeof Popper === 'undefined') {\n        throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');\n      }\n      let referenceElement = this._element;\n      if (this._config.reference === 'parent') {\n        referenceElement = this._parent;\n      } else if (isElement$1(this._config.reference)) {\n        referenceElement = getElement(this._config.reference);\n      } else if (typeof this._config.reference === 'object') {\n        referenceElement = this._config.reference;\n      }\n      const popperConfig = this._getPopperConfig();\n      this._popper = createPopper(referenceElement, this._menu, popperConfig);\n    }\n    _isShown() {\n      return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n    }\n    _getPlacement() {\n      const parentDropdown = this._parent;\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n        return PLACEMENT_RIGHT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n        return PLACEMENT_LEFT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n        return PLACEMENT_TOPCENTER;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n        return PLACEMENT_BOTTOMCENTER;\n      }\n\n      // We need to trim the value because custom properties can also include spaces\n      const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n        return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n      }\n      return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n    }\n    _detectNavbar() {\n      return this._element.closest(SELECTOR_NAVBAR) !== null;\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _getPopperConfig() {\n      const defaultBsPopperConfig = {\n        placement: this._getPlacement(),\n        modifiers: [{\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }]\n      };\n\n      // Disable Popper if we have a static display or Dropdown is in Navbar\n      if (this._inNavbar || this._config.display === 'static') {\n        Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n        defaultBsPopperConfig.modifiers = [{\n          name: 'applyStyles',\n          enabled: false\n        }];\n      }\n      return {\n        ...defaultBsPopperConfig,\n        ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n      };\n    }\n    _selectMenuItem({\n      key,\n      target\n    }) {\n      const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n      if (!items.length) {\n        return;\n      }\n\n      // if target isn't included in items (e.g. when expanding the dropdown)\n      // allow cycling to get the last item in case key equals ARROW_UP_KEY\n      getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Dropdown.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n    static clearMenus(event) {\n      if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n        return;\n      }\n      const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n      for (const toggle of openToggles) {\n        const context = Dropdown.getInstance(toggle);\n        if (!context || context._config.autoClose === false) {\n          continue;\n        }\n        const composedPath = event.composedPath();\n        const isMenuTarget = composedPath.includes(context._menu);\n        if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n          continue;\n        }\n\n        // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n        if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n          continue;\n        }\n        const relatedTarget = {\n          relatedTarget: context._element\n        };\n        if (event.type === 'click') {\n          relatedTarget.clickEvent = event;\n        }\n        context._completeHide(relatedTarget);\n      }\n    }\n    static dataApiKeydownHandler(event) {\n      // If not an UP | DOWN | ESCAPE key => not a dropdown command\n      // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n      const isInput = /input|textarea/i.test(event.target.tagName);\n      const isEscapeEvent = event.key === ESCAPE_KEY$2;\n      const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n      if (!isUpOrDownEvent && !isEscapeEvent) {\n        return;\n      }\n      if (isInput && !isEscapeEvent) {\n        return;\n      }\n      event.preventDefault();\n\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n      const instance = Dropdown.getOrCreateInstance(getToggleButton);\n      if (isUpOrDownEvent) {\n        event.stopPropagation();\n        instance.show();\n        instance._selectMenuItem(event);\n        return;\n      }\n      if (instance._isShown()) {\n        // else is escape and we check if it is shown\n        event.stopPropagation();\n        instance.hide();\n        getToggleButton.focus();\n      }\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n    event.preventDefault();\n    Dropdown.getOrCreateInstance(this).toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Dropdown);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/backdrop.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$9 = 'backdrop';\n  const CLASS_NAME_FADE$4 = 'fade';\n  const CLASS_NAME_SHOW$5 = 'show';\n  const EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\n  const Default$8 = {\n    className: 'modal-backdrop',\n    clickCallback: null,\n    isAnimated: false,\n    isVisible: true,\n    // if false, we use the backdrop helper without adding any element to the dom\n    rootElement: 'body' // give the choice to place backdrop under different elements\n  };\n  const DefaultType$8 = {\n    className: 'string',\n    clickCallback: '(function|null)',\n    isAnimated: 'boolean',\n    isVisible: 'boolean',\n    rootElement: '(element|string)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Backdrop extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isAppended = false;\n      this._element = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default$8;\n    }\n    static get DefaultType() {\n      return DefaultType$8;\n    }\n    static get NAME() {\n      return NAME$9;\n    }\n\n    // Public\n    show(callback) {\n      if (!this._config.isVisible) {\n        execute(callback);\n        return;\n      }\n      this._append();\n      const element = this._getElement();\n      if (this._config.isAnimated) {\n        reflow(element);\n      }\n      element.classList.add(CLASS_NAME_SHOW$5);\n      this._emulateAnimation(() => {\n        execute(callback);\n      });\n    }\n    hide(callback) {\n      if (!this._config.isVisible) {\n        execute(callback);\n        return;\n      }\n      this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n      this._emulateAnimation(() => {\n        this.dispose();\n        execute(callback);\n      });\n    }\n    dispose() {\n      if (!this._isAppended) {\n        return;\n      }\n      EventHandler.off(this._element, EVENT_MOUSEDOWN);\n      this._element.remove();\n      this._isAppended = false;\n    }\n\n    // Private\n    _getElement() {\n      if (!this._element) {\n        const backdrop = document.createElement('div');\n        backdrop.className = this._config.className;\n        if (this._config.isAnimated) {\n          backdrop.classList.add(CLASS_NAME_FADE$4);\n        }\n        this._element = backdrop;\n      }\n      return this._element;\n    }\n    _configAfterMerge(config) {\n      // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n      config.rootElement = getElement(config.rootElement);\n      return config;\n    }\n    _append() {\n      if (this._isAppended) {\n        return;\n      }\n      const element = this._getElement();\n      this._config.rootElement.append(element);\n      EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n        execute(this._config.clickCallback);\n      });\n      this._isAppended = true;\n    }\n    _emulateAnimation(callback) {\n      executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/focustrap.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$8 = 'focustrap';\n  const DATA_KEY$5 = 'bs.focustrap';\n  const EVENT_KEY$5 = `.${DATA_KEY$5}`;\n  const EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\n  const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\n  const TAB_KEY = 'Tab';\n  const TAB_NAV_FORWARD = 'forward';\n  const TAB_NAV_BACKWARD = 'backward';\n  const Default$7 = {\n    autofocus: true,\n    trapElement: null // The element to trap focus inside of\n  };\n  const DefaultType$7 = {\n    autofocus: 'boolean',\n    trapElement: 'element'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class FocusTrap extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isActive = false;\n      this._lastTabNavDirection = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default$7;\n    }\n    static get DefaultType() {\n      return DefaultType$7;\n    }\n    static get NAME() {\n      return NAME$8;\n    }\n\n    // Public\n    activate() {\n      if (this._isActive) {\n        return;\n      }\n      if (this._config.autofocus) {\n        this._config.trapElement.focus();\n      }\n      EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n      EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n      EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n      this._isActive = true;\n    }\n    deactivate() {\n      if (!this._isActive) {\n        return;\n      }\n      this._isActive = false;\n      EventHandler.off(document, EVENT_KEY$5);\n    }\n\n    // Private\n    _handleFocusin(event) {\n      const {\n        trapElement\n      } = this._config;\n      if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n        return;\n      }\n      const elements = SelectorEngine.focusableChildren(trapElement);\n      if (elements.length === 0) {\n        trapElement.focus();\n      } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n        elements[elements.length - 1].focus();\n      } else {\n        elements[0].focus();\n      }\n    }\n    _handleKeydown(event) {\n      if (event.key !== TAB_KEY) {\n        return;\n      }\n      this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/scrollBar.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\n  const SELECTOR_STICKY_CONTENT = '.sticky-top';\n  const PROPERTY_PADDING = 'padding-right';\n  const PROPERTY_MARGIN = 'margin-right';\n\n  /**\n   * Class definition\n   */\n\n  class ScrollBarHelper {\n    constructor() {\n      this._element = document.body;\n    }\n\n    // Public\n    getWidth() {\n      // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n      const documentWidth = document.documentElement.clientWidth;\n      return Math.abs(window.innerWidth - documentWidth);\n    }\n    hide() {\n      const width = this.getWidth();\n      this._disableOverFlow();\n      // give padding to element to balance the hidden scrollbar width\n      this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n      this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n    }\n    reset() {\n      this._resetElementAttributes(this._element, 'overflow');\n      this._resetElementAttributes(this._element, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n    }\n    isOverflowing() {\n      return this.getWidth() > 0;\n    }\n\n    // Private\n    _disableOverFlow() {\n      this._saveInitialAttribute(this._element, 'overflow');\n      this._element.style.overflow = 'hidden';\n    }\n    _setElementAttributes(selector, styleProperty, callback) {\n      const scrollbarWidth = this.getWidth();\n      const manipulationCallBack = element => {\n        if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n          return;\n        }\n        this._saveInitialAttribute(element, styleProperty);\n        const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n        element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _saveInitialAttribute(element, styleProperty) {\n      const actualValue = element.style.getPropertyValue(styleProperty);\n      if (actualValue) {\n        Manipulator.setDataAttribute(element, styleProperty, actualValue);\n      }\n    }\n    _resetElementAttributes(selector, styleProperty) {\n      const manipulationCallBack = element => {\n        const value = Manipulator.getDataAttribute(element, styleProperty);\n        // We only want to remove the property if the value is `null`; the value can also be zero\n        if (value === null) {\n          element.style.removeProperty(styleProperty);\n          return;\n        }\n        Manipulator.removeDataAttribute(element, styleProperty);\n        element.style.setProperty(styleProperty, value);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _applyManipulationCallback(selector, callBack) {\n      if (isElement$1(selector)) {\n        callBack(selector);\n        return;\n      }\n      for (const sel of SelectorEngine.find(selector, this._element)) {\n        callBack(sel);\n      }\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap modal.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$7 = 'modal';\n  const DATA_KEY$4 = 'bs.modal';\n  const EVENT_KEY$4 = `.${DATA_KEY$4}`;\n  const DATA_API_KEY$2 = '.data-api';\n  const ESCAPE_KEY$1 = 'Escape';\n  const EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\n  const EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\n  const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\n  const EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\n  const EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\n  const EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\n  const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\n  const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\n  const EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\n  const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\n  const CLASS_NAME_OPEN = 'modal-open';\n  const CLASS_NAME_FADE$3 = 'fade';\n  const CLASS_NAME_SHOW$4 = 'show';\n  const CLASS_NAME_STATIC = 'modal-static';\n  const OPEN_SELECTOR$1 = '.modal.show';\n  const SELECTOR_DIALOG = '.modal-dialog';\n  const SELECTOR_MODAL_BODY = '.modal-body';\n  const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\n  const Default$6 = {\n    backdrop: true,\n    focus: true,\n    keyboard: true\n  };\n  const DefaultType$6 = {\n    backdrop: '(boolean|string)',\n    focus: 'boolean',\n    keyboard: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Modal extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._isShown = false;\n      this._isTransitioning = false;\n      this._scrollBar = new ScrollBarHelper();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default$6;\n    }\n    static get DefaultType() {\n      return DefaultType$6;\n    }\n    static get NAME() {\n      return NAME$7;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown || this._isTransitioning) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._isTransitioning = true;\n      this._scrollBar.hide();\n      document.body.classList.add(CLASS_NAME_OPEN);\n      this._adjustDialog();\n      this._backdrop.show(() => this._showElement(relatedTarget));\n    }\n    hide() {\n      if (!this._isShown || this._isTransitioning) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = false;\n      this._isTransitioning = true;\n      this._focustrap.deactivate();\n      this._element.classList.remove(CLASS_NAME_SHOW$4);\n      this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n    }\n    dispose() {\n      EventHandler.off(window, EVENT_KEY$4);\n      EventHandler.off(this._dialog, EVENT_KEY$4);\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n    handleUpdate() {\n      this._adjustDialog();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      return new Backdrop({\n        isVisible: Boolean(this._config.backdrop),\n        // 'static' option will be translated to true, and booleans will keep their value,\n        isAnimated: this._isAnimated()\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _showElement(relatedTarget) {\n      // try to append dynamic modal\n      if (!document.body.contains(this._element)) {\n        document.body.append(this._element);\n      }\n      this._element.style.display = 'block';\n      this._element.removeAttribute('aria-hidden');\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.scrollTop = 0;\n      const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n      if (modalBody) {\n        modalBody.scrollTop = 0;\n      }\n      reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW$4);\n      const transitionComplete = () => {\n        if (this._config.focus) {\n          this._focustrap.activate();\n        }\n        this._isTransitioning = false;\n        EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n        if (event.key !== ESCAPE_KEY$1) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        this._triggerBackdropTransition();\n      });\n      EventHandler.on(window, EVENT_RESIZE$1, () => {\n        if (this._isShown && !this._isTransitioning) {\n          this._adjustDialog();\n        }\n      });\n      EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n        // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n        EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n          if (this._element !== event.target || this._element !== event2.target) {\n            return;\n          }\n          if (this._config.backdrop === 'static') {\n            this._triggerBackdropTransition();\n            return;\n          }\n          if (this._config.backdrop) {\n            this.hide();\n          }\n        });\n      });\n    }\n    _hideModal() {\n      this._element.style.display = 'none';\n      this._element.setAttribute('aria-hidden', true);\n      this._element.removeAttribute('aria-modal');\n      this._element.removeAttribute('role');\n      this._isTransitioning = false;\n      this._backdrop.hide(() => {\n        document.body.classList.remove(CLASS_NAME_OPEN);\n        this._resetAdjustments();\n        this._scrollBar.reset();\n        EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n      });\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_FADE$3);\n    }\n    _triggerBackdropTransition() {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const initialOverflowY = this._element.style.overflowY;\n      // return if the following background transition hasn't yet completed\n      if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n        return;\n      }\n      if (!isModalOverflowing) {\n        this._element.style.overflowY = 'hidden';\n      }\n      this._element.classList.add(CLASS_NAME_STATIC);\n      this._queueCallback(() => {\n        this._element.classList.remove(CLASS_NAME_STATIC);\n        this._queueCallback(() => {\n          this._element.style.overflowY = initialOverflowY;\n        }, this._dialog);\n      }, this._dialog);\n      this._element.focus();\n    }\n\n    /**\n     * The following methods are used to handle overflowing modals\n     */\n\n    _adjustDialog() {\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const scrollbarWidth = this._scrollBar.getWidth();\n      const isBodyOverflowing = scrollbarWidth > 0;\n      if (isBodyOverflowing && !isModalOverflowing) {\n        const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n      if (!isBodyOverflowing && isModalOverflowing) {\n        const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n    }\n    _resetAdjustments() {\n      this._element.style.paddingLeft = '';\n      this._element.style.paddingRight = '';\n    }\n\n    // Static\n    static jQueryInterface(config, relatedTarget) {\n      return this.each(function () {\n        const data = Modal.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](relatedTarget);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n      if (showEvent.defaultPrevented) {\n        // only register focus restorer if modal will actually get shown\n        return;\n      }\n      EventHandler.one(target, EVENT_HIDDEN$4, () => {\n        if (isVisible(this)) {\n          this.focus();\n        }\n      });\n    });\n\n    // avoid conflict when clicking modal toggler while another one is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n    if (alreadyOpen) {\n      Modal.getInstance(alreadyOpen).hide();\n    }\n    const data = Modal.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  enableDismissTrigger(Modal);\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Modal);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap offcanvas.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$6 = 'offcanvas';\n  const DATA_KEY$3 = 'bs.offcanvas';\n  const EVENT_KEY$3 = `.${DATA_KEY$3}`;\n  const DATA_API_KEY$1 = '.data-api';\n  const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\n  const ESCAPE_KEY = 'Escape';\n  const CLASS_NAME_SHOW$3 = 'show';\n  const CLASS_NAME_SHOWING$1 = 'showing';\n  const CLASS_NAME_HIDING = 'hiding';\n  const CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\n  const OPEN_SELECTOR = '.offcanvas.show';\n  const EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\n  const EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\n  const EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\n  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\n  const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\n  const EVENT_RESIZE = `resize${EVENT_KEY$3}`;\n  const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\n  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\n  const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\n  const Default$5 = {\n    backdrop: true,\n    keyboard: true,\n    scroll: false\n  };\n  const DefaultType$5 = {\n    backdrop: '(boolean|string)',\n    keyboard: 'boolean',\n    scroll: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Offcanvas extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isShown = false;\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default$5;\n    }\n    static get DefaultType() {\n      return DefaultType$5;\n    }\n    static get NAME() {\n      return NAME$6;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._backdrop.show();\n      if (!this._config.scroll) {\n        new ScrollBarHelper().hide();\n      }\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.classList.add(CLASS_NAME_SHOWING$1);\n      const completeCallBack = () => {\n        if (!this._config.scroll || this._config.backdrop) {\n          this._focustrap.activate();\n        }\n        this._element.classList.add(CLASS_NAME_SHOW$3);\n        this._element.classList.remove(CLASS_NAME_SHOWING$1);\n        EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(completeCallBack, this._element, true);\n    }\n    hide() {\n      if (!this._isShown) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._focustrap.deactivate();\n      this._element.blur();\n      this._isShown = false;\n      this._element.classList.add(CLASS_NAME_HIDING);\n      this._backdrop.hide();\n      const completeCallback = () => {\n        this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n        this._element.removeAttribute('aria-modal');\n        this._element.removeAttribute('role');\n        if (!this._config.scroll) {\n          new ScrollBarHelper().reset();\n        }\n        EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n      };\n      this._queueCallback(completeCallback, this._element, true);\n    }\n    dispose() {\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      const clickCallback = () => {\n        if (this._config.backdrop === 'static') {\n          EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n          return;\n        }\n        this.hide();\n      };\n\n      // 'static' option will be translated to true, and booleans will keep their value\n      const isVisible = Boolean(this._config.backdrop);\n      return new Backdrop({\n        className: CLASS_NAME_BACKDROP,\n        isVisible,\n        isAnimated: true,\n        rootElement: this._element.parentNode,\n        clickCallback: isVisible ? clickCallback : null\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n        if (event.key !== ESCAPE_KEY) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n      });\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Offcanvas.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (isDisabled(this)) {\n      return;\n    }\n    EventHandler.one(target, EVENT_HIDDEN$3, () => {\n      // focus on trigger when it is closed\n      if (isVisible(this)) {\n        this.focus();\n      }\n    });\n\n    // avoid conflict when clicking a toggler of an offcanvas, while another is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n    if (alreadyOpen && alreadyOpen !== target) {\n      Offcanvas.getInstance(alreadyOpen).hide();\n    }\n    const data = Offcanvas.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n    for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n      Offcanvas.getOrCreateInstance(selector).show();\n    }\n  });\n  EventHandler.on(window, EVENT_RESIZE, () => {\n    for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n      if (getComputedStyle(element).position !== 'fixed') {\n        Offcanvas.getOrCreateInstance(element).hide();\n      }\n    }\n  });\n  enableDismissTrigger(Offcanvas);\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Offcanvas);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/sanitizer.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  // js-docs-start allow-list\n  const ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n  const DefaultAllowlist = {\n    // Global attributes allowed on any supplied element below.\n    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n    a: ['target', 'href', 'title', 'rel'],\n    area: [],\n    b: [],\n    br: [],\n    col: [],\n    code: [],\n    dd: [],\n    div: [],\n    dl: [],\n    dt: [],\n    em: [],\n    hr: [],\n    h1: [],\n    h2: [],\n    h3: [],\n    h4: [],\n    h5: [],\n    h6: [],\n    i: [],\n    img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n    li: [],\n    ol: [],\n    p: [],\n    pre: [],\n    s: [],\n    small: [],\n    span: [],\n    sub: [],\n    sup: [],\n    strong: [],\n    u: [],\n    ul: []\n  };\n  // js-docs-end allow-list\n\n  const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n  /**\n   * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n   * contexts.\n   *\n   * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n   */\n  const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\n  const allowedAttribute = (attribute, allowedAttributeList) => {\n    const attributeName = attribute.nodeName.toLowerCase();\n    if (allowedAttributeList.includes(attributeName)) {\n      if (uriAttributes.has(attributeName)) {\n        return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n      }\n      return true;\n    }\n\n    // Check if a regular expression validates the attribute.\n    return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n  };\n  function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n    if (!unsafeHtml.length) {\n      return unsafeHtml;\n    }\n    if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n      return sanitizeFunction(unsafeHtml);\n    }\n    const domParser = new window.DOMParser();\n    const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n    const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n    for (const element of elements) {\n      const elementName = element.nodeName.toLowerCase();\n      if (!Object.keys(allowList).includes(elementName)) {\n        element.remove();\n        continue;\n      }\n      const attributeList = [].concat(...element.attributes);\n      const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n      for (const attribute of attributeList) {\n        if (!allowedAttribute(attribute, allowedAttributes)) {\n          element.removeAttribute(attribute.nodeName);\n        }\n      }\n    }\n    return createdDocument.body.innerHTML;\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/template-factory.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$5 = 'TemplateFactory';\n  const Default$4 = {\n    allowList: DefaultAllowlist,\n    content: {},\n    // { selector : text ,  selector2 : text2 , }\n    extraClass: '',\n    html: false,\n    sanitize: true,\n    sanitizeFn: null,\n    template: '<div></div>'\n  };\n  const DefaultType$4 = {\n    allowList: 'object',\n    content: 'object',\n    extraClass: '(string|function)',\n    html: 'boolean',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    template: 'string'\n  };\n  const DefaultContentType = {\n    entry: '(string|element|function|null)',\n    selector: '(string|element)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class TemplateFactory extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n    }\n\n    // Getters\n    static get Default() {\n      return Default$4;\n    }\n    static get DefaultType() {\n      return DefaultType$4;\n    }\n    static get NAME() {\n      return NAME$5;\n    }\n\n    // Public\n    getContent() {\n      return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n    }\n    hasContent() {\n      return this.getContent().length > 0;\n    }\n    changeContent(content) {\n      this._checkContent(content);\n      this._config.content = {\n        ...this._config.content,\n        ...content\n      };\n      return this;\n    }\n    toHtml() {\n      const templateWrapper = document.createElement('div');\n      templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n      for (const [selector, text] of Object.entries(this._config.content)) {\n        this._setContent(templateWrapper, text, selector);\n      }\n      const template = templateWrapper.children[0];\n      const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n      if (extraClass) {\n        template.classList.add(...extraClass.split(' '));\n      }\n      return template;\n    }\n\n    // Private\n    _typeCheckConfig(config) {\n      super._typeCheckConfig(config);\n      this._checkContent(config.content);\n    }\n    _checkContent(arg) {\n      for (const [selector, content] of Object.entries(arg)) {\n        super._typeCheckConfig({\n          selector,\n          entry: content\n        }, DefaultContentType);\n      }\n    }\n    _setContent(template, content, selector) {\n      const templateElement = SelectorEngine.findOne(selector, template);\n      if (!templateElement) {\n        return;\n      }\n      content = this._resolvePossibleFunction(content);\n      if (!content) {\n        templateElement.remove();\n        return;\n      }\n      if (isElement$1(content)) {\n        this._putElementInTemplate(getElement(content), templateElement);\n        return;\n      }\n      if (this._config.html) {\n        templateElement.innerHTML = this._maybeSanitize(content);\n        return;\n      }\n      templateElement.textContent = content;\n    }\n    _maybeSanitize(arg) {\n      return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n    }\n    _resolvePossibleFunction(arg) {\n      return execute(arg, [undefined, this]);\n    }\n    _putElementInTemplate(element, templateElement) {\n      if (this._config.html) {\n        templateElement.innerHTML = '';\n        templateElement.append(element);\n        return;\n      }\n      templateElement.textContent = element.textContent;\n    }\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tooltip.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$4 = 'tooltip';\n  const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\n  const CLASS_NAME_FADE$2 = 'fade';\n  const CLASS_NAME_MODAL = 'modal';\n  const CLASS_NAME_SHOW$2 = 'show';\n  const SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\n  const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\n  const EVENT_MODAL_HIDE = 'hide.bs.modal';\n  const TRIGGER_HOVER = 'hover';\n  const TRIGGER_FOCUS = 'focus';\n  const TRIGGER_CLICK = 'click';\n  const TRIGGER_MANUAL = 'manual';\n  const EVENT_HIDE$2 = 'hide';\n  const EVENT_HIDDEN$2 = 'hidden';\n  const EVENT_SHOW$2 = 'show';\n  const EVENT_SHOWN$2 = 'shown';\n  const EVENT_INSERTED = 'inserted';\n  const EVENT_CLICK$1 = 'click';\n  const EVENT_FOCUSIN$1 = 'focusin';\n  const EVENT_FOCUSOUT$1 = 'focusout';\n  const EVENT_MOUSEENTER = 'mouseenter';\n  const EVENT_MOUSELEAVE = 'mouseleave';\n  const AttachmentMap = {\n    AUTO: 'auto',\n    TOP: 'top',\n    RIGHT: isRTL() ? 'left' : 'right',\n    BOTTOM: 'bottom',\n    LEFT: isRTL() ? 'right' : 'left'\n  };\n  const Default$3 = {\n    allowList: DefaultAllowlist,\n    animation: true,\n    boundary: 'clippingParents',\n    container: false,\n    customClass: '',\n    delay: 0,\n    fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n    html: false,\n    offset: [0, 6],\n    placement: 'top',\n    popperConfig: null,\n    sanitize: true,\n    sanitizeFn: null,\n    selector: false,\n    template: '<div class=\"tooltip\" role=\"tooltip\">' + '<div class=\"tooltip-arrow\"></div>' + '<div class=\"tooltip-inner\"></div>' + '</div>',\n    title: '',\n    trigger: 'hover focus'\n  };\n  const DefaultType$3 = {\n    allowList: 'object',\n    animation: 'boolean',\n    boundary: '(string|element)',\n    container: '(string|element|boolean)',\n    customClass: '(string|function)',\n    delay: '(number|object)',\n    fallbackPlacements: 'array',\n    html: 'boolean',\n    offset: '(array|string|function)',\n    placement: '(string|function)',\n    popperConfig: '(null|object|function)',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    selector: '(string|boolean)',\n    template: 'string',\n    title: '(string|element|function)',\n    trigger: 'string'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Tooltip extends BaseComponent {\n    constructor(element, config) {\n      if (typeof Popper === 'undefined') {\n        throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org/docs/v2/)');\n      }\n      super(element, config);\n\n      // Private\n      this._isEnabled = true;\n      this._timeout = 0;\n      this._isHovered = null;\n      this._activeTrigger = {};\n      this._popper = null;\n      this._templateFactory = null;\n      this._newContent = null;\n\n      // Protected\n      this.tip = null;\n      this._setListeners();\n      if (!this._config.selector) {\n        this._fixTitle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default$3;\n    }\n    static get DefaultType() {\n      return DefaultType$3;\n    }\n    static get NAME() {\n      return NAME$4;\n    }\n\n    // Public\n    enable() {\n      this._isEnabled = true;\n    }\n    disable() {\n      this._isEnabled = false;\n    }\n    toggleEnabled() {\n      this._isEnabled = !this._isEnabled;\n    }\n    toggle() {\n      if (!this._isEnabled) {\n        return;\n      }\n      if (this._isShown()) {\n        this._leave();\n        return;\n      }\n      this._enter();\n    }\n    dispose() {\n      clearTimeout(this._timeout);\n      EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n      if (this._element.getAttribute('data-bs-original-title')) {\n        this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n      }\n      this._disposePopper();\n      super.dispose();\n    }\n    show() {\n      if (this._element.style.display === 'none') {\n        throw new Error('Please use show on visible elements');\n      }\n      if (!(this._isWithContent() && this._isEnabled)) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n      const shadowRoot = findShadowRoot(this._element);\n      const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n      if (showEvent.defaultPrevented || !isInTheDom) {\n        return;\n      }\n\n      // TODO: v6 remove this or make it optional\n      this._disposePopper();\n      const tip = this._getTipElement();\n      this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n      const {\n        container\n      } = this._config;\n      if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n        container.append(tip);\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n      }\n      this._popper = this._createPopper(tip);\n      tip.classList.add(CLASS_NAME_SHOW$2);\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', noop);\n        }\n      }\n      const complete = () => {\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n        if (this._isHovered === false) {\n          this._leave();\n        }\n        this._isHovered = false;\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    hide() {\n      if (!this._isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const tip = this._getTipElement();\n      tip.classList.remove(CLASS_NAME_SHOW$2);\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', noop);\n        }\n      }\n      this._activeTrigger[TRIGGER_CLICK] = false;\n      this._activeTrigger[TRIGGER_FOCUS] = false;\n      this._activeTrigger[TRIGGER_HOVER] = false;\n      this._isHovered = null; // it is a trick to support manual triggering\n\n      const complete = () => {\n        if (this._isWithActiveTrigger()) {\n          return;\n        }\n        if (!this._isHovered) {\n          this._disposePopper();\n        }\n        this._element.removeAttribute('aria-describedby');\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    update() {\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Protected\n    _isWithContent() {\n      return Boolean(this._getTitle());\n    }\n    _getTipElement() {\n      if (!this.tip) {\n        this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n      }\n      return this.tip;\n    }\n    _createTipElement(content) {\n      const tip = this._getTemplateFactory(content).toHtml();\n\n      // TODO: remove this check in v6\n      if (!tip) {\n        return null;\n      }\n      tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n      // TODO: v6 the following can be achieved with CSS only\n      tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n      const tipId = getUID(this.constructor.NAME).toString();\n      tip.setAttribute('id', tipId);\n      if (this._isAnimated()) {\n        tip.classList.add(CLASS_NAME_FADE$2);\n      }\n      return tip;\n    }\n    setContent(content) {\n      this._newContent = content;\n      if (this._isShown()) {\n        this._disposePopper();\n        this.show();\n      }\n    }\n    _getTemplateFactory(content) {\n      if (this._templateFactory) {\n        this._templateFactory.changeContent(content);\n      } else {\n        this._templateFactory = new TemplateFactory({\n          ...this._config,\n          // the `content` var has to be after `this._config`\n          // to override config.content in case of popover\n          content,\n          extraClass: this._resolvePossibleFunction(this._config.customClass)\n        });\n      }\n      return this._templateFactory;\n    }\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n      };\n    }\n    _getTitle() {\n      return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n    }\n\n    // Private\n    _initializeOnDelegatedTarget(event) {\n      return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n    }\n    _isAnimated() {\n      return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n    }\n    _isShown() {\n      return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n    }\n    _createPopper(tip) {\n      const placement = execute(this._config.placement, [this, tip, this._element]);\n      const attachment = AttachmentMap[placement.toUpperCase()];\n      return createPopper(this._element, tip, this._getPopperConfig(attachment));\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _resolvePossibleFunction(arg) {\n      return execute(arg, [this._element, this._element]);\n    }\n    _getPopperConfig(attachment) {\n      const defaultBsPopperConfig = {\n        placement: attachment,\n        modifiers: [{\n          name: 'flip',\n          options: {\n            fallbackPlacements: this._config.fallbackPlacements\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }, {\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'arrow',\n          options: {\n            element: `.${this.constructor.NAME}-arrow`\n          }\n        }, {\n          name: 'preSetPlacement',\n          enabled: true,\n          phase: 'beforeMain',\n          fn: data => {\n            // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n            // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n            this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n          }\n        }]\n      };\n      return {\n        ...defaultBsPopperConfig,\n        ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n      };\n    }\n    _setListeners() {\n      const triggers = this._config.trigger.split(' ');\n      for (const trigger of triggers) {\n        if (trigger === 'click') {\n          EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);\n            context.toggle();\n          });\n        } else if (trigger !== TRIGGER_MANUAL) {\n          const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n          const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n          EventHandler.on(this._element, eventIn, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n            context._enter();\n          });\n          EventHandler.on(this._element, eventOut, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n            context._leave();\n          });\n        }\n      }\n      this._hideModalHandler = () => {\n        if (this._element) {\n          this.hide();\n        }\n      };\n      EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n    }\n    _fixTitle() {\n      const title = this._element.getAttribute('title');\n      if (!title) {\n        return;\n      }\n      if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n        this._element.setAttribute('aria-label', title);\n      }\n      this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n      this._element.removeAttribute('title');\n    }\n    _enter() {\n      if (this._isShown() || this._isHovered) {\n        this._isHovered = true;\n        return;\n      }\n      this._isHovered = true;\n      this._setTimeout(() => {\n        if (this._isHovered) {\n          this.show();\n        }\n      }, this._config.delay.show);\n    }\n    _leave() {\n      if (this._isWithActiveTrigger()) {\n        return;\n      }\n      this._isHovered = false;\n      this._setTimeout(() => {\n        if (!this._isHovered) {\n          this.hide();\n        }\n      }, this._config.delay.hide);\n    }\n    _setTimeout(handler, timeout) {\n      clearTimeout(this._timeout);\n      this._timeout = setTimeout(handler, timeout);\n    }\n    _isWithActiveTrigger() {\n      return Object.values(this._activeTrigger).includes(true);\n    }\n    _getConfig(config) {\n      const dataAttributes = Manipulator.getDataAttributes(this._element);\n      for (const dataAttribute of Object.keys(dataAttributes)) {\n        if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n          delete dataAttributes[dataAttribute];\n        }\n      }\n      config = {\n        ...dataAttributes,\n        ...(typeof config === 'object' && config ? config : {})\n      };\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      config.container = config.container === false ? document.body : getElement(config.container);\n      if (typeof config.delay === 'number') {\n        config.delay = {\n          show: config.delay,\n          hide: config.delay\n        };\n      }\n      if (typeof config.title === 'number') {\n        config.title = config.title.toString();\n      }\n      if (typeof config.content === 'number') {\n        config.content = config.content.toString();\n      }\n      return config;\n    }\n    _getDelegateConfig() {\n      const config = {};\n      for (const [key, value] of Object.entries(this._config)) {\n        if (this.constructor.Default[key] !== value) {\n          config[key] = value;\n        }\n      }\n      config.selector = false;\n      config.trigger = 'manual';\n\n      // In the future can be replaced with:\n      // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n      // `Object.fromEntries(keysWithDifferentValues)`\n      return config;\n    }\n    _disposePopper() {\n      if (this._popper) {\n        this._popper.destroy();\n        this._popper = null;\n      }\n      if (this.tip) {\n        this.tip.remove();\n        this.tip = null;\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tooltip.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Tooltip);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap popover.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$3 = 'popover';\n  const SELECTOR_TITLE = '.popover-header';\n  const SELECTOR_CONTENT = '.popover-body';\n  const Default$2 = {\n    ...Tooltip.Default,\n    content: '',\n    offset: [0, 8],\n    placement: 'right',\n    template: '<div class=\"popover\" role=\"tooltip\">' + '<div class=\"popover-arrow\"></div>' + '<h3 class=\"popover-header\"></h3>' + '<div class=\"popover-body\"></div>' + '</div>',\n    trigger: 'click'\n  };\n  const DefaultType$2 = {\n    ...Tooltip.DefaultType,\n    content: '(null|string|element|function)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Popover extends Tooltip {\n    // Getters\n    static get Default() {\n      return Default$2;\n    }\n    static get DefaultType() {\n      return DefaultType$2;\n    }\n    static get NAME() {\n      return NAME$3;\n    }\n\n    // Overrides\n    _isWithContent() {\n      return this._getTitle() || this._getContent();\n    }\n\n    // Private\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TITLE]: this._getTitle(),\n        [SELECTOR_CONTENT]: this._getContent()\n      };\n    }\n    _getContent() {\n      return this._resolvePossibleFunction(this._config.content);\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Popover.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Popover);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap scrollspy.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$2 = 'scrollspy';\n  const DATA_KEY$2 = 'bs.scrollspy';\n  const EVENT_KEY$2 = `.${DATA_KEY$2}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\n  const EVENT_CLICK = `click${EVENT_KEY$2}`;\n  const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\n  const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\n  const CLASS_NAME_ACTIVE$1 = 'active';\n  const SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\n  const SELECTOR_TARGET_LINKS = '[href]';\n  const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\n  const SELECTOR_NAV_LINKS = '.nav-link';\n  const SELECTOR_NAV_ITEMS = '.nav-item';\n  const SELECTOR_LIST_ITEMS = '.list-group-item';\n  const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\n  const SELECTOR_DROPDOWN = '.dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\n  const Default$1 = {\n    offset: null,\n    // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: '0px 0px -25%',\n    smoothScroll: false,\n    target: null,\n    threshold: [0.1, 0.5, 1]\n  };\n  const DefaultType$1 = {\n    offset: '(number|null)',\n    // TODO v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: 'string',\n    smoothScroll: 'boolean',\n    target: 'element',\n    threshold: 'array'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class ScrollSpy extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n\n      // this._element is the observablesContainer and config.target the menu links wrapper\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n      this._activeTarget = null;\n      this._observer = null;\n      this._previousScrollData = {\n        visibleEntryTop: 0,\n        parentScrollTop: 0\n      };\n      this.refresh(); // initialize\n    }\n\n    // Getters\n    static get Default() {\n      return Default$1;\n    }\n    static get DefaultType() {\n      return DefaultType$1;\n    }\n    static get NAME() {\n      return NAME$2;\n    }\n\n    // Public\n    refresh() {\n      this._initializeTargetsAndObservables();\n      this._maybeEnableSmoothScroll();\n      if (this._observer) {\n        this._observer.disconnect();\n      } else {\n        this._observer = this._getNewObserver();\n      }\n      for (const section of this._observableSections.values()) {\n        this._observer.observe(section);\n      }\n    }\n    dispose() {\n      this._observer.disconnect();\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n      config.target = getElement(config.target) || document.body;\n\n      // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n      config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n      if (typeof config.threshold === 'string') {\n        config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n      }\n      return config;\n    }\n    _maybeEnableSmoothScroll() {\n      if (!this._config.smoothScroll) {\n        return;\n      }\n\n      // unregister any previous listeners\n      EventHandler.off(this._config.target, EVENT_CLICK);\n      EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n        const observableSection = this._observableSections.get(event.target.hash);\n        if (observableSection) {\n          event.preventDefault();\n          const root = this._rootElement || window;\n          const height = observableSection.offsetTop - this._element.offsetTop;\n          if (root.scrollTo) {\n            root.scrollTo({\n              top: height,\n              behavior: 'smooth'\n            });\n            return;\n          }\n\n          // Chrome 60 doesn't support `scrollTo`\n          root.scrollTop = height;\n        }\n      });\n    }\n    _getNewObserver() {\n      const options = {\n        root: this._rootElement,\n        threshold: this._config.threshold,\n        rootMargin: this._config.rootMargin\n      };\n      return new IntersectionObserver(entries => this._observerCallback(entries), options);\n    }\n\n    // The logic of selection\n    _observerCallback(entries) {\n      const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n      const activate = entry => {\n        this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n        this._process(targetElement(entry));\n      };\n      const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n      const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n      this._previousScrollData.parentScrollTop = parentScrollTop;\n      for (const entry of entries) {\n        if (!entry.isIntersecting) {\n          this._activeTarget = null;\n          this._clearActiveClass(targetElement(entry));\n          continue;\n        }\n        const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n        // if we are scrolling down, pick the bigger offsetTop\n        if (userScrollsDown && entryIsLowerThanPrevious) {\n          activate(entry);\n          // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n          if (!parentScrollTop) {\n            return;\n          }\n          continue;\n        }\n\n        // if we are scrolling up, pick the smallest offsetTop\n        if (!userScrollsDown && !entryIsLowerThanPrevious) {\n          activate(entry);\n        }\n      }\n    }\n    _initializeTargetsAndObservables() {\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n      for (const anchor of targetLinks) {\n        // ensure that the anchor has an id and is not disabled\n        if (!anchor.hash || isDisabled(anchor)) {\n          continue;\n        }\n        const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n        // ensure that the observableSection exists & is visible\n        if (isVisible(observableSection)) {\n          this._targetLinks.set(decodeURI(anchor.hash), anchor);\n          this._observableSections.set(anchor.hash, observableSection);\n        }\n      }\n    }\n    _process(target) {\n      if (this._activeTarget === target) {\n        return;\n      }\n      this._clearActiveClass(this._config.target);\n      this._activeTarget = target;\n      target.classList.add(CLASS_NAME_ACTIVE$1);\n      this._activateParents(target);\n      EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n        relatedTarget: target\n      });\n    }\n    _activateParents(target) {\n      // Activate dropdown parents\n      if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n        SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n        return;\n      }\n      for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n        // Set triggered links parents as active\n        // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n        for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {\n          item.classList.add(CLASS_NAME_ACTIVE$1);\n        }\n      }\n    }\n    _clearActiveClass(parent) {\n      parent.classList.remove(CLASS_NAME_ACTIVE$1);\n      const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent);\n      for (const node of activeNodes) {\n        node.classList.remove(CLASS_NAME_ACTIVE$1);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = ScrollSpy.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => {\n    for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {\n      ScrollSpy.getOrCreateInstance(spy);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(ScrollSpy);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tab.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME$1 = 'tab';\n  const DATA_KEY$1 = 'bs.tab';\n  const EVENT_KEY$1 = `.${DATA_KEY$1}`;\n  const EVENT_HIDE$1 = `hide${EVENT_KEY$1}`;\n  const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$1}`;\n  const EVENT_SHOW$1 = `show${EVENT_KEY$1}`;\n  const EVENT_SHOWN$1 = `shown${EVENT_KEY$1}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY$1}`;\n  const EVENT_KEYDOWN = `keydown${EVENT_KEY$1}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY$1}`;\n  const ARROW_LEFT_KEY = 'ArrowLeft';\n  const ARROW_RIGHT_KEY = 'ArrowRight';\n  const ARROW_UP_KEY = 'ArrowUp';\n  const ARROW_DOWN_KEY = 'ArrowDown';\n  const HOME_KEY = 'Home';\n  const END_KEY = 'End';\n  const CLASS_NAME_ACTIVE = 'active';\n  const CLASS_NAME_FADE$1 = 'fade';\n  const CLASS_NAME_SHOW$1 = 'show';\n  const CLASS_DROPDOWN = 'dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n  const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';\n  const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;\n  const SELECTOR_TAB_PANEL = '.list-group, .nav, [role=\"tablist\"]';\n  const SELECTOR_OUTER = '.nav-item, .list-group-item';\n  const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role=\"tab\"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"tab\"], [data-bs-toggle=\"pill\"], [data-bs-toggle=\"list\"]'; // TODO: could only be `tab` in v6\n  const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle=\"tab\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"pill\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"list\"]`;\n\n  /**\n   * Class definition\n   */\n\n  class Tab extends BaseComponent {\n    constructor(element) {\n      super(element);\n      this._parent = this._element.closest(SELECTOR_TAB_PANEL);\n      if (!this._parent) {\n        return;\n        // TODO: should throw exception in v6\n        // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)\n      }\n\n      // Set up initial aria attributes\n      this._setInitialAttributes(this._parent, this._getChildren());\n      EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));\n    }\n\n    // Getters\n    static get NAME() {\n      return NAME$1;\n    }\n\n    // Public\n    show() {\n      // Shows this elem and deactivate the active sibling if exists\n      const innerElem = this._element;\n      if (this._elemIsActive(innerElem)) {\n        return;\n      }\n\n      // Search for active tab on same parent to deactivate it\n      const active = this._getActiveElem();\n      const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, {\n        relatedTarget: innerElem\n      }) : null;\n      const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, {\n        relatedTarget: active\n      });\n      if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) {\n        return;\n      }\n      this._deactivate(active, innerElem);\n      this._activate(innerElem, active);\n    }\n\n    // Private\n    _activate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.add(CLASS_NAME_ACTIVE);\n      this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.add(CLASS_NAME_SHOW$1);\n          return;\n        }\n        element.removeAttribute('tabindex');\n        element.setAttribute('aria-selected', true);\n        this._toggleDropDown(element, true);\n        EventHandler.trigger(element, EVENT_SHOWN$1, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1));\n    }\n    _deactivate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.remove(CLASS_NAME_ACTIVE);\n      element.blur();\n      this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.remove(CLASS_NAME_SHOW$1);\n          return;\n        }\n        element.setAttribute('aria-selected', false);\n        element.setAttribute('tabindex', '-1');\n        this._toggleDropDown(element, false);\n        EventHandler.trigger(element, EVENT_HIDDEN$1, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1));\n    }\n    _keydown(event) {\n      if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) {\n        return;\n      }\n      event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page\n      event.preventDefault();\n      const children = this._getChildren().filter(element => !isDisabled(element));\n      let nextActiveElement;\n      if ([HOME_KEY, END_KEY].includes(event.key)) {\n        nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1];\n      } else {\n        const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key);\n        nextActiveElement = getNextActiveElement(children, event.target, isNext, true);\n      }\n      if (nextActiveElement) {\n        nextActiveElement.focus({\n          preventScroll: true\n        });\n        Tab.getOrCreateInstance(nextActiveElement).show();\n      }\n    }\n    _getChildren() {\n      // collection of inner elements\n      return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent);\n    }\n    _getActiveElem() {\n      return this._getChildren().find(child => this._elemIsActive(child)) || null;\n    }\n    _setInitialAttributes(parent, children) {\n      this._setAttributeIfNotExists(parent, 'role', 'tablist');\n      for (const child of children) {\n        this._setInitialAttributesOnChild(child);\n      }\n    }\n    _setInitialAttributesOnChild(child) {\n      child = this._getInnerElement(child);\n      const isActive = this._elemIsActive(child);\n      const outerElem = this._getOuterElement(child);\n      child.setAttribute('aria-selected', isActive);\n      if (outerElem !== child) {\n        this._setAttributeIfNotExists(outerElem, 'role', 'presentation');\n      }\n      if (!isActive) {\n        child.setAttribute('tabindex', '-1');\n      }\n      this._setAttributeIfNotExists(child, 'role', 'tab');\n\n      // set attributes to the related panel too\n      this._setInitialAttributesOnTargetPanel(child);\n    }\n    _setInitialAttributesOnTargetPanel(child) {\n      const target = SelectorEngine.getElementFromSelector(child);\n      if (!target) {\n        return;\n      }\n      this._setAttributeIfNotExists(target, 'role', 'tabpanel');\n      if (child.id) {\n        this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`);\n      }\n    }\n    _toggleDropDown(element, open) {\n      const outerElem = this._getOuterElement(element);\n      if (!outerElem.classList.contains(CLASS_DROPDOWN)) {\n        return;\n      }\n      const toggle = (selector, className) => {\n        const element = SelectorEngine.findOne(selector, outerElem);\n        if (element) {\n          element.classList.toggle(className, open);\n        }\n      };\n      toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE);\n      toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW$1);\n      outerElem.setAttribute('aria-expanded', open);\n    }\n    _setAttributeIfNotExists(element, attribute, value) {\n      if (!element.hasAttribute(attribute)) {\n        element.setAttribute(attribute, value);\n      }\n    }\n    _elemIsActive(elem) {\n      return elem.classList.contains(CLASS_NAME_ACTIVE);\n    }\n\n    // Try to get the inner element (usually the .nav-link)\n    _getInnerElement(elem) {\n      return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem);\n    }\n\n    // Try to get the outer element (usually the .nav-item)\n    _getOuterElement(elem) {\n      return elem.closest(SELECTOR_OUTER) || elem;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tab.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (isDisabled(this)) {\n      return;\n    }\n    Tab.getOrCreateInstance(this).show();\n  });\n\n  /**\n   * Initialize on focus\n   */\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {\n      Tab.getOrCreateInstance(element);\n    }\n  });\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Tab);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap toast.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'toast';\n  const DATA_KEY = 'bs.toast';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`;\n  const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`;\n  const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;\n  const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_SHOWING = 'showing';\n  const DefaultType = {\n    animation: 'boolean',\n    autohide: 'boolean',\n    delay: 'number'\n  };\n  const Default = {\n    animation: true,\n    autohide: true,\n    delay: 5000\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Toast extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._timeout = null;\n      this._hasMouseInteraction = false;\n      this._hasKeyboardInteraction = false;\n      this._setListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show() {\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._clearTimeout();\n      if (this._config.animation) {\n        this._element.classList.add(CLASS_NAME_FADE);\n      }\n      const complete = () => {\n        this._element.classList.remove(CLASS_NAME_SHOWING);\n        EventHandler.trigger(this._element, EVENT_SHOWN);\n        this._maybeScheduleHide();\n      };\n      this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated\n      reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    hide() {\n      if (!this.isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const complete = () => {\n        this._element.classList.add(CLASS_NAME_HIDE); // @deprecated\n        this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW);\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._element.classList.add(CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    dispose() {\n      this._clearTimeout();\n      if (this.isShown()) {\n        this._element.classList.remove(CLASS_NAME_SHOW);\n      }\n      super.dispose();\n    }\n    isShown() {\n      return this._element.classList.contains(CLASS_NAME_SHOW);\n    }\n\n    // Private\n    _maybeScheduleHide() {\n      if (!this._config.autohide) {\n        return;\n      }\n      if (this._hasMouseInteraction || this._hasKeyboardInteraction) {\n        return;\n      }\n      this._timeout = setTimeout(() => {\n        this.hide();\n      }, this._config.delay);\n    }\n    _onInteraction(event, isInteracting) {\n      switch (event.type) {\n        case 'mouseover':\n        case 'mouseout':\n          {\n            this._hasMouseInteraction = isInteracting;\n            break;\n          }\n        case 'focusin':\n        case 'focusout':\n          {\n            this._hasKeyboardInteraction = isInteracting;\n            break;\n          }\n      }\n      if (isInteracting) {\n        this._clearTimeout();\n        return;\n      }\n      const nextElement = event.relatedTarget;\n      if (this._element === nextElement || this._element.contains(nextElement)) {\n        return;\n      }\n      this._maybeScheduleHide();\n    }\n    _setListeners() {\n      EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false));\n      EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false));\n    }\n    _clearTimeout() {\n      clearTimeout(this._timeout);\n      this._timeout = null;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Toast.getOrCreateInstance(this, config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config](this);\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  enableDismissTrigger(Toast);\n\n  /**\n   * jQuery\n   */\n\n  defineJQueryPlugin(Toast);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap index.umd.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const index_umd = {\n    Alert,\n    Button,\n    Carousel,\n    Collapse,\n    Dropdown,\n    Modal,\n    Offcanvas,\n    Popover,\n    ScrollSpy,\n    Tab,\n    Toast,\n    Tooltip\n  };\n\n  return index_umd;\n\n}));\n//# sourceMappingURL=bootstrap.bundle.js.map\n"
  },
  {
    "path": "src/static/scripts/bootstrap.css",
    "content": "@charset \"UTF-8\";\n/*!\n * Bootstrap  v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n  --bs-blue: #0d6efd;\n  --bs-indigo: #6610f2;\n  --bs-purple: #6f42c1;\n  --bs-pink: #d63384;\n  --bs-red: #dc3545;\n  --bs-orange: #fd7e14;\n  --bs-yellow: #ffc107;\n  --bs-green: #198754;\n  --bs-teal: #20c997;\n  --bs-cyan: #0dcaf0;\n  --bs-black: #000;\n  --bs-white: #fff;\n  --bs-gray: #6c757d;\n  --bs-gray-dark: #343a40;\n  --bs-gray-100: #f8f9fa;\n  --bs-gray-200: #e9ecef;\n  --bs-gray-300: #dee2e6;\n  --bs-gray-400: #ced4da;\n  --bs-gray-500: #adb5bd;\n  --bs-gray-600: #6c757d;\n  --bs-gray-700: #495057;\n  --bs-gray-800: #343a40;\n  --bs-gray-900: #212529;\n  --bs-primary: #0d6efd;\n  --bs-secondary: #6c757d;\n  --bs-success: #198754;\n  --bs-info: #0dcaf0;\n  --bs-warning: #ffc107;\n  --bs-danger: #dc3545;\n  --bs-light: #f8f9fa;\n  --bs-dark: #212529;\n  --bs-primary-rgb: 13, 110, 253;\n  --bs-secondary-rgb: 108, 117, 125;\n  --bs-success-rgb: 25, 135, 84;\n  --bs-info-rgb: 13, 202, 240;\n  --bs-warning-rgb: 255, 193, 7;\n  --bs-danger-rgb: 220, 53, 69;\n  --bs-light-rgb: 248, 249, 250;\n  --bs-dark-rgb: 33, 37, 41;\n  --bs-primary-text-emphasis: #052c65;\n  --bs-secondary-text-emphasis: #2b2f32;\n  --bs-success-text-emphasis: #0a3622;\n  --bs-info-text-emphasis: #055160;\n  --bs-warning-text-emphasis: #664d03;\n  --bs-danger-text-emphasis: #58151c;\n  --bs-light-text-emphasis: #495057;\n  --bs-dark-text-emphasis: #495057;\n  --bs-primary-bg-subtle: #cfe2ff;\n  --bs-secondary-bg-subtle: #e2e3e5;\n  --bs-success-bg-subtle: #d1e7dd;\n  --bs-info-bg-subtle: #cff4fc;\n  --bs-warning-bg-subtle: #fff3cd;\n  --bs-danger-bg-subtle: #f8d7da;\n  --bs-light-bg-subtle: #fcfcfd;\n  --bs-dark-bg-subtle: #ced4da;\n  --bs-primary-border-subtle: #9ec5fe;\n  --bs-secondary-border-subtle: #c4c8cb;\n  --bs-success-border-subtle: #a3cfbb;\n  --bs-info-border-subtle: #9eeaf9;\n  --bs-warning-border-subtle: #ffe69c;\n  --bs-danger-border-subtle: #f1aeb5;\n  --bs-light-border-subtle: #e9ecef;\n  --bs-dark-border-subtle: #adb5bd;\n  --bs-white-rgb: 255, 255, 255;\n  --bs-black-rgb: 0, 0, 0;\n  --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n  --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n  --bs-body-font-family: var(--bs-font-sans-serif);\n  --bs-body-font-size: 1rem;\n  --bs-body-font-weight: 400;\n  --bs-body-line-height: 1.5;\n  --bs-body-color: #212529;\n  --bs-body-color-rgb: 33, 37, 41;\n  --bs-body-bg: #fff;\n  --bs-body-bg-rgb: 255, 255, 255;\n  --bs-emphasis-color: #000;\n  --bs-emphasis-color-rgb: 0, 0, 0;\n  --bs-secondary-color: rgba(33, 37, 41, 0.75);\n  --bs-secondary-color-rgb: 33, 37, 41;\n  --bs-secondary-bg: #e9ecef;\n  --bs-secondary-bg-rgb: 233, 236, 239;\n  --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n  --bs-tertiary-color-rgb: 33, 37, 41;\n  --bs-tertiary-bg: #f8f9fa;\n  --bs-tertiary-bg-rgb: 248, 249, 250;\n  --bs-heading-color: inherit;\n  --bs-link-color: #0d6efd;\n  --bs-link-color-rgb: 13, 110, 253;\n  --bs-link-decoration: underline;\n  --bs-link-hover-color: #0a58ca;\n  --bs-link-hover-color-rgb: 10, 88, 202;\n  --bs-code-color: #d63384;\n  --bs-highlight-color: #212529;\n  --bs-highlight-bg: #fff3cd;\n  --bs-border-width: 1px;\n  --bs-border-style: solid;\n  --bs-border-color: #dee2e6;\n  --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n  --bs-border-radius: 0.375rem;\n  --bs-border-radius-sm: 0.25rem;\n  --bs-border-radius-lg: 0.5rem;\n  --bs-border-radius-xl: 1rem;\n  --bs-border-radius-xxl: 2rem;\n  --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n  --bs-border-radius-pill: 50rem;\n  --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n  --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n  --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n  --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n  --bs-focus-ring-width: 0.25rem;\n  --bs-focus-ring-opacity: 0.25;\n  --bs-focus-ring-color: rgba(13, 110, 253, 0.25);\n  --bs-form-valid-color: #198754;\n  --bs-form-valid-border-color: #198754;\n  --bs-form-invalid-color: #dc3545;\n  --bs-form-invalid-border-color: #dc3545;\n}\n\n[data-bs-theme=dark] {\n  color-scheme: dark;\n  --bs-body-color: #dee2e6;\n  --bs-body-color-rgb: 222, 226, 230;\n  --bs-body-bg: #212529;\n  --bs-body-bg-rgb: 33, 37, 41;\n  --bs-emphasis-color: #fff;\n  --bs-emphasis-color-rgb: 255, 255, 255;\n  --bs-secondary-color: rgba(222, 226, 230, 0.75);\n  --bs-secondary-color-rgb: 222, 226, 230;\n  --bs-secondary-bg: #343a40;\n  --bs-secondary-bg-rgb: 52, 58, 64;\n  --bs-tertiary-color: rgba(222, 226, 230, 0.5);\n  --bs-tertiary-color-rgb: 222, 226, 230;\n  --bs-tertiary-bg: #2b3035;\n  --bs-tertiary-bg-rgb: 43, 48, 53;\n  --bs-primary-text-emphasis: #6ea8fe;\n  --bs-secondary-text-emphasis: #a7acb1;\n  --bs-success-text-emphasis: #75b798;\n  --bs-info-text-emphasis: #6edff6;\n  --bs-warning-text-emphasis: #ffda6a;\n  --bs-danger-text-emphasis: #ea868f;\n  --bs-light-text-emphasis: #f8f9fa;\n  --bs-dark-text-emphasis: #dee2e6;\n  --bs-primary-bg-subtle: #031633;\n  --bs-secondary-bg-subtle: #161719;\n  --bs-success-bg-subtle: #051b11;\n  --bs-info-bg-subtle: #032830;\n  --bs-warning-bg-subtle: #332701;\n  --bs-danger-bg-subtle: #2c0b0e;\n  --bs-light-bg-subtle: #343a40;\n  --bs-dark-bg-subtle: #1a1d20;\n  --bs-primary-border-subtle: #084298;\n  --bs-secondary-border-subtle: #41464b;\n  --bs-success-border-subtle: #0f5132;\n  --bs-info-border-subtle: #087990;\n  --bs-warning-border-subtle: #997404;\n  --bs-danger-border-subtle: #842029;\n  --bs-light-border-subtle: #495057;\n  --bs-dark-border-subtle: #343a40;\n  --bs-heading-color: inherit;\n  --bs-link-color: #6ea8fe;\n  --bs-link-hover-color: #8bb9fe;\n  --bs-link-color-rgb: 110, 168, 254;\n  --bs-link-hover-color-rgb: 139, 185, 254;\n  --bs-code-color: #e685b5;\n  --bs-highlight-color: #dee2e6;\n  --bs-highlight-bg: #664d03;\n  --bs-border-color: #495057;\n  --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n  --bs-form-valid-color: #75b798;\n  --bs-form-valid-border-color: #75b798;\n  --bs-form-invalid-color: #ea868f;\n  --bs-form-invalid-border-color: #ea868f;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  :root {\n    scroll-behavior: smooth;\n  }\n}\n\nbody {\n  margin: 0;\n  font-family: var(--bs-body-font-family);\n  font-size: var(--bs-body-font-size);\n  font-weight: var(--bs-body-font-weight);\n  line-height: var(--bs-body-line-height);\n  color: var(--bs-body-color);\n  text-align: var(--bs-body-text-align);\n  background-color: var(--bs-body-bg);\n  -webkit-text-size-adjust: 100%;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n  margin: 1rem 0;\n  color: inherit;\n  border: 0;\n  border-top: var(--bs-border-width) solid;\n  opacity: 0.25;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n  margin-top: 0;\n  margin-bottom: 0.5rem;\n  font-weight: 500;\n  line-height: 1.2;\n  color: var(--bs-heading-color);\n}\n\nh1, .h1 {\n  font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n  h1, .h1 {\n    font-size: 2.5rem;\n  }\n}\n\nh2, .h2 {\n  font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n  h2, .h2 {\n    font-size: 2rem;\n  }\n}\n\nh3, .h3 {\n  font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n  h3, .h3 {\n    font-size: 1.75rem;\n  }\n}\n\nh4, .h4 {\n  font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n  h4, .h4 {\n    font-size: 1.5rem;\n  }\n}\n\nh5, .h5 {\n  font-size: 1.25rem;\n}\n\nh6, .h6 {\n  font-size: 1rem;\n}\n\np {\n  margin-top: 0;\n  margin-bottom: 1rem;\n}\n\nabbr[title] {\n  -webkit-text-decoration: underline dotted;\n  text-decoration: underline dotted;\n  cursor: help;\n  -webkit-text-decoration-skip-ink: none;\n  text-decoration-skip-ink: none;\n}\n\naddress {\n  margin-bottom: 1rem;\n  font-style: normal;\n  line-height: inherit;\n}\n\nol,\nul {\n  padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n  margin-top: 0;\n  margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n  margin-bottom: 0;\n}\n\ndt {\n  font-weight: 700;\n}\n\ndd {\n  margin-bottom: 0.5rem;\n  margin-left: 0;\n}\n\nblockquote {\n  margin: 0 0 1rem;\n}\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\nsmall, .small {\n  font-size: 0.875em;\n}\n\nmark, .mark {\n  padding: 0.1875em;\n  color: var(--bs-highlight-color);\n  background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n  position: relative;\n  font-size: 0.75em;\n  line-height: 0;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\na {\n  color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n  text-decoration: underline;\n}\na:hover {\n  --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n  color: inherit;\n  text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n  font-family: var(--bs-font-monospace);\n  font-size: 1em;\n}\n\npre {\n  display: block;\n  margin-top: 0;\n  margin-bottom: 1rem;\n  overflow: auto;\n  font-size: 0.875em;\n}\npre code {\n  font-size: inherit;\n  color: inherit;\n  word-break: normal;\n}\n\ncode {\n  font-size: 0.875em;\n  color: var(--bs-code-color);\n  word-wrap: break-word;\n}\na > code {\n  color: inherit;\n}\n\nkbd {\n  padding: 0.1875rem 0.375rem;\n  font-size: 0.875em;\n  color: var(--bs-body-bg);\n  background-color: var(--bs-body-color);\n  border-radius: 0.25rem;\n}\nkbd kbd {\n  padding: 0;\n  font-size: 1em;\n}\n\nfigure {\n  margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n  vertical-align: middle;\n}\n\ntable {\n  caption-side: bottom;\n  border-collapse: collapse;\n}\n\ncaption {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  color: var(--bs-secondary-color);\n  text-align: left;\n}\n\nth {\n  text-align: inherit;\n  text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n  border-color: inherit;\n  border-style: solid;\n  border-width: 0;\n}\n\nlabel {\n  display: inline-block;\n}\n\nbutton {\n  border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n  outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n  margin: 0;\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\nbutton,\nselect {\n  text-transform: none;\n}\n\n[role=button] {\n  cursor: pointer;\n}\n\nselect {\n  word-wrap: normal;\n}\nselect:disabled {\n  opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n  display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n  -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n  cursor: pointer;\n}\n\n::-moz-focus-inner {\n  padding: 0;\n  border-style: none;\n}\n\ntextarea {\n  resize: vertical;\n}\n\nfieldset {\n  min-width: 0;\n  padding: 0;\n  margin: 0;\n  border: 0;\n}\n\nlegend {\n  float: left;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 0.5rem;\n  line-height: inherit;\n  font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n  legend {\n    font-size: 1.5rem;\n  }\n}\nlegend + * {\n  clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n  padding: 0;\n}\n\n::-webkit-inner-spin-button {\n  height: auto;\n}\n\n[type=search] {\n  -webkit-appearance: textfield;\n  outline-offset: -2px;\n}\n[type=search]::-webkit-search-cancel-button {\n  cursor: pointer;\n  filter: grayscale(1);\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n  direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n  padding: 0;\n}\n\n::-webkit-file-upload-button {\n  font: inherit;\n  -webkit-appearance: button;\n}\n\n::file-selector-button {\n  font: inherit;\n  -webkit-appearance: button;\n}\n\noutput {\n  display: inline-block;\n}\n\niframe {\n  border: 0;\n}\n\nsummary {\n  display: list-item;\n  cursor: pointer;\n}\n\nprogress {\n  vertical-align: baseline;\n}\n\n[hidden] {\n  display: none !important;\n}\n\n.lead {\n  font-size: 1.25rem;\n  font-weight: 300;\n}\n\n.display-1 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.625rem + 4.5vw);\n}\n@media (min-width: 1200px) {\n  .display-1 {\n    font-size: 5rem;\n  }\n}\n\n.display-2 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.575rem + 3.9vw);\n}\n@media (min-width: 1200px) {\n  .display-2 {\n    font-size: 4.5rem;\n  }\n}\n\n.display-3 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.525rem + 3.3vw);\n}\n@media (min-width: 1200px) {\n  .display-3 {\n    font-size: 4rem;\n  }\n}\n\n.display-4 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.475rem + 2.7vw);\n}\n@media (min-width: 1200px) {\n  .display-4 {\n    font-size: 3.5rem;\n  }\n}\n\n.display-5 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.425rem + 2.1vw);\n}\n@media (min-width: 1200px) {\n  .display-5 {\n    font-size: 3rem;\n  }\n}\n\n.display-6 {\n  font-weight: 300;\n  line-height: 1.2;\n  font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n  .display-6 {\n    font-size: 2.5rem;\n  }\n}\n\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n.list-inline {\n  padding-left: 0;\n  list-style: none;\n}\n\n.list-inline-item {\n  display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n  margin-right: 0.5rem;\n}\n\n.initialism {\n  font-size: 0.875em;\n  text-transform: uppercase;\n}\n\n.blockquote {\n  margin-bottom: 1rem;\n  font-size: 1.25rem;\n}\n.blockquote > :last-child {\n  margin-bottom: 0;\n}\n\n.blockquote-footer {\n  margin-top: -1rem;\n  margin-bottom: 1rem;\n  font-size: 0.875em;\n  color: #6c757d;\n}\n.blockquote-footer::before {\n  content: \"— \";\n}\n\n.img-fluid {\n  max-width: 100%;\n  height: auto;\n}\n\n.img-thumbnail {\n  padding: 0.25rem;\n  background-color: var(--bs-body-bg);\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  max-width: 100%;\n  height: auto;\n}\n\n.figure {\n  display: inline-block;\n}\n\n.figure-img {\n  margin-bottom: 0.5rem;\n  line-height: 1;\n}\n\n.figure-caption {\n  font-size: 0.875em;\n  color: var(--bs-secondary-color);\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n  --bs-gutter-x: 1.5rem;\n  --bs-gutter-y: 0;\n  width: 100%;\n  padding-right: calc(var(--bs-gutter-x) * 0.5);\n  padding-left: calc(var(--bs-gutter-x) * 0.5);\n  margin-right: auto;\n  margin-left: auto;\n}\n\n@media (min-width: 576px) {\n  .container-sm, .container {\n    max-width: 540px;\n  }\n}\n@media (min-width: 768px) {\n  .container-md, .container-sm, .container {\n    max-width: 720px;\n  }\n}\n@media (min-width: 992px) {\n  .container-lg, .container-md, .container-sm, .container {\n    max-width: 960px;\n  }\n}\n@media (min-width: 1200px) {\n  .container-xl, .container-lg, .container-md, .container-sm, .container {\n    max-width: 1140px;\n  }\n}\n@media (min-width: 1400px) {\n  .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n    max-width: 1320px;\n  }\n}\n:root {\n  --bs-breakpoint-xs: 0;\n  --bs-breakpoint-sm: 576px;\n  --bs-breakpoint-md: 768px;\n  --bs-breakpoint-lg: 992px;\n  --bs-breakpoint-xl: 1200px;\n  --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n  --bs-gutter-x: 1.5rem;\n  --bs-gutter-y: 0;\n  display: flex;\n  flex-wrap: wrap;\n  margin-top: calc(-1 * var(--bs-gutter-y));\n  margin-right: calc(-0.5 * var(--bs-gutter-x));\n  margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n  flex-shrink: 0;\n  width: 100%;\n  max-width: 100%;\n  padding-right: calc(var(--bs-gutter-x) * 0.5);\n  padding-left: calc(var(--bs-gutter-x) * 0.5);\n  margin-top: var(--bs-gutter-y);\n}\n\n.col {\n  flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n  flex: 0 0 auto;\n  width: auto;\n}\n\n.row-cols-1 > * {\n  flex: 0 0 auto;\n  width: 100%;\n}\n\n.row-cols-2 > * {\n  flex: 0 0 auto;\n  width: 50%;\n}\n\n.row-cols-3 > * {\n  flex: 0 0 auto;\n  width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n  flex: 0 0 auto;\n  width: 25%;\n}\n\n.row-cols-5 > * {\n  flex: 0 0 auto;\n  width: 20%;\n}\n\n.row-cols-6 > * {\n  flex: 0 0 auto;\n  width: 16.66666667%;\n}\n\n.col-auto {\n  flex: 0 0 auto;\n  width: auto;\n}\n\n.col-1 {\n  flex: 0 0 auto;\n  width: 8.33333333%;\n}\n\n.col-2 {\n  flex: 0 0 auto;\n  width: 16.66666667%;\n}\n\n.col-3 {\n  flex: 0 0 auto;\n  width: 25%;\n}\n\n.col-4 {\n  flex: 0 0 auto;\n  width: 33.33333333%;\n}\n\n.col-5 {\n  flex: 0 0 auto;\n  width: 41.66666667%;\n}\n\n.col-6 {\n  flex: 0 0 auto;\n  width: 50%;\n}\n\n.col-7 {\n  flex: 0 0 auto;\n  width: 58.33333333%;\n}\n\n.col-8 {\n  flex: 0 0 auto;\n  width: 66.66666667%;\n}\n\n.col-9 {\n  flex: 0 0 auto;\n  width: 75%;\n}\n\n.col-10 {\n  flex: 0 0 auto;\n  width: 83.33333333%;\n}\n\n.col-11 {\n  flex: 0 0 auto;\n  width: 91.66666667%;\n}\n\n.col-12 {\n  flex: 0 0 auto;\n  width: 100%;\n}\n\n.offset-1 {\n  margin-left: 8.33333333%;\n}\n\n.offset-2 {\n  margin-left: 16.66666667%;\n}\n\n.offset-3 {\n  margin-left: 25%;\n}\n\n.offset-4 {\n  margin-left: 33.33333333%;\n}\n\n.offset-5 {\n  margin-left: 41.66666667%;\n}\n\n.offset-6 {\n  margin-left: 50%;\n}\n\n.offset-7 {\n  margin-left: 58.33333333%;\n}\n\n.offset-8 {\n  margin-left: 66.66666667%;\n}\n\n.offset-9 {\n  margin-left: 75%;\n}\n\n.offset-10 {\n  margin-left: 83.33333333%;\n}\n\n.offset-11 {\n  margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n  --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n  --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n  --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n  --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n  --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n  --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n  --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n  --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n  --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n  --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n  --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n  --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n  .col-sm {\n    flex: 1 0 0;\n  }\n  .row-cols-sm-auto > * {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .row-cols-sm-1 > * {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .row-cols-sm-2 > * {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .row-cols-sm-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .row-cols-sm-4 > * {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .row-cols-sm-5 > * {\n    flex: 0 0 auto;\n    width: 20%;\n  }\n  .row-cols-sm-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-sm-auto {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .col-sm-1 {\n    flex: 0 0 auto;\n    width: 8.33333333%;\n  }\n  .col-sm-2 {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-sm-3 {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .col-sm-4 {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .col-sm-5 {\n    flex: 0 0 auto;\n    width: 41.66666667%;\n  }\n  .col-sm-6 {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .col-sm-7 {\n    flex: 0 0 auto;\n    width: 58.33333333%;\n  }\n  .col-sm-8 {\n    flex: 0 0 auto;\n    width: 66.66666667%;\n  }\n  .col-sm-9 {\n    flex: 0 0 auto;\n    width: 75%;\n  }\n  .col-sm-10 {\n    flex: 0 0 auto;\n    width: 83.33333333%;\n  }\n  .col-sm-11 {\n    flex: 0 0 auto;\n    width: 91.66666667%;\n  }\n  .col-sm-12 {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .offset-sm-0 {\n    margin-left: 0;\n  }\n  .offset-sm-1 {\n    margin-left: 8.33333333%;\n  }\n  .offset-sm-2 {\n    margin-left: 16.66666667%;\n  }\n  .offset-sm-3 {\n    margin-left: 25%;\n  }\n  .offset-sm-4 {\n    margin-left: 33.33333333%;\n  }\n  .offset-sm-5 {\n    margin-left: 41.66666667%;\n  }\n  .offset-sm-6 {\n    margin-left: 50%;\n  }\n  .offset-sm-7 {\n    margin-left: 58.33333333%;\n  }\n  .offset-sm-8 {\n    margin-left: 66.66666667%;\n  }\n  .offset-sm-9 {\n    margin-left: 75%;\n  }\n  .offset-sm-10 {\n    margin-left: 83.33333333%;\n  }\n  .offset-sm-11 {\n    margin-left: 91.66666667%;\n  }\n  .g-sm-0,\n  .gx-sm-0 {\n    --bs-gutter-x: 0;\n  }\n  .g-sm-0,\n  .gy-sm-0 {\n    --bs-gutter-y: 0;\n  }\n  .g-sm-1,\n  .gx-sm-1 {\n    --bs-gutter-x: 0.25rem;\n  }\n  .g-sm-1,\n  .gy-sm-1 {\n    --bs-gutter-y: 0.25rem;\n  }\n  .g-sm-2,\n  .gx-sm-2 {\n    --bs-gutter-x: 0.5rem;\n  }\n  .g-sm-2,\n  .gy-sm-2 {\n    --bs-gutter-y: 0.5rem;\n  }\n  .g-sm-3,\n  .gx-sm-3 {\n    --bs-gutter-x: 1rem;\n  }\n  .g-sm-3,\n  .gy-sm-3 {\n    --bs-gutter-y: 1rem;\n  }\n  .g-sm-4,\n  .gx-sm-4 {\n    --bs-gutter-x: 1.5rem;\n  }\n  .g-sm-4,\n  .gy-sm-4 {\n    --bs-gutter-y: 1.5rem;\n  }\n  .g-sm-5,\n  .gx-sm-5 {\n    --bs-gutter-x: 3rem;\n  }\n  .g-sm-5,\n  .gy-sm-5 {\n    --bs-gutter-y: 3rem;\n  }\n}\n@media (min-width: 768px) {\n  .col-md {\n    flex: 1 0 0;\n  }\n  .row-cols-md-auto > * {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .row-cols-md-1 > * {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .row-cols-md-2 > * {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .row-cols-md-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .row-cols-md-4 > * {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .row-cols-md-5 > * {\n    flex: 0 0 auto;\n    width: 20%;\n  }\n  .row-cols-md-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-md-auto {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .col-md-1 {\n    flex: 0 0 auto;\n    width: 8.33333333%;\n  }\n  .col-md-2 {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-md-3 {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .col-md-4 {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .col-md-5 {\n    flex: 0 0 auto;\n    width: 41.66666667%;\n  }\n  .col-md-6 {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .col-md-7 {\n    flex: 0 0 auto;\n    width: 58.33333333%;\n  }\n  .col-md-8 {\n    flex: 0 0 auto;\n    width: 66.66666667%;\n  }\n  .col-md-9 {\n    flex: 0 0 auto;\n    width: 75%;\n  }\n  .col-md-10 {\n    flex: 0 0 auto;\n    width: 83.33333333%;\n  }\n  .col-md-11 {\n    flex: 0 0 auto;\n    width: 91.66666667%;\n  }\n  .col-md-12 {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .offset-md-0 {\n    margin-left: 0;\n  }\n  .offset-md-1 {\n    margin-left: 8.33333333%;\n  }\n  .offset-md-2 {\n    margin-left: 16.66666667%;\n  }\n  .offset-md-3 {\n    margin-left: 25%;\n  }\n  .offset-md-4 {\n    margin-left: 33.33333333%;\n  }\n  .offset-md-5 {\n    margin-left: 41.66666667%;\n  }\n  .offset-md-6 {\n    margin-left: 50%;\n  }\n  .offset-md-7 {\n    margin-left: 58.33333333%;\n  }\n  .offset-md-8 {\n    margin-left: 66.66666667%;\n  }\n  .offset-md-9 {\n    margin-left: 75%;\n  }\n  .offset-md-10 {\n    margin-left: 83.33333333%;\n  }\n  .offset-md-11 {\n    margin-left: 91.66666667%;\n  }\n  .g-md-0,\n  .gx-md-0 {\n    --bs-gutter-x: 0;\n  }\n  .g-md-0,\n  .gy-md-0 {\n    --bs-gutter-y: 0;\n  }\n  .g-md-1,\n  .gx-md-1 {\n    --bs-gutter-x: 0.25rem;\n  }\n  .g-md-1,\n  .gy-md-1 {\n    --bs-gutter-y: 0.25rem;\n  }\n  .g-md-2,\n  .gx-md-2 {\n    --bs-gutter-x: 0.5rem;\n  }\n  .g-md-2,\n  .gy-md-2 {\n    --bs-gutter-y: 0.5rem;\n  }\n  .g-md-3,\n  .gx-md-3 {\n    --bs-gutter-x: 1rem;\n  }\n  .g-md-3,\n  .gy-md-3 {\n    --bs-gutter-y: 1rem;\n  }\n  .g-md-4,\n  .gx-md-4 {\n    --bs-gutter-x: 1.5rem;\n  }\n  .g-md-4,\n  .gy-md-4 {\n    --bs-gutter-y: 1.5rem;\n  }\n  .g-md-5,\n  .gx-md-5 {\n    --bs-gutter-x: 3rem;\n  }\n  .g-md-5,\n  .gy-md-5 {\n    --bs-gutter-y: 3rem;\n  }\n}\n@media (min-width: 992px) {\n  .col-lg {\n    flex: 1 0 0;\n  }\n  .row-cols-lg-auto > * {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .row-cols-lg-1 > * {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .row-cols-lg-2 > * {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .row-cols-lg-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .row-cols-lg-4 > * {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .row-cols-lg-5 > * {\n    flex: 0 0 auto;\n    width: 20%;\n  }\n  .row-cols-lg-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-lg-auto {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .col-lg-1 {\n    flex: 0 0 auto;\n    width: 8.33333333%;\n  }\n  .col-lg-2 {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-lg-3 {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .col-lg-4 {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .col-lg-5 {\n    flex: 0 0 auto;\n    width: 41.66666667%;\n  }\n  .col-lg-6 {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .col-lg-7 {\n    flex: 0 0 auto;\n    width: 58.33333333%;\n  }\n  .col-lg-8 {\n    flex: 0 0 auto;\n    width: 66.66666667%;\n  }\n  .col-lg-9 {\n    flex: 0 0 auto;\n    width: 75%;\n  }\n  .col-lg-10 {\n    flex: 0 0 auto;\n    width: 83.33333333%;\n  }\n  .col-lg-11 {\n    flex: 0 0 auto;\n    width: 91.66666667%;\n  }\n  .col-lg-12 {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .offset-lg-0 {\n    margin-left: 0;\n  }\n  .offset-lg-1 {\n    margin-left: 8.33333333%;\n  }\n  .offset-lg-2 {\n    margin-left: 16.66666667%;\n  }\n  .offset-lg-3 {\n    margin-left: 25%;\n  }\n  .offset-lg-4 {\n    margin-left: 33.33333333%;\n  }\n  .offset-lg-5 {\n    margin-left: 41.66666667%;\n  }\n  .offset-lg-6 {\n    margin-left: 50%;\n  }\n  .offset-lg-7 {\n    margin-left: 58.33333333%;\n  }\n  .offset-lg-8 {\n    margin-left: 66.66666667%;\n  }\n  .offset-lg-9 {\n    margin-left: 75%;\n  }\n  .offset-lg-10 {\n    margin-left: 83.33333333%;\n  }\n  .offset-lg-11 {\n    margin-left: 91.66666667%;\n  }\n  .g-lg-0,\n  .gx-lg-0 {\n    --bs-gutter-x: 0;\n  }\n  .g-lg-0,\n  .gy-lg-0 {\n    --bs-gutter-y: 0;\n  }\n  .g-lg-1,\n  .gx-lg-1 {\n    --bs-gutter-x: 0.25rem;\n  }\n  .g-lg-1,\n  .gy-lg-1 {\n    --bs-gutter-y: 0.25rem;\n  }\n  .g-lg-2,\n  .gx-lg-2 {\n    --bs-gutter-x: 0.5rem;\n  }\n  .g-lg-2,\n  .gy-lg-2 {\n    --bs-gutter-y: 0.5rem;\n  }\n  .g-lg-3,\n  .gx-lg-3 {\n    --bs-gutter-x: 1rem;\n  }\n  .g-lg-3,\n  .gy-lg-3 {\n    --bs-gutter-y: 1rem;\n  }\n  .g-lg-4,\n  .gx-lg-4 {\n    --bs-gutter-x: 1.5rem;\n  }\n  .g-lg-4,\n  .gy-lg-4 {\n    --bs-gutter-y: 1.5rem;\n  }\n  .g-lg-5,\n  .gx-lg-5 {\n    --bs-gutter-x: 3rem;\n  }\n  .g-lg-5,\n  .gy-lg-5 {\n    --bs-gutter-y: 3rem;\n  }\n}\n@media (min-width: 1200px) {\n  .col-xl {\n    flex: 1 0 0;\n  }\n  .row-cols-xl-auto > * {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .row-cols-xl-1 > * {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .row-cols-xl-2 > * {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .row-cols-xl-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .row-cols-xl-4 > * {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .row-cols-xl-5 > * {\n    flex: 0 0 auto;\n    width: 20%;\n  }\n  .row-cols-xl-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-xl-auto {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .col-xl-1 {\n    flex: 0 0 auto;\n    width: 8.33333333%;\n  }\n  .col-xl-2 {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-xl-3 {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .col-xl-4 {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .col-xl-5 {\n    flex: 0 0 auto;\n    width: 41.66666667%;\n  }\n  .col-xl-6 {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .col-xl-7 {\n    flex: 0 0 auto;\n    width: 58.33333333%;\n  }\n  .col-xl-8 {\n    flex: 0 0 auto;\n    width: 66.66666667%;\n  }\n  .col-xl-9 {\n    flex: 0 0 auto;\n    width: 75%;\n  }\n  .col-xl-10 {\n    flex: 0 0 auto;\n    width: 83.33333333%;\n  }\n  .col-xl-11 {\n    flex: 0 0 auto;\n    width: 91.66666667%;\n  }\n  .col-xl-12 {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .offset-xl-0 {\n    margin-left: 0;\n  }\n  .offset-xl-1 {\n    margin-left: 8.33333333%;\n  }\n  .offset-xl-2 {\n    margin-left: 16.66666667%;\n  }\n  .offset-xl-3 {\n    margin-left: 25%;\n  }\n  .offset-xl-4 {\n    margin-left: 33.33333333%;\n  }\n  .offset-xl-5 {\n    margin-left: 41.66666667%;\n  }\n  .offset-xl-6 {\n    margin-left: 50%;\n  }\n  .offset-xl-7 {\n    margin-left: 58.33333333%;\n  }\n  .offset-xl-8 {\n    margin-left: 66.66666667%;\n  }\n  .offset-xl-9 {\n    margin-left: 75%;\n  }\n  .offset-xl-10 {\n    margin-left: 83.33333333%;\n  }\n  .offset-xl-11 {\n    margin-left: 91.66666667%;\n  }\n  .g-xl-0,\n  .gx-xl-0 {\n    --bs-gutter-x: 0;\n  }\n  .g-xl-0,\n  .gy-xl-0 {\n    --bs-gutter-y: 0;\n  }\n  .g-xl-1,\n  .gx-xl-1 {\n    --bs-gutter-x: 0.25rem;\n  }\n  .g-xl-1,\n  .gy-xl-1 {\n    --bs-gutter-y: 0.25rem;\n  }\n  .g-xl-2,\n  .gx-xl-2 {\n    --bs-gutter-x: 0.5rem;\n  }\n  .g-xl-2,\n  .gy-xl-2 {\n    --bs-gutter-y: 0.5rem;\n  }\n  .g-xl-3,\n  .gx-xl-3 {\n    --bs-gutter-x: 1rem;\n  }\n  .g-xl-3,\n  .gy-xl-3 {\n    --bs-gutter-y: 1rem;\n  }\n  .g-xl-4,\n  .gx-xl-4 {\n    --bs-gutter-x: 1.5rem;\n  }\n  .g-xl-4,\n  .gy-xl-4 {\n    --bs-gutter-y: 1.5rem;\n  }\n  .g-xl-5,\n  .gx-xl-5 {\n    --bs-gutter-x: 3rem;\n  }\n  .g-xl-5,\n  .gy-xl-5 {\n    --bs-gutter-y: 3rem;\n  }\n}\n@media (min-width: 1400px) {\n  .col-xxl {\n    flex: 1 0 0;\n  }\n  .row-cols-xxl-auto > * {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .row-cols-xxl-1 > * {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .row-cols-xxl-2 > * {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .row-cols-xxl-3 > * {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .row-cols-xxl-4 > * {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .row-cols-xxl-5 > * {\n    flex: 0 0 auto;\n    width: 20%;\n  }\n  .row-cols-xxl-6 > * {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-xxl-auto {\n    flex: 0 0 auto;\n    width: auto;\n  }\n  .col-xxl-1 {\n    flex: 0 0 auto;\n    width: 8.33333333%;\n  }\n  .col-xxl-2 {\n    flex: 0 0 auto;\n    width: 16.66666667%;\n  }\n  .col-xxl-3 {\n    flex: 0 0 auto;\n    width: 25%;\n  }\n  .col-xxl-4 {\n    flex: 0 0 auto;\n    width: 33.33333333%;\n  }\n  .col-xxl-5 {\n    flex: 0 0 auto;\n    width: 41.66666667%;\n  }\n  .col-xxl-6 {\n    flex: 0 0 auto;\n    width: 50%;\n  }\n  .col-xxl-7 {\n    flex: 0 0 auto;\n    width: 58.33333333%;\n  }\n  .col-xxl-8 {\n    flex: 0 0 auto;\n    width: 66.66666667%;\n  }\n  .col-xxl-9 {\n    flex: 0 0 auto;\n    width: 75%;\n  }\n  .col-xxl-10 {\n    flex: 0 0 auto;\n    width: 83.33333333%;\n  }\n  .col-xxl-11 {\n    flex: 0 0 auto;\n    width: 91.66666667%;\n  }\n  .col-xxl-12 {\n    flex: 0 0 auto;\n    width: 100%;\n  }\n  .offset-xxl-0 {\n    margin-left: 0;\n  }\n  .offset-xxl-1 {\n    margin-left: 8.33333333%;\n  }\n  .offset-xxl-2 {\n    margin-left: 16.66666667%;\n  }\n  .offset-xxl-3 {\n    margin-left: 25%;\n  }\n  .offset-xxl-4 {\n    margin-left: 33.33333333%;\n  }\n  .offset-xxl-5 {\n    margin-left: 41.66666667%;\n  }\n  .offset-xxl-6 {\n    margin-left: 50%;\n  }\n  .offset-xxl-7 {\n    margin-left: 58.33333333%;\n  }\n  .offset-xxl-8 {\n    margin-left: 66.66666667%;\n  }\n  .offset-xxl-9 {\n    margin-left: 75%;\n  }\n  .offset-xxl-10 {\n    margin-left: 83.33333333%;\n  }\n  .offset-xxl-11 {\n    margin-left: 91.66666667%;\n  }\n  .g-xxl-0,\n  .gx-xxl-0 {\n    --bs-gutter-x: 0;\n  }\n  .g-xxl-0,\n  .gy-xxl-0 {\n    --bs-gutter-y: 0;\n  }\n  .g-xxl-1,\n  .gx-xxl-1 {\n    --bs-gutter-x: 0.25rem;\n  }\n  .g-xxl-1,\n  .gy-xxl-1 {\n    --bs-gutter-y: 0.25rem;\n  }\n  .g-xxl-2,\n  .gx-xxl-2 {\n    --bs-gutter-x: 0.5rem;\n  }\n  .g-xxl-2,\n  .gy-xxl-2 {\n    --bs-gutter-y: 0.5rem;\n  }\n  .g-xxl-3,\n  .gx-xxl-3 {\n    --bs-gutter-x: 1rem;\n  }\n  .g-xxl-3,\n  .gy-xxl-3 {\n    --bs-gutter-y: 1rem;\n  }\n  .g-xxl-4,\n  .gx-xxl-4 {\n    --bs-gutter-x: 1.5rem;\n  }\n  .g-xxl-4,\n  .gy-xxl-4 {\n    --bs-gutter-y: 1.5rem;\n  }\n  .g-xxl-5,\n  .gx-xxl-5 {\n    --bs-gutter-x: 3rem;\n  }\n  .g-xxl-5,\n  .gy-xxl-5 {\n    --bs-gutter-y: 3rem;\n  }\n}\n.table {\n  --bs-table-color-type: initial;\n  --bs-table-bg-type: initial;\n  --bs-table-color-state: initial;\n  --bs-table-bg-state: initial;\n  --bs-table-color: var(--bs-emphasis-color);\n  --bs-table-bg: var(--bs-body-bg);\n  --bs-table-border-color: var(--bs-border-color);\n  --bs-table-accent-bg: transparent;\n  --bs-table-striped-color: var(--bs-emphasis-color);\n  --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);\n  --bs-table-active-color: var(--bs-emphasis-color);\n  --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);\n  --bs-table-hover-color: var(--bs-emphasis-color);\n  --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);\n  width: 100%;\n  margin-bottom: 1rem;\n  vertical-align: top;\n  border-color: var(--bs-table-border-color);\n}\n.table > :not(caption) > * > * {\n  padding: 0.5rem 0.5rem;\n  color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));\n  background-color: var(--bs-table-bg);\n  border-bottom-width: var(--bs-border-width);\n  box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)));\n}\n.table > tbody {\n  vertical-align: inherit;\n}\n.table > thead {\n  vertical-align: bottom;\n}\n\n.table-group-divider {\n  border-top: calc(var(--bs-border-width) * 2) solid currentcolor;\n}\n\n.caption-top {\n  caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n  padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n  border-width: var(--bs-border-width) 0;\n}\n.table-bordered > :not(caption) > * > * {\n  border-width: 0 var(--bs-border-width);\n}\n\n.table-borderless > :not(caption) > * > * {\n  border-bottom-width: 0;\n}\n.table-borderless > :not(:first-child) {\n  border-top-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n  --bs-table-color-type: var(--bs-table-striped-color);\n  --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-striped-columns > :not(caption) > tr > :nth-child(even) {\n  --bs-table-color-type: var(--bs-table-striped-color);\n  --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-active {\n  --bs-table-color-state: var(--bs-table-active-color);\n  --bs-table-bg-state: var(--bs-table-active-bg);\n}\n\n.table-hover > tbody > tr:hover > * {\n  --bs-table-color-state: var(--bs-table-hover-color);\n  --bs-table-bg-state: var(--bs-table-hover-bg);\n}\n\n.table-primary {\n  --bs-table-color: #000;\n  --bs-table-bg: #cfe2ff;\n  --bs-table-border-color: #a6b5cc;\n  --bs-table-striped-bg: #c5d7f2;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #bacbe6;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #bfd1ec;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-secondary {\n  --bs-table-color: #000;\n  --bs-table-bg: #e2e3e5;\n  --bs-table-border-color: #b5b6b7;\n  --bs-table-striped-bg: #d7d8da;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #cbccce;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #d1d2d4;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-success {\n  --bs-table-color: #000;\n  --bs-table-bg: #d1e7dd;\n  --bs-table-border-color: #a7b9b1;\n  --bs-table-striped-bg: #c7dbd2;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #bcd0c7;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #c1d6cc;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-info {\n  --bs-table-color: #000;\n  --bs-table-bg: #cff4fc;\n  --bs-table-border-color: #a6c3ca;\n  --bs-table-striped-bg: #c5e8ef;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #badce3;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #bfe2e9;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-warning {\n  --bs-table-color: #000;\n  --bs-table-bg: #fff3cd;\n  --bs-table-border-color: #ccc2a4;\n  --bs-table-striped-bg: #f2e7c3;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #e6dbb9;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #ece1be;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-danger {\n  --bs-table-color: #000;\n  --bs-table-bg: #f8d7da;\n  --bs-table-border-color: #c6acae;\n  --bs-table-striped-bg: #eccccf;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #dfc2c4;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #e5c7ca;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-light {\n  --bs-table-color: #000;\n  --bs-table-bg: #f8f9fa;\n  --bs-table-border-color: #c6c7c8;\n  --bs-table-striped-bg: #ecedee;\n  --bs-table-striped-color: #000;\n  --bs-table-active-bg: #dfe0e1;\n  --bs-table-active-color: #000;\n  --bs-table-hover-bg: #e5e6e7;\n  --bs-table-hover-color: #000;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-dark {\n  --bs-table-color: #fff;\n  --bs-table-bg: #212529;\n  --bs-table-border-color: #4d5154;\n  --bs-table-striped-bg: #2c3034;\n  --bs-table-striped-color: #fff;\n  --bs-table-active-bg: #373b3e;\n  --bs-table-active-color: #fff;\n  --bs-table-hover-bg: #323539;\n  --bs-table-hover-color: #fff;\n  color: var(--bs-table-color);\n  border-color: var(--bs-table-border-color);\n}\n\n.table-responsive {\n  overflow-x: auto;\n  -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n  .table-responsive-sm {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n}\n@media (max-width: 767.98px) {\n  .table-responsive-md {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n}\n@media (max-width: 991.98px) {\n  .table-responsive-lg {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n}\n@media (max-width: 1199.98px) {\n  .table-responsive-xl {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n}\n@media (max-width: 1399.98px) {\n  .table-responsive-xxl {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n}\n.form-label {\n  margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n  padding-top: calc(0.375rem + var(--bs-border-width));\n  padding-bottom: calc(0.375rem + var(--bs-border-width));\n  margin-bottom: 0;\n  font-size: inherit;\n  line-height: 1.5;\n}\n\n.col-form-label-lg {\n  padding-top: calc(0.5rem + var(--bs-border-width));\n  padding-bottom: calc(0.5rem + var(--bs-border-width));\n  font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n  padding-top: calc(0.25rem + var(--bs-border-width));\n  padding-bottom: calc(0.25rem + var(--bs-border-width));\n  font-size: 0.875rem;\n}\n\n.form-text {\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-secondary-color);\n}\n\n.form-control {\n  display: block;\n  width: 100%;\n  padding: 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: var(--bs-body-bg);\n  background-clip: padding-box;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-control {\n    transition: none;\n  }\n}\n.form-control[type=file] {\n  overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n  cursor: pointer;\n}\n.form-control:focus {\n  color: var(--bs-body-color);\n  background-color: var(--bs-body-bg);\n  border-color: #86b7fe;\n  outline: 0;\n  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n  min-width: 85px;\n  height: 1.5em;\n  margin: 0;\n}\n.form-control::-webkit-datetime-edit {\n  display: block;\n  padding: 0;\n}\n.form-control::placeholder {\n  color: var(--bs-secondary-color);\n  opacity: 1;\n}\n.form-control:disabled {\n  background-color: var(--bs-secondary-bg);\n  opacity: 1;\n}\n.form-control::-webkit-file-upload-button {\n  padding: 0.375rem 0.75rem;\n  margin: -0.375rem -0.75rem;\n  -webkit-margin-end: 0.75rem;\n  margin-inline-end: 0.75rem;\n  color: var(--bs-body-color);\n  background-color: var(--bs-tertiary-bg);\n  pointer-events: none;\n  border-color: inherit;\n  border-style: solid;\n  border-width: 0;\n  border-inline-end-width: var(--bs-border-width);\n  border-radius: 0;\n  -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n.form-control::file-selector-button {\n  padding: 0.375rem 0.75rem;\n  margin: -0.375rem -0.75rem;\n  -webkit-margin-end: 0.75rem;\n  margin-inline-end: 0.75rem;\n  color: var(--bs-body-color);\n  background-color: var(--bs-tertiary-bg);\n  pointer-events: none;\n  border-color: inherit;\n  border-style: solid;\n  border-width: 0;\n  border-inline-end-width: var(--bs-border-width);\n  border-radius: 0;\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-control::-webkit-file-upload-button {\n    -webkit-transition: none;\n    transition: none;\n  }\n  .form-control::file-selector-button {\n    transition: none;\n  }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n  background-color: var(--bs-secondary-bg);\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n  background-color: var(--bs-secondary-bg);\n}\n\n.form-control-plaintext {\n  display: block;\n  width: 100%;\n  padding: 0.375rem 0;\n  margin-bottom: 0;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  background-color: transparent;\n  border: solid transparent;\n  border-width: var(--bs-border-width) 0;\n}\n.form-control-plaintext:focus {\n  outline: 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n  padding-right: 0;\n  padding-left: 0;\n}\n\n.form-control-sm {\n  min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n  padding: 0.25rem 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm);\n}\n.form-control-sm::-webkit-file-upload-button {\n  padding: 0.25rem 0.5rem;\n  margin: -0.25rem -0.5rem;\n  -webkit-margin-end: 0.5rem;\n  margin-inline-end: 0.5rem;\n}\n.form-control-sm::file-selector-button {\n  padding: 0.25rem 0.5rem;\n  margin: -0.25rem -0.5rem;\n  -webkit-margin-end: 0.5rem;\n  margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n  min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n  padding: 0.5rem 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg);\n}\n.form-control-lg::-webkit-file-upload-button {\n  padding: 0.5rem 1rem;\n  margin: -0.5rem -1rem;\n  -webkit-margin-end: 1rem;\n  margin-inline-end: 1rem;\n}\n.form-control-lg::file-selector-button {\n  padding: 0.5rem 1rem;\n  margin: -0.5rem -1rem;\n  -webkit-margin-end: 1rem;\n  margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n  min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-sm {\n  min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-lg {\n  min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-control-color {\n  width: 3rem;\n  height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n  padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n  cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n  border: 0 !important;\n  border-radius: var(--bs-border-radius);\n}\n.form-control-color::-webkit-color-swatch {\n  border: 0 !important;\n  border-radius: var(--bs-border-radius);\n}\n.form-control-color.form-control-sm {\n  height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\n.form-control-color.form-control-lg {\n  height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-select {\n  --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n  display: block;\n  width: 100%;\n  padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: var(--bs-body-bg);\n  background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);\n  background-repeat: no-repeat;\n  background-position: right 0.75rem center;\n  background-size: 16px 12px;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-select {\n    transition: none;\n  }\n}\n.form-select:focus {\n  border-color: #86b7fe;\n  outline: 0;\n  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n  padding-right: 0.75rem;\n  background-image: none;\n}\n.form-select:disabled {\n  background-color: var(--bs-secondary-bg);\n}\n.form-select:-moz-focusring {\n  color: transparent;\n  text-shadow: 0 0 0 var(--bs-body-color);\n}\n\n.form-select-sm {\n  padding-top: 0.25rem;\n  padding-bottom: 0.25rem;\n  padding-left: 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm);\n}\n\n.form-select-lg {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  padding-left: 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg);\n}\n\n[data-bs-theme=dark] .form-select {\n  --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n}\n\n.form-check {\n  display: block;\n  min-height: 1.5rem;\n  padding-left: 1.5em;\n  margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n  float: left;\n  margin-left: -1.5em;\n}\n\n.form-check-reverse {\n  padding-right: 1.5em;\n  padding-left: 0;\n  text-align: right;\n}\n.form-check-reverse .form-check-input {\n  float: right;\n  margin-right: -1.5em;\n  margin-left: 0;\n}\n\n.form-check-input {\n  --bs-form-check-bg: var(--bs-body-bg);\n  flex-shrink: 0;\n  width: 1em;\n  height: 1em;\n  margin-top: 0.25em;\n  vertical-align: top;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: var(--bs-form-check-bg);\n  background-image: var(--bs-form-check-bg-image);\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: contain;\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  -webkit-print-color-adjust: exact;\n  color-adjust: exact;\n  print-color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n  border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n  border-radius: 50%;\n}\n.form-check-input:active {\n  filter: brightness(90%);\n}\n.form-check-input:focus {\n  border-color: #86b7fe;\n  outline: 0;\n  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n  background-color: #0d6efd;\n  border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n  --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n  --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n  background-color: #0d6efd;\n  border-color: #0d6efd;\n  --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n  pointer-events: none;\n  filter: none;\n  opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.form-switch {\n  padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n  --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n  width: 2em;\n  margin-left: -2.5em;\n  background-image: var(--bs-form-switch-bg);\n  background-position: left center;\n  border-radius: 2em;\n  transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-switch .form-check-input {\n    transition: none;\n  }\n}\n.form-switch .form-check-input:focus {\n  --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n  background-position: right center;\n  --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-switch.form-check-reverse {\n  padding-right: 2.5em;\n  padding-left: 0;\n}\n.form-switch.form-check-reverse .form-check-input {\n  margin-right: -2.5em;\n  margin-left: 0;\n}\n\n.form-check-inline {\n  display: inline-block;\n  margin-right: 1rem;\n}\n\n.btn-check {\n  position: absolute;\n  clip: rect(0, 0, 0, 0);\n  pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n  pointer-events: none;\n  filter: none;\n  opacity: 0.65;\n}\n\n[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) {\n  --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e\");\n}\n\n.form-range {\n  width: 100%;\n  height: 1.5rem;\n  padding: 0;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: transparent;\n}\n.form-range:focus {\n  outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n  box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n  box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n  border: 0;\n}\n.form-range::-webkit-slider-thumb {\n  width: 1rem;\n  height: 1rem;\n  margin-top: -0.25rem;\n  -webkit-appearance: none;\n  appearance: none;\n  background-color: #0d6efd;\n  border: 0;\n  border-radius: 1rem;\n  -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-range::-webkit-slider-thumb {\n    -webkit-transition: none;\n    transition: none;\n  }\n}\n.form-range::-webkit-slider-thumb:active {\n  background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n  width: 100%;\n  height: 0.5rem;\n  color: transparent;\n  cursor: pointer;\n  background-color: var(--bs-secondary-bg);\n  border-color: transparent;\n  border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n  width: 1rem;\n  height: 1rem;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: #0d6efd;\n  border: 0;\n  border-radius: 1rem;\n  -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-range::-moz-range-thumb {\n    -moz-transition: none;\n    transition: none;\n  }\n}\n.form-range::-moz-range-thumb:active {\n  background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n  width: 100%;\n  height: 0.5rem;\n  color: transparent;\n  cursor: pointer;\n  background-color: var(--bs-secondary-bg);\n  border-color: transparent;\n  border-radius: 1rem;\n}\n.form-range:disabled {\n  pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n  background-color: var(--bs-secondary-color);\n}\n.form-range:disabled::-moz-range-thumb {\n  background-color: var(--bs-secondary-color);\n}\n\n.form-floating {\n  position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext,\n.form-floating > .form-select {\n  height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n  min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n  line-height: 1.25;\n}\n.form-floating > label {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 2;\n  max-width: 100%;\n  height: 100%;\n  padding: 1rem 0.75rem;\n  overflow: hidden;\n  color: rgba(var(--bs-body-color-rgb), 0.65);\n  text-align: start;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  pointer-events: none;\n  border: var(--bs-border-width) solid transparent;\n  transform-origin: 0 0;\n  transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .form-floating > label {\n    transition: none;\n  }\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext {\n  padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::placeholder,\n.form-floating > .form-control-plaintext::placeholder {\n  color: transparent;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),\n.form-floating > .form-control-plaintext:focus,\n.form-floating > .form-control-plaintext:not(:placeholder-shown) {\n  padding-top: 1.625rem;\n  padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill,\n.form-floating > .form-control-plaintext:-webkit-autofill {\n  padding-top: 1.625rem;\n  padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n  padding-top: 1.625rem;\n  padding-bottom: 0.625rem;\n  padding-left: 0.75rem;\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-control-plaintext ~ label,\n.form-floating > .form-select ~ label {\n  transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n  transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > textarea:focus ~ label::after,\n.form-floating > textarea:not(:placeholder-shown) ~ label::after {\n  position: absolute;\n  inset: 1rem 0.375rem;\n  z-index: -1;\n  height: 1.5em;\n  content: \"\";\n  background-color: var(--bs-body-bg);\n  border-radius: var(--bs-border-radius);\n}\n.form-floating > textarea:disabled ~ label::after {\n  background-color: var(--bs-secondary-bg);\n}\n.form-floating > .form-control-plaintext ~ label {\n  border-width: var(--bs-border-width) 0;\n}\n.form-floating > :disabled ~ label,\n.form-floating > .form-control:disabled ~ label {\n  color: #6c757d;\n}\n\n.input-group {\n  position: relative;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: stretch;\n  width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select,\n.input-group > .form-floating {\n  position: relative;\n  flex: 1 1 auto;\n  width: 1%;\n  min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus,\n.input-group > .form-floating:focus-within {\n  z-index: 5;\n}\n.input-group .btn {\n  position: relative;\n  z-index: 2;\n}\n.input-group .btn:focus {\n  z-index: 5;\n}\n\n.input-group-text {\n  display: flex;\n  align-items: center;\n  padding: 0.375rem 0.75rem;\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  text-align: center;\n  white-space: nowrap;\n  background-color: var(--bs-tertiary-bg);\n  border: var(--bs-border-width) solid var(--bs-border-color);\n  border-radius: var(--bs-border-radius);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n  padding: 0.5rem 1rem;\n  font-size: 1.25rem;\n  border-radius: var(--bs-border-radius-lg);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n  padding: 0.25rem 0.5rem;\n  font-size: 0.875rem;\n  border-radius: var(--bs-border-radius-sm);\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n  padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4),\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control,\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n  margin-left: calc(-1 * var(--bs-border-width));\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.input-group > .form-floating:not(:first-child) > .form-control,\n.input-group > .form-floating:not(:first-child) > .form-select {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n  display: none;\n  width: 100%;\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-form-valid-color);\n}\n\n.valid-tooltip {\n  position: absolute;\n  top: 100%;\n  z-index: 5;\n  display: none;\n  max-width: 100%;\n  padding: 0.25rem 0.5rem;\n  margin-top: 0.1rem;\n  font-size: 0.875rem;\n  color: #fff;\n  background-color: var(--bs-success);\n  border-radius: var(--bs-border-radius);\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n  display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n  border-color: var(--bs-form-valid-border-color);\n  padding-right: calc(1.5em + 0.75rem);\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n  border-color: var(--bs-form-valid-border-color);\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n  padding-right: calc(1.5em + 0.75rem);\n  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n  border-color: var(--bs-form-valid-border-color);\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n  --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e\");\n  padding-right: 4.125rem;\n  background-position: right 0.75rem center, center right 2.25rem;\n  background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n  border-color: var(--bs-form-valid-border-color);\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated .form-control-color:valid, .form-control-color.is-valid {\n  width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n  border-color: var(--bs-form-valid-border-color);\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n  background-color: var(--bs-form-valid-color);\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n  color: var(--bs-form-valid-color);\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n  margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid,\n.was-validated .input-group > .form-select:not(:focus):valid,\n.input-group > .form-select:not(:focus).is-valid,\n.was-validated .input-group > .form-floating:not(:focus-within):valid,\n.input-group > .form-floating:not(:focus-within).is-valid {\n  z-index: 3;\n}\n\n.invalid-feedback {\n  display: none;\n  width: 100%;\n  margin-top: 0.25rem;\n  font-size: 0.875em;\n  color: var(--bs-form-invalid-color);\n}\n\n.invalid-tooltip {\n  position: absolute;\n  top: 100%;\n  z-index: 5;\n  display: none;\n  max-width: 100%;\n  padding: 0.25rem 0.5rem;\n  margin-top: 0.1rem;\n  font-size: 0.875rem;\n  color: #fff;\n  background-color: var(--bs-danger);\n  border-radius: var(--bs-border-radius);\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n  display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n  border-color: var(--bs-form-invalid-border-color);\n  padding-right: calc(1.5em + 0.75rem);\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-position: right calc(0.375em + 0.1875rem) center;\n  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n  border-color: var(--bs-form-invalid-border-color);\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n  padding-right: calc(1.5em + 0.75rem);\n  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n  border-color: var(--bs-form-invalid-border-color);\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n  --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n  padding-right: 4.125rem;\n  background-position: right 0.75rem center, center right 2.25rem;\n  background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n  border-color: var(--bs-form-invalid-border-color);\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated .form-control-color:invalid, .form-control-color.is-invalid {\n  width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n  border-color: var(--bs-form-invalid-border-color);\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n  background-color: var(--bs-form-invalid-color);\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n  box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n  color: var(--bs-form-invalid-color);\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n  margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid,\n.was-validated .input-group > .form-select:not(:focus):invalid,\n.input-group > .form-select:not(:focus).is-invalid,\n.was-validated .input-group > .form-floating:not(:focus-within):invalid,\n.input-group > .form-floating:not(:focus-within).is-invalid {\n  z-index: 4;\n}\n\n.btn {\n  --bs-btn-padding-x: 0.75rem;\n  --bs-btn-padding-y: 0.375rem;\n  --bs-btn-font-family: ;\n  --bs-btn-font-size: 1rem;\n  --bs-btn-font-weight: 400;\n  --bs-btn-line-height: 1.5;\n  --bs-btn-color: var(--bs-body-color);\n  --bs-btn-bg: transparent;\n  --bs-btn-border-width: var(--bs-border-width);\n  --bs-btn-border-color: transparent;\n  --bs-btn-border-radius: var(--bs-border-radius);\n  --bs-btn-hover-border-color: transparent;\n  --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n  --bs-btn-disabled-opacity: 0.65;\n  --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);\n  display: inline-block;\n  padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);\n  font-family: var(--bs-btn-font-family);\n  font-size: var(--bs-btn-font-size);\n  font-weight: var(--bs-btn-font-weight);\n  line-height: var(--bs-btn-line-height);\n  color: var(--bs-btn-color);\n  text-align: center;\n  text-decoration: none;\n  vertical-align: middle;\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  user-select: none;\n  border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);\n  border-radius: var(--bs-btn-border-radius);\n  background-color: var(--bs-btn-bg);\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .btn {\n    transition: none;\n  }\n}\n.btn:hover {\n  color: var(--bs-btn-hover-color);\n  background-color: var(--bs-btn-hover-bg);\n  border-color: var(--bs-btn-hover-border-color);\n}\n.btn-check + .btn:hover {\n  color: var(--bs-btn-color);\n  background-color: var(--bs-btn-bg);\n  border-color: var(--bs-btn-border-color);\n}\n.btn:focus-visible {\n  color: var(--bs-btn-hover-color);\n  background-color: var(--bs-btn-hover-bg);\n  border-color: var(--bs-btn-hover-border-color);\n  outline: 0;\n  box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:focus-visible + .btn {\n  border-color: var(--bs-btn-hover-border-color);\n  outline: 0;\n  box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {\n  color: var(--bs-btn-active-color);\n  background-color: var(--bs-btn-active-bg);\n  border-color: var(--bs-btn-active-border-color);\n}\n.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible {\n  box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked:focus-visible + .btn {\n  box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n  color: var(--bs-btn-disabled-color);\n  pointer-events: none;\n  background-color: var(--bs-btn-disabled-bg);\n  border-color: var(--bs-btn-disabled-border-color);\n  opacity: var(--bs-btn-disabled-opacity);\n}\n\n.btn-primary {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #0d6efd;\n  --bs-btn-border-color: #0d6efd;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #0b5ed7;\n  --bs-btn-hover-border-color: #0a58ca;\n  --bs-btn-focus-shadow-rgb: 49, 132, 253;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #0a58ca;\n  --bs-btn-active-border-color: #0a53be;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #0d6efd;\n  --bs-btn-disabled-border-color: #0d6efd;\n}\n\n.btn-secondary {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #6c757d;\n  --bs-btn-border-color: #6c757d;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #5c636a;\n  --bs-btn-hover-border-color: #565e64;\n  --bs-btn-focus-shadow-rgb: 130, 138, 145;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #565e64;\n  --bs-btn-active-border-color: #51585e;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #6c757d;\n  --bs-btn-disabled-border-color: #6c757d;\n}\n\n.btn-success {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #198754;\n  --bs-btn-border-color: #198754;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #157347;\n  --bs-btn-hover-border-color: #146c43;\n  --bs-btn-focus-shadow-rgb: 60, 153, 110;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #146c43;\n  --bs-btn-active-border-color: #13653f;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #198754;\n  --bs-btn-disabled-border-color: #198754;\n}\n\n.btn-info {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #0dcaf0;\n  --bs-btn-border-color: #0dcaf0;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #31d2f2;\n  --bs-btn-hover-border-color: #25cff2;\n  --bs-btn-focus-shadow-rgb: 11, 172, 204;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #3dd5f3;\n  --bs-btn-active-border-color: #25cff2;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #0dcaf0;\n  --bs-btn-disabled-border-color: #0dcaf0;\n}\n\n.btn-warning {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #ffc107;\n  --bs-btn-border-color: #ffc107;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #ffca2c;\n  --bs-btn-hover-border-color: #ffc720;\n  --bs-btn-focus-shadow-rgb: 217, 164, 6;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #ffcd39;\n  --bs-btn-active-border-color: #ffc720;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #ffc107;\n  --bs-btn-disabled-border-color: #ffc107;\n}\n\n.btn-danger {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #dc3545;\n  --bs-btn-border-color: #dc3545;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #bb2d3b;\n  --bs-btn-hover-border-color: #b02a37;\n  --bs-btn-focus-shadow-rgb: 225, 83, 97;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #b02a37;\n  --bs-btn-active-border-color: #a52834;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #dc3545;\n  --bs-btn-disabled-border-color: #dc3545;\n}\n\n.btn-light {\n  --bs-btn-color: #000;\n  --bs-btn-bg: #f8f9fa;\n  --bs-btn-border-color: #f8f9fa;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #d3d4d5;\n  --bs-btn-hover-border-color: #c6c7c8;\n  --bs-btn-focus-shadow-rgb: 211, 212, 213;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #c6c7c8;\n  --bs-btn-active-border-color: #babbbc;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #000;\n  --bs-btn-disabled-bg: #f8f9fa;\n  --bs-btn-disabled-border-color: #f8f9fa;\n}\n\n.btn-dark {\n  --bs-btn-color: #fff;\n  --bs-btn-bg: #212529;\n  --bs-btn-border-color: #212529;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #424649;\n  --bs-btn-hover-border-color: #373b3e;\n  --bs-btn-focus-shadow-rgb: 66, 70, 73;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #4d5154;\n  --bs-btn-active-border-color: #373b3e;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #fff;\n  --bs-btn-disabled-bg: #212529;\n  --bs-btn-disabled-border-color: #212529;\n}\n\n.btn-outline-primary {\n  --bs-btn-color: #0d6efd;\n  --bs-btn-border-color: #0d6efd;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #0d6efd;\n  --bs-btn-hover-border-color: #0d6efd;\n  --bs-btn-focus-shadow-rgb: 13, 110, 253;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #0d6efd;\n  --bs-btn-active-border-color: #0d6efd;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #0d6efd;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #0d6efd;\n  --bs-gradient: none;\n}\n\n.btn-outline-secondary {\n  --bs-btn-color: #6c757d;\n  --bs-btn-border-color: #6c757d;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #6c757d;\n  --bs-btn-hover-border-color: #6c757d;\n  --bs-btn-focus-shadow-rgb: 108, 117, 125;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #6c757d;\n  --bs-btn-active-border-color: #6c757d;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #6c757d;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #6c757d;\n  --bs-gradient: none;\n}\n\n.btn-outline-success {\n  --bs-btn-color: #198754;\n  --bs-btn-border-color: #198754;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #198754;\n  --bs-btn-hover-border-color: #198754;\n  --bs-btn-focus-shadow-rgb: 25, 135, 84;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #198754;\n  --bs-btn-active-border-color: #198754;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #198754;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #198754;\n  --bs-gradient: none;\n}\n\n.btn-outline-info {\n  --bs-btn-color: #0dcaf0;\n  --bs-btn-border-color: #0dcaf0;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #0dcaf0;\n  --bs-btn-hover-border-color: #0dcaf0;\n  --bs-btn-focus-shadow-rgb: 13, 202, 240;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #0dcaf0;\n  --bs-btn-active-border-color: #0dcaf0;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #0dcaf0;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #0dcaf0;\n  --bs-gradient: none;\n}\n\n.btn-outline-warning {\n  --bs-btn-color: #ffc107;\n  --bs-btn-border-color: #ffc107;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #ffc107;\n  --bs-btn-hover-border-color: #ffc107;\n  --bs-btn-focus-shadow-rgb: 255, 193, 7;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #ffc107;\n  --bs-btn-active-border-color: #ffc107;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #ffc107;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #ffc107;\n  --bs-gradient: none;\n}\n\n.btn-outline-danger {\n  --bs-btn-color: #dc3545;\n  --bs-btn-border-color: #dc3545;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #dc3545;\n  --bs-btn-hover-border-color: #dc3545;\n  --bs-btn-focus-shadow-rgb: 220, 53, 69;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #dc3545;\n  --bs-btn-active-border-color: #dc3545;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #dc3545;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #dc3545;\n  --bs-gradient: none;\n}\n\n.btn-outline-light {\n  --bs-btn-color: #f8f9fa;\n  --bs-btn-border-color: #f8f9fa;\n  --bs-btn-hover-color: #000;\n  --bs-btn-hover-bg: #f8f9fa;\n  --bs-btn-hover-border-color: #f8f9fa;\n  --bs-btn-focus-shadow-rgb: 248, 249, 250;\n  --bs-btn-active-color: #000;\n  --bs-btn-active-bg: #f8f9fa;\n  --bs-btn-active-border-color: #f8f9fa;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #f8f9fa;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #f8f9fa;\n  --bs-gradient: none;\n}\n\n.btn-outline-dark {\n  --bs-btn-color: #212529;\n  --bs-btn-border-color: #212529;\n  --bs-btn-hover-color: #fff;\n  --bs-btn-hover-bg: #212529;\n  --bs-btn-hover-border-color: #212529;\n  --bs-btn-focus-shadow-rgb: 33, 37, 41;\n  --bs-btn-active-color: #fff;\n  --bs-btn-active-bg: #212529;\n  --bs-btn-active-border-color: #212529;\n  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  --bs-btn-disabled-color: #212529;\n  --bs-btn-disabled-bg: transparent;\n  --bs-btn-disabled-border-color: #212529;\n  --bs-gradient: none;\n}\n\n.btn-link {\n  --bs-btn-font-weight: 400;\n  --bs-btn-color: var(--bs-link-color);\n  --bs-btn-bg: transparent;\n  --bs-btn-border-color: transparent;\n  --bs-btn-hover-color: var(--bs-link-hover-color);\n  --bs-btn-hover-border-color: transparent;\n  --bs-btn-active-color: var(--bs-link-hover-color);\n  --bs-btn-active-border-color: transparent;\n  --bs-btn-disabled-color: #6c757d;\n  --bs-btn-disabled-border-color: transparent;\n  --bs-btn-box-shadow: 0 0 0 #000;\n  --bs-btn-focus-shadow-rgb: 49, 132, 253;\n  text-decoration: underline;\n}\n.btn-link:focus-visible {\n  color: var(--bs-btn-color);\n}\n.btn-link:hover {\n  color: var(--bs-btn-hover-color);\n}\n\n.btn-lg, .btn-group-lg > .btn {\n  --bs-btn-padding-y: 0.5rem;\n  --bs-btn-padding-x: 1rem;\n  --bs-btn-font-size: 1.25rem;\n  --bs-btn-border-radius: var(--bs-border-radius-lg);\n}\n\n.btn-sm, .btn-group-sm > .btn {\n  --bs-btn-padding-y: 0.25rem;\n  --bs-btn-padding-x: 0.5rem;\n  --bs-btn-font-size: 0.875rem;\n  --bs-btn-border-radius: var(--bs-border-radius-sm);\n}\n\n.fade {\n  transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n  .fade {\n    transition: none;\n  }\n}\n.fade:not(.show) {\n  opacity: 0;\n}\n\n.collapse:not(.show) {\n  display: none;\n}\n\n.collapsing {\n  height: 0;\n  overflow: hidden;\n  transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n  .collapsing {\n    transition: none;\n  }\n}\n.collapsing.collapse-horizontal {\n  width: 0;\n  height: auto;\n  transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n  .collapsing.collapse-horizontal {\n    transition: none;\n  }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart,\n.dropup-center,\n.dropdown-center {\n  position: relative;\n}\n\n.dropdown-toggle {\n  white-space: nowrap;\n}\n.dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0.3em solid;\n  border-right: 0.3em solid transparent;\n  border-bottom: 0;\n  border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n  margin-left: 0;\n}\n\n.dropdown-menu {\n  --bs-dropdown-zindex: 1000;\n  --bs-dropdown-min-width: 10rem;\n  --bs-dropdown-padding-x: 0;\n  --bs-dropdown-padding-y: 0.5rem;\n  --bs-dropdown-spacer: 0.125rem;\n  --bs-dropdown-font-size: 1rem;\n  --bs-dropdown-color: var(--bs-body-color);\n  --bs-dropdown-bg: var(--bs-body-bg);\n  --bs-dropdown-border-color: var(--bs-border-color-translucent);\n  --bs-dropdown-border-radius: var(--bs-border-radius);\n  --bs-dropdown-border-width: var(--bs-border-width);\n  --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));\n  --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n  --bs-dropdown-divider-margin-y: 0.5rem;\n  --bs-dropdown-box-shadow: var(--bs-box-shadow);\n  --bs-dropdown-link-color: var(--bs-body-color);\n  --bs-dropdown-link-hover-color: var(--bs-body-color);\n  --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);\n  --bs-dropdown-link-active-color: #fff;\n  --bs-dropdown-link-active-bg: #0d6efd;\n  --bs-dropdown-link-disabled-color: var(--bs-tertiary-color);\n  --bs-dropdown-item-padding-x: 1rem;\n  --bs-dropdown-item-padding-y: 0.25rem;\n  --bs-dropdown-header-color: #6c757d;\n  --bs-dropdown-header-padding-x: 1rem;\n  --bs-dropdown-header-padding-y: 0.5rem;\n  position: absolute;\n  z-index: var(--bs-dropdown-zindex);\n  display: none;\n  min-width: var(--bs-dropdown-min-width);\n  padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);\n  margin: 0;\n  font-size: var(--bs-dropdown-font-size);\n  color: var(--bs-dropdown-color);\n  text-align: left;\n  list-style: none;\n  background-color: var(--bs-dropdown-bg);\n  background-clip: padding-box;\n  border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);\n  border-radius: var(--bs-dropdown-border-radius);\n}\n.dropdown-menu[data-bs-popper] {\n  top: 100%;\n  left: 0;\n  margin-top: var(--bs-dropdown-spacer);\n}\n\n.dropdown-menu-start {\n  --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n  right: auto;\n  left: 0;\n}\n\n.dropdown-menu-end {\n  --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n  right: 0;\n  left: auto;\n}\n\n@media (min-width: 576px) {\n  .dropdown-menu-sm-start {\n    --bs-position: start;\n  }\n  .dropdown-menu-sm-start[data-bs-popper] {\n    right: auto;\n    left: 0;\n  }\n  .dropdown-menu-sm-end {\n    --bs-position: end;\n  }\n  .dropdown-menu-sm-end[data-bs-popper] {\n    right: 0;\n    left: auto;\n  }\n}\n@media (min-width: 768px) {\n  .dropdown-menu-md-start {\n    --bs-position: start;\n  }\n  .dropdown-menu-md-start[data-bs-popper] {\n    right: auto;\n    left: 0;\n  }\n  .dropdown-menu-md-end {\n    --bs-position: end;\n  }\n  .dropdown-menu-md-end[data-bs-popper] {\n    right: 0;\n    left: auto;\n  }\n}\n@media (min-width: 992px) {\n  .dropdown-menu-lg-start {\n    --bs-position: start;\n  }\n  .dropdown-menu-lg-start[data-bs-popper] {\n    right: auto;\n    left: 0;\n  }\n  .dropdown-menu-lg-end {\n    --bs-position: end;\n  }\n  .dropdown-menu-lg-end[data-bs-popper] {\n    right: 0;\n    left: auto;\n  }\n}\n@media (min-width: 1200px) {\n  .dropdown-menu-xl-start {\n    --bs-position: start;\n  }\n  .dropdown-menu-xl-start[data-bs-popper] {\n    right: auto;\n    left: 0;\n  }\n  .dropdown-menu-xl-end {\n    --bs-position: end;\n  }\n  .dropdown-menu-xl-end[data-bs-popper] {\n    right: 0;\n    left: auto;\n  }\n}\n@media (min-width: 1400px) {\n  .dropdown-menu-xxl-start {\n    --bs-position: start;\n  }\n  .dropdown-menu-xxl-start[data-bs-popper] {\n    right: auto;\n    left: 0;\n  }\n  .dropdown-menu-xxl-end {\n    --bs-position: end;\n  }\n  .dropdown-menu-xxl-end[data-bs-popper] {\n    right: 0;\n    left: auto;\n  }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n  top: auto;\n  bottom: 100%;\n  margin-top: 0;\n  margin-bottom: var(--bs-dropdown-spacer);\n}\n.dropup .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0;\n  border-right: 0.3em solid transparent;\n  border-bottom: 0.3em solid;\n  border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n  margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n  top: 0;\n  right: auto;\n  left: 100%;\n  margin-top: 0;\n  margin-left: var(--bs-dropdown-spacer);\n}\n.dropend .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0.3em solid transparent;\n  border-right: 0;\n  border-bottom: 0.3em solid transparent;\n  border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n  margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n  vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n  top: 0;\n  right: 100%;\n  left: auto;\n  margin-top: 0;\n  margin-right: var(--bs-dropdown-spacer);\n}\n.dropstart .dropdown-toggle::after {\n  display: inline-block;\n  margin-left: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n  display: none;\n}\n.dropstart .dropdown-toggle::before {\n  display: inline-block;\n  margin-right: 0.255em;\n  vertical-align: 0.255em;\n  content: \"\";\n  border-top: 0.3em solid transparent;\n  border-right: 0.3em solid;\n  border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n  margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n  vertical-align: 0;\n}\n\n.dropdown-divider {\n  height: 0;\n  margin: var(--bs-dropdown-divider-margin-y) 0;\n  overflow: hidden;\n  border-top: 1px solid var(--bs-dropdown-divider-bg);\n  opacity: 1;\n}\n\n.dropdown-item {\n  display: block;\n  width: 100%;\n  padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n  clear: both;\n  font-weight: 400;\n  color: var(--bs-dropdown-link-color);\n  text-align: inherit;\n  text-decoration: none;\n  white-space: nowrap;\n  background-color: transparent;\n  border: 0;\n  border-radius: var(--bs-dropdown-item-border-radius, 0);\n}\n.dropdown-item:hover, .dropdown-item:focus {\n  color: var(--bs-dropdown-link-hover-color);\n  background-color: var(--bs-dropdown-link-hover-bg);\n}\n.dropdown-item.active, .dropdown-item:active {\n  color: var(--bs-dropdown-link-active-color);\n  text-decoration: none;\n  background-color: var(--bs-dropdown-link-active-bg);\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n  color: var(--bs-dropdown-link-disabled-color);\n  pointer-events: none;\n  background-color: transparent;\n}\n\n.dropdown-menu.show {\n  display: block;\n}\n\n.dropdown-header {\n  display: block;\n  padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);\n  margin-bottom: 0;\n  font-size: 0.875rem;\n  color: var(--bs-dropdown-header-color);\n  white-space: nowrap;\n}\n\n.dropdown-item-text {\n  display: block;\n  padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n  color: var(--bs-dropdown-link-color);\n}\n\n.dropdown-menu-dark {\n  --bs-dropdown-color: #dee2e6;\n  --bs-dropdown-bg: #343a40;\n  --bs-dropdown-border-color: var(--bs-border-color-translucent);\n  --bs-dropdown-box-shadow: ;\n  --bs-dropdown-link-color: #dee2e6;\n  --bs-dropdown-link-hover-color: #fff;\n  --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n  --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);\n  --bs-dropdown-link-active-color: #fff;\n  --bs-dropdown-link-active-bg: #0d6efd;\n  --bs-dropdown-link-disabled-color: #adb5bd;\n  --bs-dropdown-header-color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-flex;\n  vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n  position: relative;\n  flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n  z-index: 1;\n}\n\n.btn-toolbar {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n  width: auto;\n}\n\n.btn-group {\n  border-radius: var(--bs-border-radius);\n}\n.btn-group > :not(.btn-check:first-child) + .btn,\n.btn-group > .btn-group:not(:first-child) {\n  margin-left: calc(-1 * var(--bs-border-width));\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn.dropdown-toggle-split:first-child,\n.btn-group > .btn-group:not(:last-child) > .btn {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n  padding-right: 0.5625rem;\n  padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n  margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n  margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n  padding-right: 0.375rem;\n  padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n  padding-right: 0.75rem;\n  padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n  width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n  margin-top: calc(-1 * var(--bs-border-width));\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:nth-child(n+3),\n.btn-group-vertical > :not(.btn-check) + .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n\n.nav {\n  --bs-nav-link-padding-x: 1rem;\n  --bs-nav-link-padding-y: 0.5rem;\n  --bs-nav-link-font-weight: ;\n  --bs-nav-link-color: var(--bs-link-color);\n  --bs-nav-link-hover-color: var(--bs-link-hover-color);\n  --bs-nav-link-disabled-color: var(--bs-secondary-color);\n  display: flex;\n  flex-wrap: wrap;\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none;\n}\n\n.nav-link {\n  display: block;\n  padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);\n  font-size: var(--bs-nav-link-font-size);\n  font-weight: var(--bs-nav-link-font-weight);\n  color: var(--bs-nav-link-color);\n  text-decoration: none;\n  background: none;\n  border: 0;\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .nav-link {\n    transition: none;\n  }\n}\n.nav-link:hover, .nav-link:focus {\n  color: var(--bs-nav-link-hover-color);\n}\n.nav-link:focus-visible {\n  outline: 0;\n  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.nav-link.disabled, .nav-link:disabled {\n  color: var(--bs-nav-link-disabled-color);\n  pointer-events: none;\n  cursor: default;\n}\n\n.nav-tabs {\n  --bs-nav-tabs-border-width: var(--bs-border-width);\n  --bs-nav-tabs-border-color: var(--bs-border-color);\n  --bs-nav-tabs-border-radius: var(--bs-border-radius);\n  --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);\n  --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);\n  --bs-nav-tabs-link-active-bg: var(--bs-body-bg);\n  --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);\n  border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color);\n}\n.nav-tabs .nav-link {\n  margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));\n  border: var(--bs-nav-tabs-border-width) solid transparent;\n  border-top-left-radius: var(--bs-nav-tabs-border-radius);\n  border-top-right-radius: var(--bs-nav-tabs-border-radius);\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n  isolation: isolate;\n  border-color: var(--bs-nav-tabs-link-hover-border-color);\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n  color: var(--bs-nav-tabs-link-active-color);\n  background-color: var(--bs-nav-tabs-link-active-bg);\n  border-color: var(--bs-nav-tabs-link-active-border-color);\n}\n.nav-tabs .dropdown-menu {\n  margin-top: calc(-1 * var(--bs-nav-tabs-border-width));\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n\n.nav-pills {\n  --bs-nav-pills-border-radius: var(--bs-border-radius);\n  --bs-nav-pills-link-active-color: #fff;\n  --bs-nav-pills-link-active-bg: #0d6efd;\n}\n.nav-pills .nav-link {\n  border-radius: var(--bs-nav-pills-border-radius);\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n  color: var(--bs-nav-pills-link-active-color);\n  background-color: var(--bs-nav-pills-link-active-bg);\n}\n\n.nav-underline {\n  --bs-nav-underline-gap: 1rem;\n  --bs-nav-underline-border-width: 0.125rem;\n  --bs-nav-underline-link-active-color: var(--bs-emphasis-color);\n  gap: var(--bs-nav-underline-gap);\n}\n.nav-underline .nav-link {\n  padding-right: 0;\n  padding-left: 0;\n  border-bottom: var(--bs-nav-underline-border-width) solid transparent;\n}\n.nav-underline .nav-link:hover, .nav-underline .nav-link:focus {\n  border-bottom-color: currentcolor;\n}\n.nav-underline .nav-link.active,\n.nav-underline .show > .nav-link {\n  font-weight: 700;\n  color: var(--bs-nav-underline-link-active-color);\n  border-bottom-color: currentcolor;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n  flex: 1 1 auto;\n  text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n  flex-grow: 1;\n  flex-basis: 0;\n  text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n  width: 100%;\n}\n\n.tab-content > .tab-pane {\n  display: none;\n}\n.tab-content > .active {\n  display: block;\n}\n\n.navbar {\n  --bs-navbar-padding-x: 0;\n  --bs-navbar-padding-y: 0.5rem;\n  --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n  --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);\n  --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);\n  --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-brand-padding-y: 0.3125rem;\n  --bs-navbar-brand-margin-end: 1rem;\n  --bs-navbar-brand-font-size: 1.25rem;\n  --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);\n  --bs-navbar-nav-link-padding-x: 0.5rem;\n  --bs-navbar-toggler-padding-y: 0.25rem;\n  --bs-navbar-toggler-padding-x: 0.75rem;\n  --bs-navbar-toggler-font-size: 1.25rem;\n  --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n  --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);\n  --bs-navbar-toggler-border-radius: var(--bs-border-radius);\n  --bs-navbar-toggler-focus-width: 0.25rem;\n  --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;\n  position: relative;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: space-between;\n  padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n  display: flex;\n  flex-wrap: inherit;\n  align-items: center;\n  justify-content: space-between;\n}\n.navbar-brand {\n  padding-top: var(--bs-navbar-brand-padding-y);\n  padding-bottom: var(--bs-navbar-brand-padding-y);\n  margin-right: var(--bs-navbar-brand-margin-end);\n  font-size: var(--bs-navbar-brand-font-size);\n  color: var(--bs-navbar-brand-color);\n  text-decoration: none;\n  white-space: nowrap;\n}\n.navbar-brand:hover, .navbar-brand:focus {\n  color: var(--bs-navbar-brand-hover-color);\n}\n\n.navbar-nav {\n  --bs-nav-link-padding-x: 0;\n  --bs-nav-link-padding-y: 0.5rem;\n  --bs-nav-link-font-weight: ;\n  --bs-nav-link-color: var(--bs-navbar-color);\n  --bs-nav-link-hover-color: var(--bs-navbar-hover-color);\n  --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);\n  display: flex;\n  flex-direction: column;\n  padding-left: 0;\n  margin-bottom: 0;\n  list-style: none;\n}\n.navbar-nav .nav-link.active, .navbar-nav .nav-link.show {\n  color: var(--bs-navbar-active-color);\n}\n.navbar-nav .dropdown-menu {\n  position: static;\n}\n\n.navbar-text {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  color: var(--bs-navbar-color);\n}\n.navbar-text a,\n.navbar-text a:hover,\n.navbar-text a:focus {\n  color: var(--bs-navbar-active-color);\n}\n\n.navbar-collapse {\n  flex-grow: 1;\n  flex-basis: 100%;\n  align-items: center;\n}\n\n.navbar-toggler {\n  padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);\n  font-size: var(--bs-navbar-toggler-font-size);\n  line-height: 1;\n  color: var(--bs-navbar-color);\n  background-color: transparent;\n  border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);\n  border-radius: var(--bs-navbar-toggler-border-radius);\n  transition: var(--bs-navbar-toggler-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n  .navbar-toggler {\n    transition: none;\n  }\n}\n.navbar-toggler:hover {\n  text-decoration: none;\n}\n.navbar-toggler:focus {\n  text-decoration: none;\n  outline: 0;\n  box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);\n}\n\n.navbar-toggler-icon {\n  display: inline-block;\n  width: 1.5em;\n  height: 1.5em;\n  vertical-align: middle;\n  background-image: var(--bs-navbar-toggler-icon-bg);\n  background-repeat: no-repeat;\n  background-position: center;\n  background-size: 100%;\n}\n\n.navbar-nav-scroll {\n  max-height: var(--bs-scroll-height, 75vh);\n  overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n  .navbar-expand-sm {\n    flex-wrap: nowrap;\n    justify-content: flex-start;\n  }\n  .navbar-expand-sm .navbar-nav {\n    flex-direction: row;\n  }\n  .navbar-expand-sm .navbar-nav .dropdown-menu {\n    position: absolute;\n  }\n  .navbar-expand-sm .navbar-nav .nav-link {\n    padding-right: var(--bs-navbar-nav-link-padding-x);\n    padding-left: var(--bs-navbar-nav-link-padding-x);\n  }\n  .navbar-expand-sm .navbar-nav-scroll {\n    overflow: visible;\n  }\n  .navbar-expand-sm .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto;\n  }\n  .navbar-expand-sm .navbar-toggler {\n    display: none;\n  }\n  .navbar-expand-sm .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none;\n  }\n  .navbar-expand-sm .offcanvas .offcanvas-header {\n    display: none;\n  }\n  .navbar-expand-sm .offcanvas .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n  }\n}\n@media (min-width: 768px) {\n  .navbar-expand-md {\n    flex-wrap: nowrap;\n    justify-content: flex-start;\n  }\n  .navbar-expand-md .navbar-nav {\n    flex-direction: row;\n  }\n  .navbar-expand-md .navbar-nav .dropdown-menu {\n    position: absolute;\n  }\n  .navbar-expand-md .navbar-nav .nav-link {\n    padding-right: var(--bs-navbar-nav-link-padding-x);\n    padding-left: var(--bs-navbar-nav-link-padding-x);\n  }\n  .navbar-expand-md .navbar-nav-scroll {\n    overflow: visible;\n  }\n  .navbar-expand-md .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto;\n  }\n  .navbar-expand-md .navbar-toggler {\n    display: none;\n  }\n  .navbar-expand-md .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none;\n  }\n  .navbar-expand-md .offcanvas .offcanvas-header {\n    display: none;\n  }\n  .navbar-expand-md .offcanvas .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n  }\n}\n@media (min-width: 992px) {\n  .navbar-expand-lg {\n    flex-wrap: nowrap;\n    justify-content: flex-start;\n  }\n  .navbar-expand-lg .navbar-nav {\n    flex-direction: row;\n  }\n  .navbar-expand-lg .navbar-nav .dropdown-menu {\n    position: absolute;\n  }\n  .navbar-expand-lg .navbar-nav .nav-link {\n    padding-right: var(--bs-navbar-nav-link-padding-x);\n    padding-left: var(--bs-navbar-nav-link-padding-x);\n  }\n  .navbar-expand-lg .navbar-nav-scroll {\n    overflow: visible;\n  }\n  .navbar-expand-lg .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto;\n  }\n  .navbar-expand-lg .navbar-toggler {\n    display: none;\n  }\n  .navbar-expand-lg .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none;\n  }\n  .navbar-expand-lg .offcanvas .offcanvas-header {\n    display: none;\n  }\n  .navbar-expand-lg .offcanvas .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n  }\n}\n@media (min-width: 1200px) {\n  .navbar-expand-xl {\n    flex-wrap: nowrap;\n    justify-content: flex-start;\n  }\n  .navbar-expand-xl .navbar-nav {\n    flex-direction: row;\n  }\n  .navbar-expand-xl .navbar-nav .dropdown-menu {\n    position: absolute;\n  }\n  .navbar-expand-xl .navbar-nav .nav-link {\n    padding-right: var(--bs-navbar-nav-link-padding-x);\n    padding-left: var(--bs-navbar-nav-link-padding-x);\n  }\n  .navbar-expand-xl .navbar-nav-scroll {\n    overflow: visible;\n  }\n  .navbar-expand-xl .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto;\n  }\n  .navbar-expand-xl .navbar-toggler {\n    display: none;\n  }\n  .navbar-expand-xl .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none;\n  }\n  .navbar-expand-xl .offcanvas .offcanvas-header {\n    display: none;\n  }\n  .navbar-expand-xl .offcanvas .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n  }\n}\n@media (min-width: 1400px) {\n  .navbar-expand-xxl {\n    flex-wrap: nowrap;\n    justify-content: flex-start;\n  }\n  .navbar-expand-xxl .navbar-nav {\n    flex-direction: row;\n  }\n  .navbar-expand-xxl .navbar-nav .dropdown-menu {\n    position: absolute;\n  }\n  .navbar-expand-xxl .navbar-nav .nav-link {\n    padding-right: var(--bs-navbar-nav-link-padding-x);\n    padding-left: var(--bs-navbar-nav-link-padding-x);\n  }\n  .navbar-expand-xxl .navbar-nav-scroll {\n    overflow: visible;\n  }\n  .navbar-expand-xxl .navbar-collapse {\n    display: flex !important;\n    flex-basis: auto;\n  }\n  .navbar-expand-xxl .navbar-toggler {\n    display: none;\n  }\n  .navbar-expand-xxl .offcanvas {\n    position: static;\n    z-index: auto;\n    flex-grow: 1;\n    width: auto !important;\n    height: auto !important;\n    visibility: visible !important;\n    background-color: transparent !important;\n    border: 0 !important;\n    transform: none !important;\n    transition: none;\n  }\n  .navbar-expand-xxl .offcanvas .offcanvas-header {\n    display: none;\n  }\n  .navbar-expand-xxl .offcanvas .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n  }\n}\n.navbar-expand {\n  flex-wrap: nowrap;\n  justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n  flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n  position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n  padding-right: var(--bs-navbar-nav-link-padding-x);\n  padding-left: var(--bs-navbar-nav-link-padding-x);\n}\n.navbar-expand .navbar-nav-scroll {\n  overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n  display: flex !important;\n  flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n  display: none;\n}\n.navbar-expand .offcanvas {\n  position: static;\n  z-index: auto;\n  flex-grow: 1;\n  width: auto !important;\n  height: auto !important;\n  visibility: visible !important;\n  background-color: transparent !important;\n  border: 0 !important;\n  transform: none !important;\n  transition: none;\n}\n.navbar-expand .offcanvas .offcanvas-header {\n  display: none;\n}\n.navbar-expand .offcanvas .offcanvas-body {\n  display: flex;\n  flex-grow: 0;\n  padding: 0;\n  overflow-y: visible;\n}\n\n.navbar-dark,\n.navbar[data-bs-theme=dark] {\n  --bs-navbar-color: rgba(255, 255, 255, 0.55);\n  --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);\n  --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n  --bs-navbar-active-color: #fff;\n  --bs-navbar-brand-color: #fff;\n  --bs-navbar-brand-hover-color: #fff;\n  --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);\n  --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n[data-bs-theme=dark] .navbar-toggler-icon {\n  --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.card {\n  --bs-card-spacer-y: 1rem;\n  --bs-card-spacer-x: 1rem;\n  --bs-card-title-spacer-y: 0.5rem;\n  --bs-card-title-color: ;\n  --bs-card-subtitle-color: ;\n  --bs-card-border-width: var(--bs-border-width);\n  --bs-card-border-color: var(--bs-border-color-translucent);\n  --bs-card-border-radius: var(--bs-border-radius);\n  --bs-card-box-shadow: ;\n  --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n  --bs-card-cap-padding-y: 0.5rem;\n  --bs-card-cap-padding-x: 1rem;\n  --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);\n  --bs-card-cap-color: ;\n  --bs-card-height: ;\n  --bs-card-color: ;\n  --bs-card-bg: var(--bs-body-bg);\n  --bs-card-img-overlay-padding: 1rem;\n  --bs-card-group-margin: 0.75rem;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  min-width: 0;\n  height: var(--bs-card-height);\n  color: var(--bs-body-color);\n  word-wrap: break-word;\n  background-color: var(--bs-card-bg);\n  background-clip: border-box;\n  border: var(--bs-card-border-width) solid var(--bs-card-border-color);\n  border-radius: var(--bs-card-border-radius);\n}\n.card > hr {\n  margin-right: 0;\n  margin-left: 0;\n}\n.card > .list-group {\n  border-top: inherit;\n  border-bottom: inherit;\n}\n.card > .list-group:first-child {\n  border-top-width: 0;\n  border-top-left-radius: var(--bs-card-inner-border-radius);\n  border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n.card > .list-group:last-child {\n  border-bottom-width: 0;\n  border-bottom-right-radius: var(--bs-card-inner-border-radius);\n  border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n  border-top: 0;\n}\n\n.card-body {\n  flex: 1 1 auto;\n  padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);\n  color: var(--bs-card-color);\n}\n\n.card-title {\n  margin-bottom: var(--bs-card-title-spacer-y);\n  color: var(--bs-card-title-color);\n}\n\n.card-subtitle {\n  margin-top: calc(-0.5 * var(--bs-card-title-spacer-y));\n  margin-bottom: 0;\n  color: var(--bs-card-subtitle-color);\n}\n\n.card-text:last-child {\n  margin-bottom: 0;\n}\n\n.card-link + .card-link {\n  margin-left: var(--bs-card-spacer-x);\n}\n\n.card-header {\n  padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n  margin-bottom: 0;\n  color: var(--bs-card-cap-color);\n  background-color: var(--bs-card-cap-bg);\n  border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-header:first-child {\n  border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0;\n}\n\n.card-footer {\n  padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n  color: var(--bs-card-cap-color);\n  background-color: var(--bs-card-cap-bg);\n  border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-footer:last-child {\n  border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius);\n}\n\n.card-header-tabs {\n  margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n  margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));\n  margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n  border-bottom: 0;\n}\n.card-header-tabs .nav-link.active {\n  background-color: var(--bs-card-bg);\n  border-bottom-color: var(--bs-card-bg);\n}\n\n.card-header-pills {\n  margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n  margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n}\n\n.card-img-overlay {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  padding: var(--bs-card-img-overlay-padding);\n  border-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n  width: 100%;\n}\n\n.card-img,\n.card-img-top {\n  border-top-left-radius: var(--bs-card-inner-border-radius);\n  border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n  border-bottom-right-radius: var(--bs-card-inner-border-radius);\n  border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-group > .card {\n  margin-bottom: var(--bs-card-group-margin);\n}\n@media (min-width: 576px) {\n  .card-group {\n    display: flex;\n    flex-flow: row wrap;\n  }\n  .card-group > .card {\n    flex: 1 0 0;\n    margin-bottom: 0;\n  }\n  .card-group > .card + .card {\n    margin-left: 0;\n    border-left: 0;\n  }\n  .card-group > .card:not(:last-child) {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n  }\n  .card-group > .card:not(:last-child) > .card-img-top,\n  .card-group > .card:not(:last-child) > .card-header {\n    border-top-right-radius: 0;\n  }\n  .card-group > .card:not(:last-child) > .card-img-bottom,\n  .card-group > .card:not(:last-child) > .card-footer {\n    border-bottom-right-radius: 0;\n  }\n  .card-group > .card:not(:first-child) {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n  }\n  .card-group > .card:not(:first-child) > .card-img-top,\n  .card-group > .card:not(:first-child) > .card-header {\n    border-top-left-radius: 0;\n  }\n  .card-group > .card:not(:first-child) > .card-img-bottom,\n  .card-group > .card:not(:first-child) > .card-footer {\n    border-bottom-left-radius: 0;\n  }\n}\n\n.accordion {\n  --bs-accordion-color: var(--bs-body-color);\n  --bs-accordion-bg: var(--bs-body-bg);\n  --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n  --bs-accordion-border-color: var(--bs-border-color);\n  --bs-accordion-border-width: var(--bs-border-width);\n  --bs-accordion-border-radius: var(--bs-border-radius);\n  --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n  --bs-accordion-btn-padding-x: 1.25rem;\n  --bs-accordion-btn-padding-y: 1rem;\n  --bs-accordion-btn-color: var(--bs-body-color);\n  --bs-accordion-btn-bg: var(--bs-accordion-bg);\n  --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-icon-width: 1.25rem;\n  --bs-accordion-btn-icon-transform: rotate(-180deg);\n  --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;\n  --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n  --bs-accordion-body-padding-x: 1.25rem;\n  --bs-accordion-body-padding-y: 1rem;\n  --bs-accordion-active-color: var(--bs-primary-text-emphasis);\n  --bs-accordion-active-bg: var(--bs-primary-bg-subtle);\n}\n\n.accordion-button {\n  position: relative;\n  display: flex;\n  align-items: center;\n  width: 100%;\n  padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);\n  font-size: 1rem;\n  color: var(--bs-accordion-btn-color);\n  text-align: left;\n  background-color: var(--bs-accordion-btn-bg);\n  border: 0;\n  border-radius: 0;\n  overflow-anchor: none;\n  transition: var(--bs-accordion-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n  .accordion-button {\n    transition: none;\n  }\n}\n.accordion-button:not(.collapsed) {\n  color: var(--bs-accordion-active-color);\n  background-color: var(--bs-accordion-active-bg);\n  box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color);\n}\n.accordion-button:not(.collapsed)::after {\n  background-image: var(--bs-accordion-btn-active-icon);\n  transform: var(--bs-accordion-btn-icon-transform);\n}\n.accordion-button::after {\n  flex-shrink: 0;\n  width: var(--bs-accordion-btn-icon-width);\n  height: var(--bs-accordion-btn-icon-width);\n  margin-left: auto;\n  content: \"\";\n  background-image: var(--bs-accordion-btn-icon);\n  background-repeat: no-repeat;\n  background-size: var(--bs-accordion-btn-icon-width);\n  transition: var(--bs-accordion-btn-icon-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n  .accordion-button::after {\n    transition: none;\n  }\n}\n.accordion-button:hover {\n  z-index: 2;\n}\n.accordion-button:focus {\n  z-index: 3;\n  outline: 0;\n  box-shadow: var(--bs-accordion-btn-focus-box-shadow);\n}\n\n.accordion-header {\n  margin-bottom: 0;\n}\n\n.accordion-item {\n  color: var(--bs-accordion-color);\n  background-color: var(--bs-accordion-bg);\n  border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color);\n}\n.accordion-item:first-of-type {\n  border-top-left-radius: var(--bs-accordion-border-radius);\n  border-top-right-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:first-of-type > .accordion-header .accordion-button {\n  border-top-left-radius: var(--bs-accordion-inner-border-radius);\n  border-top-right-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:not(:first-of-type) {\n  border-top: 0;\n}\n.accordion-item:last-of-type {\n  border-bottom-right-radius: var(--bs-accordion-border-radius);\n  border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed {\n  border-bottom-right-radius: var(--bs-accordion-inner-border-radius);\n  border-bottom-left-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:last-of-type > .accordion-collapse {\n  border-bottom-right-radius: var(--bs-accordion-border-radius);\n  border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n\n.accordion-body {\n  padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x);\n}\n\n.accordion-flush > .accordion-item {\n  border-right: 0;\n  border-left: 0;\n  border-radius: 0;\n}\n.accordion-flush > .accordion-item:first-child {\n  border-top: 0;\n}\n.accordion-flush > .accordion-item:last-child {\n  border-bottom: 0;\n}\n.accordion-flush > .accordion-item > .accordion-collapse,\n.accordion-flush > .accordion-item > .accordion-header .accordion-button,\n.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {\n  border-radius: 0;\n}\n\n[data-bs-theme=dark] .accordion-button::after {\n  --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\");\n  --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\");\n}\n\n.breadcrumb {\n  --bs-breadcrumb-padding-x: 0;\n  --bs-breadcrumb-padding-y: 0;\n  --bs-breadcrumb-margin-bottom: 1rem;\n  --bs-breadcrumb-bg: ;\n  --bs-breadcrumb-border-radius: ;\n  --bs-breadcrumb-divider-color: var(--bs-secondary-color);\n  --bs-breadcrumb-item-padding-x: 0.5rem;\n  --bs-breadcrumb-item-active-color: var(--bs-secondary-color);\n  display: flex;\n  flex-wrap: wrap;\n  padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);\n  margin-bottom: var(--bs-breadcrumb-margin-bottom);\n  font-size: var(--bs-breadcrumb-font-size);\n  list-style: none;\n  background-color: var(--bs-breadcrumb-bg);\n  border-radius: var(--bs-breadcrumb-border-radius);\n}\n\n.breadcrumb-item + .breadcrumb-item {\n  padding-left: var(--bs-breadcrumb-item-padding-x);\n}\n.breadcrumb-item + .breadcrumb-item::before {\n  float: left;\n  padding-right: var(--bs-breadcrumb-item-padding-x);\n  color: var(--bs-breadcrumb-divider-color);\n  content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n  color: var(--bs-breadcrumb-item-active-color);\n}\n\n.pagination {\n  --bs-pagination-padding-x: 0.75rem;\n  --bs-pagination-padding-y: 0.375rem;\n  --bs-pagination-font-size: 1rem;\n  --bs-pagination-color: var(--bs-link-color);\n  --bs-pagination-bg: var(--bs-body-bg);\n  --bs-pagination-border-width: var(--bs-border-width);\n  --bs-pagination-border-color: var(--bs-border-color);\n  --bs-pagination-border-radius: var(--bs-border-radius);\n  --bs-pagination-hover-color: var(--bs-link-hover-color);\n  --bs-pagination-hover-bg: var(--bs-tertiary-bg);\n  --bs-pagination-hover-border-color: var(--bs-border-color);\n  --bs-pagination-focus-color: var(--bs-link-hover-color);\n  --bs-pagination-focus-bg: var(--bs-secondary-bg);\n  --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n  --bs-pagination-active-color: #fff;\n  --bs-pagination-active-bg: #0d6efd;\n  --bs-pagination-active-border-color: #0d6efd;\n  --bs-pagination-disabled-color: var(--bs-secondary-color);\n  --bs-pagination-disabled-bg: var(--bs-secondary-bg);\n  --bs-pagination-disabled-border-color: var(--bs-border-color);\n  display: flex;\n  padding-left: 0;\n  list-style: none;\n}\n\n.page-link {\n  position: relative;\n  display: block;\n  padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);\n  font-size: var(--bs-pagination-font-size);\n  color: var(--bs-pagination-color);\n  text-decoration: none;\n  background-color: var(--bs-pagination-bg);\n  border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);\n  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .page-link {\n    transition: none;\n  }\n}\n.page-link:hover {\n  z-index: 2;\n  color: var(--bs-pagination-hover-color);\n  background-color: var(--bs-pagination-hover-bg);\n  border-color: var(--bs-pagination-hover-border-color);\n}\n.page-link:focus {\n  z-index: 3;\n  color: var(--bs-pagination-focus-color);\n  background-color: var(--bs-pagination-focus-bg);\n  outline: 0;\n  box-shadow: var(--bs-pagination-focus-box-shadow);\n}\n.page-link.active, .active > .page-link {\n  z-index: 3;\n  color: var(--bs-pagination-active-color);\n  background-color: var(--bs-pagination-active-bg);\n  border-color: var(--bs-pagination-active-border-color);\n}\n.page-link.disabled, .disabled > .page-link {\n  color: var(--bs-pagination-disabled-color);\n  pointer-events: none;\n  background-color: var(--bs-pagination-disabled-bg);\n  border-color: var(--bs-pagination-disabled-border-color);\n}\n\n.page-item:not(:first-child) .page-link {\n  margin-left: calc(-1 * var(--bs-border-width));\n}\n.page-item:first-child .page-link {\n  border-top-left-radius: var(--bs-pagination-border-radius);\n  border-bottom-left-radius: var(--bs-pagination-border-radius);\n}\n.page-item:last-child .page-link {\n  border-top-right-radius: var(--bs-pagination-border-radius);\n  border-bottom-right-radius: var(--bs-pagination-border-radius);\n}\n\n.pagination-lg {\n  --bs-pagination-padding-x: 1.5rem;\n  --bs-pagination-padding-y: 0.75rem;\n  --bs-pagination-font-size: 1.25rem;\n  --bs-pagination-border-radius: var(--bs-border-radius-lg);\n}\n\n.pagination-sm {\n  --bs-pagination-padding-x: 0.5rem;\n  --bs-pagination-padding-y: 0.25rem;\n  --bs-pagination-font-size: 0.875rem;\n  --bs-pagination-border-radius: var(--bs-border-radius-sm);\n}\n\n.badge {\n  --bs-badge-padding-x: 0.65em;\n  --bs-badge-padding-y: 0.35em;\n  --bs-badge-font-size: 0.75em;\n  --bs-badge-font-weight: 700;\n  --bs-badge-color: #fff;\n  --bs-badge-border-radius: var(--bs-border-radius);\n  display: inline-block;\n  padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);\n  font-size: var(--bs-badge-font-size);\n  font-weight: var(--bs-badge-font-weight);\n  line-height: 1;\n  color: var(--bs-badge-color);\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: var(--bs-badge-border-radius);\n}\n.badge:empty {\n  display: none;\n}\n\n.btn .badge {\n  position: relative;\n  top: -1px;\n}\n\n.alert {\n  --bs-alert-bg: transparent;\n  --bs-alert-padding-x: 1rem;\n  --bs-alert-padding-y: 1rem;\n  --bs-alert-margin-bottom: 1rem;\n  --bs-alert-color: inherit;\n  --bs-alert-border-color: transparent;\n  --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);\n  --bs-alert-border-radius: var(--bs-border-radius);\n  --bs-alert-link-color: inherit;\n  position: relative;\n  padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);\n  margin-bottom: var(--bs-alert-margin-bottom);\n  color: var(--bs-alert-color);\n  background-color: var(--bs-alert-bg);\n  border: var(--bs-alert-border);\n  border-radius: var(--bs-alert-border-radius);\n}\n\n.alert-heading {\n  color: inherit;\n}\n\n.alert-link {\n  font-weight: 700;\n  color: var(--bs-alert-link-color);\n}\n\n.alert-dismissible {\n  padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 2;\n  padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n  --bs-alert-color: var(--bs-primary-text-emphasis);\n  --bs-alert-bg: var(--bs-primary-bg-subtle);\n  --bs-alert-border-color: var(--bs-primary-border-subtle);\n  --bs-alert-link-color: var(--bs-primary-text-emphasis);\n}\n\n.alert-secondary {\n  --bs-alert-color: var(--bs-secondary-text-emphasis);\n  --bs-alert-bg: var(--bs-secondary-bg-subtle);\n  --bs-alert-border-color: var(--bs-secondary-border-subtle);\n  --bs-alert-link-color: var(--bs-secondary-text-emphasis);\n}\n\n.alert-success {\n  --bs-alert-color: var(--bs-success-text-emphasis);\n  --bs-alert-bg: var(--bs-success-bg-subtle);\n  --bs-alert-border-color: var(--bs-success-border-subtle);\n  --bs-alert-link-color: var(--bs-success-text-emphasis);\n}\n\n.alert-info {\n  --bs-alert-color: var(--bs-info-text-emphasis);\n  --bs-alert-bg: var(--bs-info-bg-subtle);\n  --bs-alert-border-color: var(--bs-info-border-subtle);\n  --bs-alert-link-color: var(--bs-info-text-emphasis);\n}\n\n.alert-warning {\n  --bs-alert-color: var(--bs-warning-text-emphasis);\n  --bs-alert-bg: var(--bs-warning-bg-subtle);\n  --bs-alert-border-color: var(--bs-warning-border-subtle);\n  --bs-alert-link-color: var(--bs-warning-text-emphasis);\n}\n\n.alert-danger {\n  --bs-alert-color: var(--bs-danger-text-emphasis);\n  --bs-alert-bg: var(--bs-danger-bg-subtle);\n  --bs-alert-border-color: var(--bs-danger-border-subtle);\n  --bs-alert-link-color: var(--bs-danger-text-emphasis);\n}\n\n.alert-light {\n  --bs-alert-color: var(--bs-light-text-emphasis);\n  --bs-alert-bg: var(--bs-light-bg-subtle);\n  --bs-alert-border-color: var(--bs-light-border-subtle);\n  --bs-alert-link-color: var(--bs-light-text-emphasis);\n}\n\n.alert-dark {\n  --bs-alert-color: var(--bs-dark-text-emphasis);\n  --bs-alert-bg: var(--bs-dark-bg-subtle);\n  --bs-alert-border-color: var(--bs-dark-border-subtle);\n  --bs-alert-link-color: var(--bs-dark-text-emphasis);\n}\n\n@keyframes progress-bar-stripes {\n  0% {\n    background-position-x: var(--bs-progress-height);\n  }\n}\n.progress,\n.progress-stacked {\n  --bs-progress-height: 1rem;\n  --bs-progress-font-size: 0.75rem;\n  --bs-progress-bg: var(--bs-secondary-bg);\n  --bs-progress-border-radius: var(--bs-border-radius);\n  --bs-progress-box-shadow: var(--bs-box-shadow-inset);\n  --bs-progress-bar-color: #fff;\n  --bs-progress-bar-bg: #0d6efd;\n  --bs-progress-bar-transition: width 0.6s ease;\n  display: flex;\n  height: var(--bs-progress-height);\n  overflow: hidden;\n  font-size: var(--bs-progress-font-size);\n  background-color: var(--bs-progress-bg);\n  border-radius: var(--bs-progress-border-radius);\n}\n\n.progress-bar {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  overflow: hidden;\n  color: var(--bs-progress-bar-color);\n  text-align: center;\n  white-space: nowrap;\n  background-color: var(--bs-progress-bar-bg);\n  transition: var(--bs-progress-bar-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n  .progress-bar {\n    transition: none;\n  }\n}\n\n.progress-bar-striped {\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-size: var(--bs-progress-height) var(--bs-progress-height);\n}\n\n.progress-stacked > .progress {\n  overflow: visible;\n}\n\n.progress-stacked > .progress > .progress-bar {\n  width: 100%;\n}\n\n.progress-bar-animated {\n  animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n  .progress-bar-animated {\n    animation: none;\n  }\n}\n\n.list-group {\n  --bs-list-group-color: var(--bs-body-color);\n  --bs-list-group-bg: var(--bs-body-bg);\n  --bs-list-group-border-color: var(--bs-border-color);\n  --bs-list-group-border-width: var(--bs-border-width);\n  --bs-list-group-border-radius: var(--bs-border-radius);\n  --bs-list-group-item-padding-x: 1rem;\n  --bs-list-group-item-padding-y: 0.5rem;\n  --bs-list-group-action-color: var(--bs-secondary-color);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);\n  --bs-list-group-action-active-color: var(--bs-body-color);\n  --bs-list-group-action-active-bg: var(--bs-secondary-bg);\n  --bs-list-group-disabled-color: var(--bs-secondary-color);\n  --bs-list-group-disabled-bg: var(--bs-body-bg);\n  --bs-list-group-active-color: #fff;\n  --bs-list-group-active-bg: #0d6efd;\n  --bs-list-group-active-border-color: #0d6efd;\n  display: flex;\n  flex-direction: column;\n  padding-left: 0;\n  margin-bottom: 0;\n  border-radius: var(--bs-list-group-border-radius);\n}\n\n.list-group-numbered {\n  list-style-type: none;\n  counter-reset: section;\n}\n.list-group-numbered > .list-group-item::before {\n  content: counters(section, \".\") \". \";\n  counter-increment: section;\n}\n\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);\n  color: var(--bs-list-group-color);\n  text-decoration: none;\n  background-color: var(--bs-list-group-bg);\n  border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);\n}\n.list-group-item:first-child {\n  border-top-left-radius: inherit;\n  border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n  border-bottom-right-radius: inherit;\n  border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n  color: var(--bs-list-group-disabled-color);\n  pointer-events: none;\n  background-color: var(--bs-list-group-disabled-bg);\n}\n.list-group-item.active {\n  z-index: 2;\n  color: var(--bs-list-group-active-color);\n  background-color: var(--bs-list-group-active-bg);\n  border-color: var(--bs-list-group-active-border-color);\n}\n.list-group-item + .list-group-item {\n  border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n  margin-top: calc(-1 * var(--bs-list-group-border-width));\n  border-top-width: var(--bs-list-group-border-width);\n}\n\n.list-group-item-action {\n  width: 100%;\n  color: var(--bs-list-group-action-color);\n  text-align: inherit;\n}\n.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {\n  z-index: 1;\n  color: var(--bs-list-group-action-hover-color);\n  text-decoration: none;\n  background-color: var(--bs-list-group-action-hover-bg);\n}\n.list-group-item-action:not(.active):active {\n  color: var(--bs-list-group-action-active-color);\n  background-color: var(--bs-list-group-action-active-bg);\n}\n\n.list-group-horizontal {\n  flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child:not(:last-child) {\n  border-bottom-left-radius: var(--bs-list-group-border-radius);\n  border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child:not(:first-child) {\n  border-top-right-radius: var(--bs-list-group-border-radius);\n  border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n  margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n  border-top-width: var(--bs-list-group-border-width);\n  border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n  margin-left: calc(-1 * var(--bs-list-group-border-width));\n  border-left-width: var(--bs-list-group-border-width);\n}\n\n@media (min-width: 576px) {\n  .list-group-horizontal-sm {\n    flex-direction: row;\n  }\n  .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0;\n  }\n  .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0;\n  }\n  .list-group-horizontal-sm > .list-group-item.active {\n    margin-top: 0;\n  }\n  .list-group-horizontal-sm > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0;\n  }\n  .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n    margin-left: calc(-1 * var(--bs-list-group-border-width));\n    border-left-width: var(--bs-list-group-border-width);\n  }\n}\n@media (min-width: 768px) {\n  .list-group-horizontal-md {\n    flex-direction: row;\n  }\n  .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0;\n  }\n  .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0;\n  }\n  .list-group-horizontal-md > .list-group-item.active {\n    margin-top: 0;\n  }\n  .list-group-horizontal-md > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0;\n  }\n  .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n    margin-left: calc(-1 * var(--bs-list-group-border-width));\n    border-left-width: var(--bs-list-group-border-width);\n  }\n}\n@media (min-width: 992px) {\n  .list-group-horizontal-lg {\n    flex-direction: row;\n  }\n  .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0;\n  }\n  .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0;\n  }\n  .list-group-horizontal-lg > .list-group-item.active {\n    margin-top: 0;\n  }\n  .list-group-horizontal-lg > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0;\n  }\n  .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n    margin-left: calc(-1 * var(--bs-list-group-border-width));\n    border-left-width: var(--bs-list-group-border-width);\n  }\n}\n@media (min-width: 1200px) {\n  .list-group-horizontal-xl {\n    flex-direction: row;\n  }\n  .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0;\n  }\n  .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0;\n  }\n  .list-group-horizontal-xl > .list-group-item.active {\n    margin-top: 0;\n  }\n  .list-group-horizontal-xl > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0;\n  }\n  .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n    margin-left: calc(-1 * var(--bs-list-group-border-width));\n    border-left-width: var(--bs-list-group-border-width);\n  }\n}\n@media (min-width: 1400px) {\n  .list-group-horizontal-xxl {\n    flex-direction: row;\n  }\n  .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {\n    border-bottom-left-radius: var(--bs-list-group-border-radius);\n    border-top-right-radius: 0;\n  }\n  .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {\n    border-top-right-radius: var(--bs-list-group-border-radius);\n    border-bottom-left-radius: 0;\n  }\n  .list-group-horizontal-xxl > .list-group-item.active {\n    margin-top: 0;\n  }\n  .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n    border-top-width: var(--bs-list-group-border-width);\n    border-left-width: 0;\n  }\n  .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n    margin-left: calc(-1 * var(--bs-list-group-border-width));\n    border-left-width: var(--bs-list-group-border-width);\n  }\n}\n.list-group-flush {\n  border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n  border-width: 0 0 var(--bs-list-group-border-width);\n}\n.list-group-flush > .list-group-item:last-child {\n  border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n  --bs-list-group-color: var(--bs-primary-text-emphasis);\n  --bs-list-group-bg: var(--bs-primary-bg-subtle);\n  --bs-list-group-border-color: var(--bs-primary-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-primary-border-subtle);\n  --bs-list-group-active-color: var(--bs-primary-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-primary-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-primary-text-emphasis);\n}\n\n.list-group-item-secondary {\n  --bs-list-group-color: var(--bs-secondary-text-emphasis);\n  --bs-list-group-bg: var(--bs-secondary-bg-subtle);\n  --bs-list-group-border-color: var(--bs-secondary-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);\n  --bs-list-group-active-color: var(--bs-secondary-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-secondary-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis);\n}\n\n.list-group-item-success {\n  --bs-list-group-color: var(--bs-success-text-emphasis);\n  --bs-list-group-bg: var(--bs-success-bg-subtle);\n  --bs-list-group-border-color: var(--bs-success-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-success-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-success-border-subtle);\n  --bs-list-group-active-color: var(--bs-success-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-success-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-success-text-emphasis);\n}\n\n.list-group-item-info {\n  --bs-list-group-color: var(--bs-info-text-emphasis);\n  --bs-list-group-bg: var(--bs-info-bg-subtle);\n  --bs-list-group-border-color: var(--bs-info-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-info-border-subtle);\n  --bs-list-group-active-color: var(--bs-info-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-info-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-info-text-emphasis);\n}\n\n.list-group-item-warning {\n  --bs-list-group-color: var(--bs-warning-text-emphasis);\n  --bs-list-group-bg: var(--bs-warning-bg-subtle);\n  --bs-list-group-border-color: var(--bs-warning-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-warning-border-subtle);\n  --bs-list-group-active-color: var(--bs-warning-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-warning-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-warning-text-emphasis);\n}\n\n.list-group-item-danger {\n  --bs-list-group-color: var(--bs-danger-text-emphasis);\n  --bs-list-group-bg: var(--bs-danger-bg-subtle);\n  --bs-list-group-border-color: var(--bs-danger-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-danger-border-subtle);\n  --bs-list-group-active-color: var(--bs-danger-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-danger-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-danger-text-emphasis);\n}\n\n.list-group-item-light {\n  --bs-list-group-color: var(--bs-light-text-emphasis);\n  --bs-list-group-bg: var(--bs-light-bg-subtle);\n  --bs-list-group-border-color: var(--bs-light-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-light-border-subtle);\n  --bs-list-group-active-color: var(--bs-light-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-light-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-light-text-emphasis);\n}\n\n.list-group-item-dark {\n  --bs-list-group-color: var(--bs-dark-text-emphasis);\n  --bs-list-group-bg: var(--bs-dark-bg-subtle);\n  --bs-list-group-border-color: var(--bs-dark-border-subtle);\n  --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n  --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);\n  --bs-list-group-action-active-color: var(--bs-emphasis-color);\n  --bs-list-group-action-active-bg: var(--bs-dark-border-subtle);\n  --bs-list-group-active-color: var(--bs-dark-bg-subtle);\n  --bs-list-group-active-bg: var(--bs-dark-text-emphasis);\n  --bs-list-group-active-border-color: var(--bs-dark-text-emphasis);\n}\n\n.btn-close {\n  --bs-btn-close-color: #000;\n  --bs-btn-close-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e\");\n  --bs-btn-close-opacity: 0.5;\n  --bs-btn-close-hover-opacity: 0.75;\n  --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n  --bs-btn-close-focus-opacity: 1;\n  --bs-btn-close-disabled-opacity: 0.25;\n  box-sizing: content-box;\n  width: 1em;\n  height: 1em;\n  padding: 0.25em 0.25em;\n  color: var(--bs-btn-close-color);\n  background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;\n  filter: var(--bs-btn-close-filter);\n  border: 0;\n  border-radius: 0.375rem;\n  opacity: var(--bs-btn-close-opacity);\n}\n.btn-close:hover {\n  color: var(--bs-btn-close-color);\n  text-decoration: none;\n  opacity: var(--bs-btn-close-hover-opacity);\n}\n.btn-close:focus {\n  outline: 0;\n  box-shadow: var(--bs-btn-close-focus-shadow);\n  opacity: var(--bs-btn-close-focus-opacity);\n}\n.btn-close:disabled, .btn-close.disabled {\n  pointer-events: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  user-select: none;\n  opacity: var(--bs-btn-close-disabled-opacity);\n}\n\n.btn-close-white {\n  --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n:root,\n[data-bs-theme=light] {\n  --bs-btn-close-filter: ;\n}\n\n[data-bs-theme=dark] {\n  --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n.toast {\n  --bs-toast-zindex: 1090;\n  --bs-toast-padding-x: 0.75rem;\n  --bs-toast-padding-y: 0.5rem;\n  --bs-toast-spacing: 1.5rem;\n  --bs-toast-max-width: 350px;\n  --bs-toast-font-size: 0.875rem;\n  --bs-toast-color: ;\n  --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n  --bs-toast-border-width: var(--bs-border-width);\n  --bs-toast-border-color: var(--bs-border-color-translucent);\n  --bs-toast-border-radius: var(--bs-border-radius);\n  --bs-toast-box-shadow: var(--bs-box-shadow);\n  --bs-toast-header-color: var(--bs-secondary-color);\n  --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n  --bs-toast-header-border-color: var(--bs-border-color-translucent);\n  width: var(--bs-toast-max-width);\n  max-width: 100%;\n  font-size: var(--bs-toast-font-size);\n  color: var(--bs-toast-color);\n  pointer-events: auto;\n  background-color: var(--bs-toast-bg);\n  background-clip: padding-box;\n  border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);\n  box-shadow: var(--bs-toast-box-shadow);\n  border-radius: var(--bs-toast-border-radius);\n}\n.toast.showing {\n  opacity: 0;\n}\n.toast:not(.show) {\n  display: none;\n}\n\n.toast-container {\n  --bs-toast-zindex: 1090;\n  position: absolute;\n  z-index: var(--bs-toast-zindex);\n  width: -webkit-max-content;\n  width: -moz-max-content;\n  width: max-content;\n  max-width: 100%;\n  pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n  margin-bottom: var(--bs-toast-spacing);\n}\n\n.toast-header {\n  display: flex;\n  align-items: center;\n  padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);\n  color: var(--bs-toast-header-color);\n  background-color: var(--bs-toast-header-bg);\n  background-clip: padding-box;\n  border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);\n  border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n  border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n}\n.toast-header .btn-close {\n  margin-right: calc(-0.5 * var(--bs-toast-padding-x));\n  margin-left: var(--bs-toast-padding-x);\n}\n\n.toast-body {\n  padding: var(--bs-toast-padding-x);\n  word-wrap: break-word;\n}\n\n.modal {\n  --bs-modal-zindex: 1055;\n  --bs-modal-width: 500px;\n  --bs-modal-padding: 1rem;\n  --bs-modal-margin: 0.5rem;\n  --bs-modal-color: var(--bs-body-color);\n  --bs-modal-bg: var(--bs-body-bg);\n  --bs-modal-border-color: var(--bs-border-color-translucent);\n  --bs-modal-border-width: var(--bs-border-width);\n  --bs-modal-border-radius: var(--bs-border-radius-lg);\n  --bs-modal-box-shadow: var(--bs-box-shadow-sm);\n  --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));\n  --bs-modal-header-padding-x: 1rem;\n  --bs-modal-header-padding-y: 1rem;\n  --bs-modal-header-padding: 1rem 1rem;\n  --bs-modal-header-border-color: var(--bs-border-color);\n  --bs-modal-header-border-width: var(--bs-border-width);\n  --bs-modal-title-line-height: 1.5;\n  --bs-modal-footer-gap: 0.5rem;\n  --bs-modal-footer-bg: ;\n  --bs-modal-footer-border-color: var(--bs-border-color);\n  --bs-modal-footer-border-width: var(--bs-border-width);\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: var(--bs-modal-zindex);\n  display: none;\n  width: 100%;\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: auto;\n  outline: 0;\n}\n\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: var(--bs-modal-margin);\n  pointer-events: none;\n}\n.modal.fade .modal-dialog {\n  transform: translate(0, -50px);\n  transition: transform 0.3s ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .modal.fade .modal-dialog {\n    transition: none;\n  }\n}\n.modal.show .modal-dialog {\n  transform: none;\n}\n.modal.modal-static .modal-dialog {\n  transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n  height: calc(100% - var(--bs-modal-margin) * 2);\n}\n.modal-dialog-scrollable .modal-content {\n  max-height: 100%;\n  overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n  overflow-y: auto;\n}\n\n.modal-dialog-centered {\n  display: flex;\n  align-items: center;\n  min-height: calc(100% - var(--bs-modal-margin) * 2);\n}\n\n.modal-content {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  color: var(--bs-modal-color);\n  pointer-events: auto;\n  background-color: var(--bs-modal-bg);\n  background-clip: padding-box;\n  border: var(--bs-modal-border-width) solid var(--bs-modal-border-color);\n  border-radius: var(--bs-modal-border-radius);\n  outline: 0;\n}\n\n.modal-backdrop {\n  --bs-backdrop-zindex: 1050;\n  --bs-backdrop-bg: #000;\n  --bs-backdrop-opacity: 0.5;\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: var(--bs-backdrop-zindex);\n  width: 100vw;\n  height: 100vh;\n  background-color: var(--bs-backdrop-bg);\n}\n.modal-backdrop.fade {\n  opacity: 0;\n}\n.modal-backdrop.show {\n  opacity: var(--bs-backdrop-opacity);\n}\n\n.modal-header {\n  display: flex;\n  flex-shrink: 0;\n  align-items: center;\n  padding: var(--bs-modal-header-padding);\n  border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);\n  border-top-left-radius: var(--bs-modal-inner-border-radius);\n  border-top-right-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-header .btn-close {\n  padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);\n  margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));\n  margin-right: calc(-0.5 * var(--bs-modal-header-padding-x));\n  margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));\n  margin-left: auto;\n}\n\n.modal-title {\n  margin-bottom: 0;\n  line-height: var(--bs-modal-title-line-height);\n}\n\n.modal-body {\n  position: relative;\n  flex: 1 1 auto;\n  padding: var(--bs-modal-padding);\n}\n\n.modal-footer {\n  display: flex;\n  flex-shrink: 0;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: flex-end;\n  padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5);\n  background-color: var(--bs-modal-footer-bg);\n  border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);\n  border-bottom-right-radius: var(--bs-modal-inner-border-radius);\n  border-bottom-left-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-footer > * {\n  margin: calc(var(--bs-modal-footer-gap) * 0.5);\n}\n\n@media (min-width: 576px) {\n  .modal {\n    --bs-modal-margin: 1.75rem;\n    --bs-modal-box-shadow: var(--bs-box-shadow);\n  }\n  .modal-dialog {\n    max-width: var(--bs-modal-width);\n    margin-right: auto;\n    margin-left: auto;\n  }\n  .modal-sm {\n    --bs-modal-width: 300px;\n  }\n}\n@media (min-width: 992px) {\n  .modal-lg,\n  .modal-xl {\n    --bs-modal-width: 800px;\n  }\n}\n@media (min-width: 1200px) {\n  .modal-xl {\n    --bs-modal-width: 1140px;\n  }\n}\n.modal-fullscreen {\n  width: 100vw;\n  max-width: none;\n  height: 100%;\n  margin: 0;\n}\n.modal-fullscreen .modal-content {\n  height: 100%;\n  border: 0;\n  border-radius: 0;\n}\n.modal-fullscreen .modal-header,\n.modal-fullscreen .modal-footer {\n  border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n  overflow-y: auto;\n}\n\n@media (max-width: 575.98px) {\n  .modal-fullscreen-sm-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0;\n  }\n  .modal-fullscreen-sm-down .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0;\n  }\n  .modal-fullscreen-sm-down .modal-header,\n  .modal-fullscreen-sm-down .modal-footer {\n    border-radius: 0;\n  }\n  .modal-fullscreen-sm-down .modal-body {\n    overflow-y: auto;\n  }\n}\n@media (max-width: 767.98px) {\n  .modal-fullscreen-md-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0;\n  }\n  .modal-fullscreen-md-down .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0;\n  }\n  .modal-fullscreen-md-down .modal-header,\n  .modal-fullscreen-md-down .modal-footer {\n    border-radius: 0;\n  }\n  .modal-fullscreen-md-down .modal-body {\n    overflow-y: auto;\n  }\n}\n@media (max-width: 991.98px) {\n  .modal-fullscreen-lg-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0;\n  }\n  .modal-fullscreen-lg-down .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0;\n  }\n  .modal-fullscreen-lg-down .modal-header,\n  .modal-fullscreen-lg-down .modal-footer {\n    border-radius: 0;\n  }\n  .modal-fullscreen-lg-down .modal-body {\n    overflow-y: auto;\n  }\n}\n@media (max-width: 1199.98px) {\n  .modal-fullscreen-xl-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0;\n  }\n  .modal-fullscreen-xl-down .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0;\n  }\n  .modal-fullscreen-xl-down .modal-header,\n  .modal-fullscreen-xl-down .modal-footer {\n    border-radius: 0;\n  }\n  .modal-fullscreen-xl-down .modal-body {\n    overflow-y: auto;\n  }\n}\n@media (max-width: 1399.98px) {\n  .modal-fullscreen-xxl-down {\n    width: 100vw;\n    max-width: none;\n    height: 100%;\n    margin: 0;\n  }\n  .modal-fullscreen-xxl-down .modal-content {\n    height: 100%;\n    border: 0;\n    border-radius: 0;\n  }\n  .modal-fullscreen-xxl-down .modal-header,\n  .modal-fullscreen-xxl-down .modal-footer {\n    border-radius: 0;\n  }\n  .modal-fullscreen-xxl-down .modal-body {\n    overflow-y: auto;\n  }\n}\n.tooltip {\n  --bs-tooltip-zindex: 1080;\n  --bs-tooltip-max-width: 200px;\n  --bs-tooltip-padding-x: 0.5rem;\n  --bs-tooltip-padding-y: 0.25rem;\n  --bs-tooltip-margin: ;\n  --bs-tooltip-font-size: 0.875rem;\n  --bs-tooltip-color: var(--bs-body-bg);\n  --bs-tooltip-bg: var(--bs-emphasis-color);\n  --bs-tooltip-border-radius: var(--bs-border-radius);\n  --bs-tooltip-opacity: 0.9;\n  --bs-tooltip-arrow-width: 0.8rem;\n  --bs-tooltip-arrow-height: 0.4rem;\n  z-index: var(--bs-tooltip-zindex);\n  display: block;\n  margin: var(--bs-tooltip-margin);\n  font-family: var(--bs-font-sans-serif);\n  font-style: normal;\n  font-weight: 400;\n  line-height: 1.5;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  white-space: normal;\n  word-spacing: normal;\n  line-break: auto;\n  font-size: var(--bs-tooltip-font-size);\n  word-wrap: break-word;\n  opacity: 0;\n}\n.tooltip.show {\n  opacity: var(--bs-tooltip-opacity);\n}\n.tooltip .tooltip-arrow {\n  display: block;\n  width: var(--bs-tooltip-arrow-width);\n  height: var(--bs-tooltip-arrow-height);\n}\n.tooltip .tooltip-arrow::before {\n  position: absolute;\n  content: \"\";\n  border-color: transparent;\n  border-style: solid;\n}\n\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n  bottom: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n  top: -1px;\n  border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n  border-top-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n  left: calc(-1 * var(--bs-tooltip-arrow-height));\n  width: var(--bs-tooltip-arrow-height);\n  height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n  right: -1px;\n  border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n  border-right-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n  top: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n  bottom: -1px;\n  border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n  border-bottom-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n  right: calc(-1 * var(--bs-tooltip-arrow-height));\n  width: var(--bs-tooltip-arrow-height);\n  height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n  left: -1px;\n  border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n  border-left-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.tooltip-inner {\n  max-width: var(--bs-tooltip-max-width);\n  padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);\n  color: var(--bs-tooltip-color);\n  text-align: center;\n  background-color: var(--bs-tooltip-bg);\n  border-radius: var(--bs-tooltip-border-radius);\n}\n\n.popover {\n  --bs-popover-zindex: 1070;\n  --bs-popover-max-width: 276px;\n  --bs-popover-font-size: 0.875rem;\n  --bs-popover-bg: var(--bs-body-bg);\n  --bs-popover-border-width: var(--bs-border-width);\n  --bs-popover-border-color: var(--bs-border-color-translucent);\n  --bs-popover-border-radius: var(--bs-border-radius-lg);\n  --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));\n  --bs-popover-box-shadow: var(--bs-box-shadow);\n  --bs-popover-header-padding-x: 1rem;\n  --bs-popover-header-padding-y: 0.5rem;\n  --bs-popover-header-font-size: 1rem;\n  --bs-popover-header-color: inherit;\n  --bs-popover-header-bg: var(--bs-secondary-bg);\n  --bs-popover-body-padding-x: 1rem;\n  --bs-popover-body-padding-y: 1rem;\n  --bs-popover-body-color: var(--bs-body-color);\n  --bs-popover-arrow-width: 1rem;\n  --bs-popover-arrow-height: 0.5rem;\n  --bs-popover-arrow-border: var(--bs-popover-border-color);\n  z-index: var(--bs-popover-zindex);\n  display: block;\n  max-width: var(--bs-popover-max-width);\n  font-family: var(--bs-font-sans-serif);\n  font-style: normal;\n  font-weight: 400;\n  line-height: 1.5;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  letter-spacing: normal;\n  word-break: normal;\n  white-space: normal;\n  word-spacing: normal;\n  line-break: auto;\n  font-size: var(--bs-popover-font-size);\n  word-wrap: break-word;\n  background-color: var(--bs-popover-bg);\n  background-clip: padding-box;\n  border: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n  border-radius: var(--bs-popover-border-radius);\n}\n.popover .popover-arrow {\n  display: block;\n  width: var(--bs-popover-arrow-width);\n  height: var(--bs-popover-arrow-height);\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n  position: absolute;\n  display: block;\n  content: \"\";\n  border-color: transparent;\n  border-style: solid;\n  border-width: 0;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n  bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n  border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n  bottom: 0;\n  border-top-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n  bottom: var(--bs-popover-border-width);\n  border-top-color: var(--bs-popover-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n  left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n  width: var(--bs-popover-arrow-height);\n  height: var(--bs-popover-arrow-width);\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n  border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n  left: 0;\n  border-right-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n  left: var(--bs-popover-border-width);\n  border-right-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n  top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n  border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n  top: 0;\n  border-bottom-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n  top: var(--bs-popover-border-width);\n  border-bottom-color: var(--bs-popover-bg);\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n  position: absolute;\n  top: 0;\n  left: 50%;\n  display: block;\n  width: var(--bs-popover-arrow-width);\n  margin-left: calc(-0.5 * var(--bs-popover-arrow-width));\n  content: \"\";\n  border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n  right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n  width: var(--bs-popover-arrow-height);\n  height: var(--bs-popover-arrow-width);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n  border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n  right: 0;\n  border-left-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n  right: var(--bs-popover-border-width);\n  border-left-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.popover-header {\n  padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);\n  margin-bottom: 0;\n  font-size: var(--bs-popover-header-font-size);\n  color: var(--bs-popover-header-color);\n  background-color: var(--bs-popover-header-bg);\n  border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n  border-top-left-radius: var(--bs-popover-inner-border-radius);\n  border-top-right-radius: var(--bs-popover-inner-border-radius);\n}\n.popover-header:empty {\n  display: none;\n}\n\n.popover-body {\n  padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);\n  color: var(--bs-popover-body-color);\n}\n\n.carousel {\n  position: relative;\n}\n\n.carousel.pointer-event {\n  touch-action: pan-y;\n}\n\n.carousel-inner {\n  position: relative;\n  width: 100%;\n  overflow: hidden;\n}\n.carousel-inner::after {\n  display: block;\n  clear: both;\n  content: \"\";\n}\n\n.carousel-item {\n  position: relative;\n  display: none;\n  float: left;\n  width: 100%;\n  margin-right: -100%;\n  -webkit-backface-visibility: hidden;\n  backface-visibility: hidden;\n  transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n  .carousel-item {\n    transition: none;\n  }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n  display: block;\n}\n\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n  transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n  transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n  opacity: 0;\n  transition-property: opacity;\n  transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n  z-index: 1;\n  opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n  z-index: 0;\n  opacity: 0;\n  transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n  .carousel-fade .active.carousel-item-start,\n  .carousel-fade .active.carousel-item-end {\n    transition: none;\n  }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  z-index: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 15%;\n  padding: 0;\n  color: #fff;\n  text-align: center;\n  background: none;\n  filter: var(--bs-carousel-control-icon-filter);\n  border: 0;\n  opacity: 0.5;\n  transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n  .carousel-control-prev,\n  .carousel-control-next {\n    transition: none;\n  }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n  color: #fff;\n  text-decoration: none;\n  outline: 0;\n  opacity: 0.9;\n}\n\n.carousel-control-prev {\n  left: 0;\n}\n\n.carousel-control-next {\n  right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n  display: inline-block;\n  width: 2rem;\n  height: 2rem;\n  background-repeat: no-repeat;\n  background-position: 50%;\n  background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-control-next-icon {\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-indicators {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 2;\n  display: flex;\n  justify-content: center;\n  padding: 0;\n  margin-right: 15%;\n  margin-bottom: 1rem;\n  margin-left: 15%;\n}\n.carousel-indicators [data-bs-target] {\n  box-sizing: content-box;\n  flex: 0 1 auto;\n  width: 30px;\n  height: 3px;\n  padding: 0;\n  margin-right: 3px;\n  margin-left: 3px;\n  text-indent: -999px;\n  cursor: pointer;\n  background-color: var(--bs-carousel-indicator-active-bg);\n  background-clip: padding-box;\n  border: 0;\n  border-top: 10px solid transparent;\n  border-bottom: 10px solid transparent;\n  opacity: 0.5;\n  transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n  .carousel-indicators [data-bs-target] {\n    transition: none;\n  }\n}\n.carousel-indicators .active {\n  opacity: 1;\n}\n\n.carousel-caption {\n  position: absolute;\n  right: 15%;\n  bottom: 1.25rem;\n  left: 15%;\n  padding-top: 1.25rem;\n  padding-bottom: 1.25rem;\n  color: var(--bs-carousel-caption-color);\n  text-align: center;\n}\n\n.carousel-dark {\n  --bs-carousel-indicator-active-bg: #000;\n  --bs-carousel-caption-color: #000;\n  --bs-carousel-control-icon-filter: invert(1) grayscale(100);\n}\n\n:root,\n[data-bs-theme=light] {\n  --bs-carousel-indicator-active-bg: #fff;\n  --bs-carousel-caption-color: #fff;\n  --bs-carousel-control-icon-filter: ;\n}\n\n[data-bs-theme=dark] {\n  --bs-carousel-indicator-active-bg: #000;\n  --bs-carousel-caption-color: #000;\n  --bs-carousel-control-icon-filter: invert(1) grayscale(100);\n}\n\n.spinner-grow,\n.spinner-border {\n  display: inline-block;\n  flex-shrink: 0;\n  width: var(--bs-spinner-width);\n  height: var(--bs-spinner-height);\n  vertical-align: var(--bs-spinner-vertical-align);\n  border-radius: 50%;\n  animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);\n}\n\n@keyframes spinner-border {\n  to {\n    transform: rotate(360deg) /* rtl:ignore */;\n  }\n}\n.spinner-border {\n  --bs-spinner-width: 2rem;\n  --bs-spinner-height: 2rem;\n  --bs-spinner-vertical-align: -0.125em;\n  --bs-spinner-border-width: 0.25em;\n  --bs-spinner-animation-speed: 0.75s;\n  --bs-spinner-animation-name: spinner-border;\n  border: var(--bs-spinner-border-width) solid currentcolor;\n  border-right-color: transparent;\n}\n\n.spinner-border-sm {\n  --bs-spinner-width: 1rem;\n  --bs-spinner-height: 1rem;\n  --bs-spinner-border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n  0% {\n    transform: scale(0);\n  }\n  50% {\n    opacity: 1;\n    transform: none;\n  }\n}\n.spinner-grow {\n  --bs-spinner-width: 2rem;\n  --bs-spinner-height: 2rem;\n  --bs-spinner-vertical-align: -0.125em;\n  --bs-spinner-animation-speed: 0.75s;\n  --bs-spinner-animation-name: spinner-grow;\n  background-color: currentcolor;\n  opacity: 0;\n}\n\n.spinner-grow-sm {\n  --bs-spinner-width: 1rem;\n  --bs-spinner-height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .spinner-border,\n  .spinner-grow {\n    --bs-spinner-animation-speed: 1.5s;\n  }\n}\n.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {\n  --bs-offcanvas-zindex: 1045;\n  --bs-offcanvas-width: 400px;\n  --bs-offcanvas-height: 30vh;\n  --bs-offcanvas-padding-x: 1rem;\n  --bs-offcanvas-padding-y: 1rem;\n  --bs-offcanvas-color: var(--bs-body-color);\n  --bs-offcanvas-bg: var(--bs-body-bg);\n  --bs-offcanvas-border-width: var(--bs-border-width);\n  --bs-offcanvas-border-color: var(--bs-border-color-translucent);\n  --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);\n  --bs-offcanvas-transition: transform 0.3s ease-in-out;\n  --bs-offcanvas-title-line-height: 1.5;\n}\n\n@media (max-width: 575.98px) {\n  .offcanvas-sm {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition);\n  }\n}\n@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {\n  .offcanvas-sm {\n    transition: none;\n  }\n}\n@media (max-width: 575.98px) {\n  .offcanvas-sm.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%);\n  }\n  .offcanvas-sm.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%);\n  }\n  .offcanvas-sm.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%);\n  }\n  .offcanvas-sm.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%);\n  }\n  .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) {\n    transform: none;\n  }\n  .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show {\n    visibility: visible;\n  }\n}\n@media (min-width: 576px) {\n  .offcanvas-sm {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important;\n  }\n  .offcanvas-sm .offcanvas-header {\n    display: none;\n  }\n  .offcanvas-sm .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n    background-color: transparent !important;\n  }\n}\n\n@media (max-width: 767.98px) {\n  .offcanvas-md {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition);\n  }\n}\n@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {\n  .offcanvas-md {\n    transition: none;\n  }\n}\n@media (max-width: 767.98px) {\n  .offcanvas-md.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%);\n  }\n  .offcanvas-md.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%);\n  }\n  .offcanvas-md.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%);\n  }\n  .offcanvas-md.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%);\n  }\n  .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) {\n    transform: none;\n  }\n  .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show {\n    visibility: visible;\n  }\n}\n@media (min-width: 768px) {\n  .offcanvas-md {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important;\n  }\n  .offcanvas-md .offcanvas-header {\n    display: none;\n  }\n  .offcanvas-md .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n    background-color: transparent !important;\n  }\n}\n\n@media (max-width: 991.98px) {\n  .offcanvas-lg {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition);\n  }\n}\n@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {\n  .offcanvas-lg {\n    transition: none;\n  }\n}\n@media (max-width: 991.98px) {\n  .offcanvas-lg.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%);\n  }\n  .offcanvas-lg.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%);\n  }\n  .offcanvas-lg.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%);\n  }\n  .offcanvas-lg.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%);\n  }\n  .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) {\n    transform: none;\n  }\n  .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show {\n    visibility: visible;\n  }\n}\n@media (min-width: 992px) {\n  .offcanvas-lg {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important;\n  }\n  .offcanvas-lg .offcanvas-header {\n    display: none;\n  }\n  .offcanvas-lg .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n    background-color: transparent !important;\n  }\n}\n\n@media (max-width: 1199.98px) {\n  .offcanvas-xl {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition);\n  }\n}\n@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {\n  .offcanvas-xl {\n    transition: none;\n  }\n}\n@media (max-width: 1199.98px) {\n  .offcanvas-xl.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%);\n  }\n  .offcanvas-xl.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%);\n  }\n  .offcanvas-xl.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%);\n  }\n  .offcanvas-xl.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%);\n  }\n  .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) {\n    transform: none;\n  }\n  .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show {\n    visibility: visible;\n  }\n}\n@media (min-width: 1200px) {\n  .offcanvas-xl {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important;\n  }\n  .offcanvas-xl .offcanvas-header {\n    display: none;\n  }\n  .offcanvas-xl .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n    background-color: transparent !important;\n  }\n}\n\n@media (max-width: 1399.98px) {\n  .offcanvas-xxl {\n    position: fixed;\n    bottom: 0;\n    z-index: var(--bs-offcanvas-zindex);\n    display: flex;\n    flex-direction: column;\n    max-width: 100%;\n    color: var(--bs-offcanvas-color);\n    visibility: hidden;\n    background-color: var(--bs-offcanvas-bg);\n    background-clip: padding-box;\n    outline: 0;\n    transition: var(--bs-offcanvas-transition);\n  }\n}\n@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {\n  .offcanvas-xxl {\n    transition: none;\n  }\n}\n@media (max-width: 1399.98px) {\n  .offcanvas-xxl.offcanvas-start {\n    top: 0;\n    left: 0;\n    width: var(--bs-offcanvas-width);\n    border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(-100%);\n  }\n  .offcanvas-xxl.offcanvas-end {\n    top: 0;\n    right: 0;\n    width: var(--bs-offcanvas-width);\n    border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateX(100%);\n  }\n  .offcanvas-xxl.offcanvas-top {\n    top: 0;\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(-100%);\n  }\n  .offcanvas-xxl.offcanvas-bottom {\n    right: 0;\n    left: 0;\n    height: var(--bs-offcanvas-height);\n    max-height: 100%;\n    border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n    transform: translateY(100%);\n  }\n  .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) {\n    transform: none;\n  }\n  .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show {\n    visibility: visible;\n  }\n}\n@media (min-width: 1400px) {\n  .offcanvas-xxl {\n    --bs-offcanvas-height: auto;\n    --bs-offcanvas-border-width: 0;\n    background-color: transparent !important;\n  }\n  .offcanvas-xxl .offcanvas-header {\n    display: none;\n  }\n  .offcanvas-xxl .offcanvas-body {\n    display: flex;\n    flex-grow: 0;\n    padding: 0;\n    overflow-y: visible;\n    background-color: transparent !important;\n  }\n}\n\n.offcanvas {\n  position: fixed;\n  bottom: 0;\n  z-index: var(--bs-offcanvas-zindex);\n  display: flex;\n  flex-direction: column;\n  max-width: 100%;\n  color: var(--bs-offcanvas-color);\n  visibility: hidden;\n  background-color: var(--bs-offcanvas-bg);\n  background-clip: padding-box;\n  outline: 0;\n  transition: var(--bs-offcanvas-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n  .offcanvas {\n    transition: none;\n  }\n}\n.offcanvas.offcanvas-start {\n  top: 0;\n  left: 0;\n  width: var(--bs-offcanvas-width);\n  border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n  transform: translateX(-100%);\n}\n.offcanvas.offcanvas-end {\n  top: 0;\n  right: 0;\n  width: var(--bs-offcanvas-width);\n  border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n  transform: translateX(100%);\n}\n.offcanvas.offcanvas-top {\n  top: 0;\n  right: 0;\n  left: 0;\n  height: var(--bs-offcanvas-height);\n  max-height: 100%;\n  border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n  transform: translateY(-100%);\n}\n.offcanvas.offcanvas-bottom {\n  right: 0;\n  left: 0;\n  height: var(--bs-offcanvas-height);\n  max-height: 100%;\n  border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n  transform: translateY(100%);\n}\n.offcanvas.showing, .offcanvas.show:not(.hiding) {\n  transform: none;\n}\n.offcanvas.showing, .offcanvas.hiding, .offcanvas.show {\n  visibility: visible;\n}\n\n.offcanvas-backdrop {\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: 1040;\n  width: 100vw;\n  height: 100vh;\n  background-color: #000;\n}\n.offcanvas-backdrop.fade {\n  opacity: 0;\n}\n.offcanvas-backdrop.show {\n  opacity: 0.5;\n}\n\n.offcanvas-header {\n  display: flex;\n  align-items: center;\n  padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n}\n.offcanvas-header .btn-close {\n  padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);\n  margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));\n  margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));\n  margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));\n  margin-left: auto;\n}\n\n.offcanvas-title {\n  margin-bottom: 0;\n  line-height: var(--bs-offcanvas-title-line-height);\n}\n\n.offcanvas-body {\n  flex-grow: 1;\n  padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n  overflow-y: auto;\n}\n\n.placeholder {\n  display: inline-block;\n  min-height: 1em;\n  vertical-align: middle;\n  cursor: wait;\n  background-color: currentcolor;\n  opacity: 0.5;\n}\n.placeholder.btn::before {\n  display: inline-block;\n  content: \"\";\n}\n\n.placeholder-xs {\n  min-height: 0.6em;\n}\n\n.placeholder-sm {\n  min-height: 0.8em;\n}\n\n.placeholder-lg {\n  min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n  animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@keyframes placeholder-glow {\n  50% {\n    opacity: 0.2;\n  }\n}\n.placeholder-wave {\n  -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n  mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n  -webkit-mask-size: 200% 100%;\n  mask-size: 200% 100%;\n  animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n  100% {\n    -webkit-mask-position: -200% 0%;\n    mask-position: -200% 0%;\n  }\n}\n.clearfix::after {\n  display: block;\n  clear: both;\n  content: \"\";\n}\n\n.text-bg-primary {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-secondary {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-success {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-info {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-warning {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-danger {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-light {\n  color: #000 !important;\n  background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-dark {\n  color: #fff !important;\n  background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.link-primary {\n  color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-primary:hover, .link-primary:focus {\n  color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-secondary {\n  color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-secondary:hover, .link-secondary:focus {\n  color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-success {\n  color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-success:hover, .link-success:focus {\n  color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-info {\n  color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-info:hover, .link-info:focus {\n  color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-warning {\n  color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-warning:hover, .link-warning:focus {\n  color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-danger {\n  color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-danger:hover, .link-danger:focus {\n  color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-light {\n  color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-light:hover, .link-light:focus {\n  color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-dark {\n  color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-dark:hover, .link-dark:focus {\n  color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-body-emphasis {\n  color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-body-emphasis:hover, .link-body-emphasis:focus {\n  color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;\n  -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important;\n  text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important;\n}\n\n.focus-ring:focus {\n  outline: 0;\n  box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color);\n}\n\n.icon-link {\n  display: inline-flex;\n  gap: 0.375rem;\n  align-items: center;\n  -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n  text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n  text-underline-offset: 0.25em;\n  -webkit-backface-visibility: hidden;\n  backface-visibility: hidden;\n}\n.icon-link > .bi {\n  flex-shrink: 0;\n  width: 1em;\n  height: 1em;\n  fill: currentcolor;\n  transition: 0.2s ease-in-out transform;\n}\n@media (prefers-reduced-motion: reduce) {\n  .icon-link > .bi {\n    transition: none;\n  }\n}\n\n.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi {\n  transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0));\n}\n\n.ratio {\n  position: relative;\n  width: 100%;\n}\n.ratio::before {\n  display: block;\n  padding-top: var(--bs-aspect-ratio);\n  content: \"\";\n}\n.ratio > * {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n\n.ratio-1x1 {\n  --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n  --bs-aspect-ratio: 75%;\n}\n\n.ratio-16x9 {\n  --bs-aspect-ratio: 56.25%;\n}\n\n.ratio-21x9 {\n  --bs-aspect-ratio: 42.8571428571%;\n}\n\n.fixed-top {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  z-index: 1030;\n}\n\n.fixed-bottom {\n  position: fixed;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1030;\n}\n\n.sticky-top {\n  position: -webkit-sticky;\n  position: sticky;\n  top: 0;\n  z-index: 1020;\n}\n\n.sticky-bottom {\n  position: -webkit-sticky;\n  position: sticky;\n  bottom: 0;\n  z-index: 1020;\n}\n\n@media (min-width: 576px) {\n  .sticky-sm-top {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 0;\n    z-index: 1020;\n  }\n  .sticky-sm-bottom {\n    position: -webkit-sticky;\n    position: sticky;\n    bottom: 0;\n    z-index: 1020;\n  }\n}\n@media (min-width: 768px) {\n  .sticky-md-top {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 0;\n    z-index: 1020;\n  }\n  .sticky-md-bottom {\n    position: -webkit-sticky;\n    position: sticky;\n    bottom: 0;\n    z-index: 1020;\n  }\n}\n@media (min-width: 992px) {\n  .sticky-lg-top {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 0;\n    z-index: 1020;\n  }\n  .sticky-lg-bottom {\n    position: -webkit-sticky;\n    position: sticky;\n    bottom: 0;\n    z-index: 1020;\n  }\n}\n@media (min-width: 1200px) {\n  .sticky-xl-top {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 0;\n    z-index: 1020;\n  }\n  .sticky-xl-bottom {\n    position: -webkit-sticky;\n    position: sticky;\n    bottom: 0;\n    z-index: 1020;\n  }\n}\n@media (min-width: 1400px) {\n  .sticky-xxl-top {\n    position: -webkit-sticky;\n    position: sticky;\n    top: 0;\n    z-index: 1020;\n  }\n  .sticky-xxl-bottom {\n    position: -webkit-sticky;\n    position: sticky;\n    bottom: 0;\n    z-index: 1020;\n  }\n}\n.hstack {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  align-self: stretch;\n}\n\n.vstack {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n  width: 1px !important;\n  height: 1px !important;\n  padding: 0 !important;\n  margin: -1px !important;\n  overflow: hidden !important;\n  clip: rect(0, 0, 0, 0) !important;\n  white-space: nowrap !important;\n  border: 0 !important;\n}\n.visually-hidden:not(caption),\n.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {\n  position: absolute !important;\n}\n.visually-hidden *,\n.visually-hidden-focusable:not(:focus):not(:focus-within) * {\n  overflow: hidden !important;\n}\n\n.stretched-link::after {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1;\n  content: \"\";\n}\n\n.text-truncate {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.vr {\n  display: inline-block;\n  align-self: stretch;\n  width: var(--bs-border-width);\n  min-height: 1em;\n  background-color: currentcolor;\n  opacity: 0.25;\n}\n\n.align-baseline {\n  vertical-align: baseline !important;\n}\n\n.align-top {\n  vertical-align: top !important;\n}\n\n.align-middle {\n  vertical-align: middle !important;\n}\n\n.align-bottom {\n  vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n  vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n  vertical-align: text-top !important;\n}\n\n.float-start {\n  float: left !important;\n}\n\n.float-end {\n  float: right !important;\n}\n\n.float-none {\n  float: none !important;\n}\n\n.object-fit-contain {\n  -o-object-fit: contain !important;\n  object-fit: contain !important;\n}\n\n.object-fit-cover {\n  -o-object-fit: cover !important;\n  object-fit: cover !important;\n}\n\n.object-fit-fill {\n  -o-object-fit: fill !important;\n  object-fit: fill !important;\n}\n\n.object-fit-scale {\n  -o-object-fit: scale-down !important;\n  object-fit: scale-down !important;\n}\n\n.object-fit-none {\n  -o-object-fit: none !important;\n  object-fit: none !important;\n}\n\n.opacity-0 {\n  opacity: 0 !important;\n}\n\n.opacity-25 {\n  opacity: 0.25 !important;\n}\n\n.opacity-50 {\n  opacity: 0.5 !important;\n}\n\n.opacity-75 {\n  opacity: 0.75 !important;\n}\n\n.opacity-100 {\n  opacity: 1 !important;\n}\n\n.overflow-auto {\n  overflow: auto !important;\n}\n\n.overflow-hidden {\n  overflow: hidden !important;\n}\n\n.overflow-visible {\n  overflow: visible !important;\n}\n\n.overflow-scroll {\n  overflow: scroll !important;\n}\n\n.overflow-x-auto {\n  overflow-x: auto !important;\n}\n\n.overflow-x-hidden {\n  overflow-x: hidden !important;\n}\n\n.overflow-x-visible {\n  overflow-x: visible !important;\n}\n\n.overflow-x-scroll {\n  overflow-x: scroll !important;\n}\n\n.overflow-y-auto {\n  overflow-y: auto !important;\n}\n\n.overflow-y-hidden {\n  overflow-y: hidden !important;\n}\n\n.overflow-y-visible {\n  overflow-y: visible !important;\n}\n\n.overflow-y-scroll {\n  overflow-y: scroll !important;\n}\n\n.d-inline {\n  display: inline !important;\n}\n\n.d-inline-block {\n  display: inline-block !important;\n}\n\n.d-block {\n  display: block !important;\n}\n\n.d-grid {\n  display: grid !important;\n}\n\n.d-inline-grid {\n  display: inline-grid !important;\n}\n\n.d-table {\n  display: table !important;\n}\n\n.d-table-row {\n  display: table-row !important;\n}\n\n.d-table-cell {\n  display: table-cell !important;\n}\n\n.d-flex {\n  display: flex !important;\n}\n\n.d-inline-flex {\n  display: inline-flex !important;\n}\n\n.d-none {\n  display: none !important;\n}\n\n.shadow {\n  box-shadow: var(--bs-box-shadow) !important;\n}\n\n.shadow-sm {\n  box-shadow: var(--bs-box-shadow-sm) !important;\n}\n\n.shadow-lg {\n  box-shadow: var(--bs-box-shadow-lg) !important;\n}\n\n.shadow-none {\n  box-shadow: none !important;\n}\n\n.focus-ring-primary {\n  --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-secondary {\n  --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-success {\n  --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-info {\n  --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-warning {\n  --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-danger {\n  --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-light {\n  --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-dark {\n  --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity));\n}\n\n.position-static {\n  position: static !important;\n}\n\n.position-relative {\n  position: relative !important;\n}\n\n.position-absolute {\n  position: absolute !important;\n}\n\n.position-fixed {\n  position: fixed !important;\n}\n\n.position-sticky {\n  position: -webkit-sticky !important;\n  position: sticky !important;\n}\n\n.top-0 {\n  top: 0 !important;\n}\n\n.top-50 {\n  top: 50% !important;\n}\n\n.top-100 {\n  top: 100% !important;\n}\n\n.bottom-0 {\n  bottom: 0 !important;\n}\n\n.bottom-50 {\n  bottom: 50% !important;\n}\n\n.bottom-100 {\n  bottom: 100% !important;\n}\n\n.start-0 {\n  left: 0 !important;\n}\n\n.start-50 {\n  left: 50% !important;\n}\n\n.start-100 {\n  left: 100% !important;\n}\n\n.end-0 {\n  right: 0 !important;\n}\n\n.end-50 {\n  right: 50% !important;\n}\n\n.end-100 {\n  right: 100% !important;\n}\n\n.translate-middle {\n  transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n  transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n  transform: translateY(-50%) !important;\n}\n\n.border {\n  border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-0 {\n  border: 0 !important;\n}\n\n.border-top {\n  border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-top-0 {\n  border-top: 0 !important;\n}\n\n.border-end {\n  border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-end-0 {\n  border-right: 0 !important;\n}\n\n.border-bottom {\n  border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-bottom-0 {\n  border-bottom: 0 !important;\n}\n\n.border-start {\n  border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-start-0 {\n  border-left: 0 !important;\n}\n\n.border-primary {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-secondary {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-success {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-info {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-warning {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-danger {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-light {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-dark {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-black {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-white {\n  --bs-border-opacity: 1;\n  border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-primary-subtle {\n  border-color: var(--bs-primary-border-subtle) !important;\n}\n\n.border-secondary-subtle {\n  border-color: var(--bs-secondary-border-subtle) !important;\n}\n\n.border-success-subtle {\n  border-color: var(--bs-success-border-subtle) !important;\n}\n\n.border-info-subtle {\n  border-color: var(--bs-info-border-subtle) !important;\n}\n\n.border-warning-subtle {\n  border-color: var(--bs-warning-border-subtle) !important;\n}\n\n.border-danger-subtle {\n  border-color: var(--bs-danger-border-subtle) !important;\n}\n\n.border-light-subtle {\n  border-color: var(--bs-light-border-subtle) !important;\n}\n\n.border-dark-subtle {\n  border-color: var(--bs-dark-border-subtle) !important;\n}\n\n.border-1 {\n  border-width: 1px !important;\n}\n\n.border-2 {\n  border-width: 2px !important;\n}\n\n.border-3 {\n  border-width: 3px !important;\n}\n\n.border-4 {\n  border-width: 4px !important;\n}\n\n.border-5 {\n  border-width: 5px !important;\n}\n\n.border-opacity-10 {\n  --bs-border-opacity: 0.1;\n}\n\n.border-opacity-25 {\n  --bs-border-opacity: 0.25;\n}\n\n.border-opacity-50 {\n  --bs-border-opacity: 0.5;\n}\n\n.border-opacity-75 {\n  --bs-border-opacity: 0.75;\n}\n\n.border-opacity-100 {\n  --bs-border-opacity: 1;\n}\n\n.w-25 {\n  width: 25% !important;\n}\n\n.w-50 {\n  width: 50% !important;\n}\n\n.w-75 {\n  width: 75% !important;\n}\n\n.w-100 {\n  width: 100% !important;\n}\n\n.w-auto {\n  width: auto !important;\n}\n\n.mw-100 {\n  max-width: 100% !important;\n}\n\n.vw-100 {\n  width: 100vw !important;\n}\n\n.min-vw-100 {\n  min-width: 100vw !important;\n}\n\n.h-25 {\n  height: 25% !important;\n}\n\n.h-50 {\n  height: 50% !important;\n}\n\n.h-75 {\n  height: 75% !important;\n}\n\n.h-100 {\n  height: 100% !important;\n}\n\n.h-auto {\n  height: auto !important;\n}\n\n.mh-100 {\n  max-height: 100% !important;\n}\n\n.vh-100 {\n  height: 100vh !important;\n}\n\n.min-vh-100 {\n  min-height: 100vh !important;\n}\n\n.flex-fill {\n  flex: 1 1 auto !important;\n}\n\n.flex-row {\n  flex-direction: row !important;\n}\n\n.flex-column {\n  flex-direction: column !important;\n}\n\n.flex-row-reverse {\n  flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n  flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n  flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n  flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n  flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n  flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n  flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n  flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n  flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n  justify-content: flex-start !important;\n}\n\n.justify-content-end {\n  justify-content: flex-end !important;\n}\n\n.justify-content-center {\n  justify-content: center !important;\n}\n\n.justify-content-between {\n  justify-content: space-between !important;\n}\n\n.justify-content-around {\n  justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n  justify-content: space-evenly !important;\n}\n\n.align-items-start {\n  align-items: flex-start !important;\n}\n\n.align-items-end {\n  align-items: flex-end !important;\n}\n\n.align-items-center {\n  align-items: center !important;\n}\n\n.align-items-baseline {\n  align-items: baseline !important;\n}\n\n.align-items-stretch {\n  align-items: stretch !important;\n}\n\n.align-content-start {\n  align-content: flex-start !important;\n}\n\n.align-content-end {\n  align-content: flex-end !important;\n}\n\n.align-content-center {\n  align-content: center !important;\n}\n\n.align-content-between {\n  align-content: space-between !important;\n}\n\n.align-content-around {\n  align-content: space-around !important;\n}\n\n.align-content-stretch {\n  align-content: stretch !important;\n}\n\n.align-self-auto {\n  align-self: auto !important;\n}\n\n.align-self-start {\n  align-self: flex-start !important;\n}\n\n.align-self-end {\n  align-self: flex-end !important;\n}\n\n.align-self-center {\n  align-self: center !important;\n}\n\n.align-self-baseline {\n  align-self: baseline !important;\n}\n\n.align-self-stretch {\n  align-self: stretch !important;\n}\n\n.order-first {\n  order: -1 !important;\n}\n\n.order-0 {\n  order: 0 !important;\n}\n\n.order-1 {\n  order: 1 !important;\n}\n\n.order-2 {\n  order: 2 !important;\n}\n\n.order-3 {\n  order: 3 !important;\n}\n\n.order-4 {\n  order: 4 !important;\n}\n\n.order-5 {\n  order: 5 !important;\n}\n\n.order-last {\n  order: 6 !important;\n}\n\n.m-0 {\n  margin: 0 !important;\n}\n\n.m-1 {\n  margin: 0.25rem !important;\n}\n\n.m-2 {\n  margin: 0.5rem !important;\n}\n\n.m-3 {\n  margin: 1rem !important;\n}\n\n.m-4 {\n  margin: 1.5rem !important;\n}\n\n.m-5 {\n  margin: 3rem !important;\n}\n\n.m-auto {\n  margin: auto !important;\n}\n\n.mx-0 {\n  margin-right: 0 !important;\n  margin-left: 0 !important;\n}\n\n.mx-1 {\n  margin-right: 0.25rem !important;\n  margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n  margin-right: 0.5rem !important;\n  margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n  margin-right: 1rem !important;\n  margin-left: 1rem !important;\n}\n\n.mx-4 {\n  margin-right: 1.5rem !important;\n  margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n  margin-right: 3rem !important;\n  margin-left: 3rem !important;\n}\n\n.mx-auto {\n  margin-right: auto !important;\n  margin-left: auto !important;\n}\n\n.my-0 {\n  margin-top: 0 !important;\n  margin-bottom: 0 !important;\n}\n\n.my-1 {\n  margin-top: 0.25rem !important;\n  margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n  margin-top: 0.5rem !important;\n  margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n  margin-top: 1rem !important;\n  margin-bottom: 1rem !important;\n}\n\n.my-4 {\n  margin-top: 1.5rem !important;\n  margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n  margin-top: 3rem !important;\n  margin-bottom: 3rem !important;\n}\n\n.my-auto {\n  margin-top: auto !important;\n  margin-bottom: auto !important;\n}\n\n.mt-0 {\n  margin-top: 0 !important;\n}\n\n.mt-1 {\n  margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n  margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n  margin-top: 1rem !important;\n}\n\n.mt-4 {\n  margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n  margin-top: 3rem !important;\n}\n\n.mt-auto {\n  margin-top: auto !important;\n}\n\n.me-0 {\n  margin-right: 0 !important;\n}\n\n.me-1 {\n  margin-right: 0.25rem !important;\n}\n\n.me-2 {\n  margin-right: 0.5rem !important;\n}\n\n.me-3 {\n  margin-right: 1rem !important;\n}\n\n.me-4 {\n  margin-right: 1.5rem !important;\n}\n\n.me-5 {\n  margin-right: 3rem !important;\n}\n\n.me-auto {\n  margin-right: auto !important;\n}\n\n.mb-0 {\n  margin-bottom: 0 !important;\n}\n\n.mb-1 {\n  margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n  margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n  margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n  margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n  margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n  margin-bottom: auto !important;\n}\n\n.ms-0 {\n  margin-left: 0 !important;\n}\n\n.ms-1 {\n  margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n  margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n  margin-left: 1rem !important;\n}\n\n.ms-4 {\n  margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n  margin-left: 3rem !important;\n}\n\n.ms-auto {\n  margin-left: auto !important;\n}\n\n.p-0 {\n  padding: 0 !important;\n}\n\n.p-1 {\n  padding: 0.25rem !important;\n}\n\n.p-2 {\n  padding: 0.5rem !important;\n}\n\n.p-3 {\n  padding: 1rem !important;\n}\n\n.p-4 {\n  padding: 1.5rem !important;\n}\n\n.p-5 {\n  padding: 3rem !important;\n}\n\n.px-0 {\n  padding-right: 0 !important;\n  padding-left: 0 !important;\n}\n\n.px-1 {\n  padding-right: 0.25rem !important;\n  padding-left: 0.25rem !important;\n}\n\n.px-2 {\n  padding-right: 0.5rem !important;\n  padding-left: 0.5rem !important;\n}\n\n.px-3 {\n  padding-right: 1rem !important;\n  padding-left: 1rem !important;\n}\n\n.px-4 {\n  padding-right: 1.5rem !important;\n  padding-left: 1.5rem !important;\n}\n\n.px-5 {\n  padding-right: 3rem !important;\n  padding-left: 3rem !important;\n}\n\n.py-0 {\n  padding-top: 0 !important;\n  padding-bottom: 0 !important;\n}\n\n.py-1 {\n  padding-top: 0.25rem !important;\n  padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n  padding-top: 0.5rem !important;\n  padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n  padding-top: 1rem !important;\n  padding-bottom: 1rem !important;\n}\n\n.py-4 {\n  padding-top: 1.5rem !important;\n  padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n  padding-top: 3rem !important;\n  padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n  padding-top: 0 !important;\n}\n\n.pt-1 {\n  padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n  padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n  padding-top: 1rem !important;\n}\n\n.pt-4 {\n  padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n  padding-top: 3rem !important;\n}\n\n.pe-0 {\n  padding-right: 0 !important;\n}\n\n.pe-1 {\n  padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n  padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n  padding-right: 1rem !important;\n}\n\n.pe-4 {\n  padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n  padding-right: 3rem !important;\n}\n\n.pb-0 {\n  padding-bottom: 0 !important;\n}\n\n.pb-1 {\n  padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n  padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n  padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n  padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n  padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n  padding-left: 0 !important;\n}\n\n.ps-1 {\n  padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n  padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n  padding-left: 1rem !important;\n}\n\n.ps-4 {\n  padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n  padding-left: 3rem !important;\n}\n\n.gap-0 {\n  gap: 0 !important;\n}\n\n.gap-1 {\n  gap: 0.25rem !important;\n}\n\n.gap-2 {\n  gap: 0.5rem !important;\n}\n\n.gap-3 {\n  gap: 1rem !important;\n}\n\n.gap-4 {\n  gap: 1.5rem !important;\n}\n\n.gap-5 {\n  gap: 3rem !important;\n}\n\n.row-gap-0 {\n  row-gap: 0 !important;\n}\n\n.row-gap-1 {\n  row-gap: 0.25rem !important;\n}\n\n.row-gap-2 {\n  row-gap: 0.5rem !important;\n}\n\n.row-gap-3 {\n  row-gap: 1rem !important;\n}\n\n.row-gap-4 {\n  row-gap: 1.5rem !important;\n}\n\n.row-gap-5 {\n  row-gap: 3rem !important;\n}\n\n.column-gap-0 {\n  -moz-column-gap: 0 !important;\n  column-gap: 0 !important;\n}\n\n.column-gap-1 {\n  -moz-column-gap: 0.25rem !important;\n  column-gap: 0.25rem !important;\n}\n\n.column-gap-2 {\n  -moz-column-gap: 0.5rem !important;\n  column-gap: 0.5rem !important;\n}\n\n.column-gap-3 {\n  -moz-column-gap: 1rem !important;\n  column-gap: 1rem !important;\n}\n\n.column-gap-4 {\n  -moz-column-gap: 1.5rem !important;\n  column-gap: 1.5rem !important;\n}\n\n.column-gap-5 {\n  -moz-column-gap: 3rem !important;\n  column-gap: 3rem !important;\n}\n\n.font-monospace {\n  font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n  font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n  font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n  font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n  font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n  font-size: 1.25rem !important;\n}\n\n.fs-6 {\n  font-size: 1rem !important;\n}\n\n.fst-italic {\n  font-style: italic !important;\n}\n\n.fst-normal {\n  font-style: normal !important;\n}\n\n.fw-lighter {\n  font-weight: lighter !important;\n}\n\n.fw-light {\n  font-weight: 300 !important;\n}\n\n.fw-normal {\n  font-weight: 400 !important;\n}\n\n.fw-medium {\n  font-weight: 500 !important;\n}\n\n.fw-semibold {\n  font-weight: 600 !important;\n}\n\n.fw-bold {\n  font-weight: 700 !important;\n}\n\n.fw-bolder {\n  font-weight: bolder !important;\n}\n\n.lh-1 {\n  line-height: 1 !important;\n}\n\n.lh-sm {\n  line-height: 1.25 !important;\n}\n\n.lh-base {\n  line-height: 1.5 !important;\n}\n\n.lh-lg {\n  line-height: 2 !important;\n}\n\n.text-start {\n  text-align: left !important;\n}\n\n.text-end {\n  text-align: right !important;\n}\n\n.text-center {\n  text-align: center !important;\n}\n\n.text-decoration-none {\n  text-decoration: none !important;\n}\n\n.text-decoration-underline {\n  text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n  text-decoration: line-through !important;\n}\n\n.text-lowercase {\n  text-transform: lowercase !important;\n}\n\n.text-uppercase {\n  text-transform: uppercase !important;\n}\n\n.text-capitalize {\n  text-transform: capitalize !important;\n}\n\n.text-wrap {\n  white-space: normal !important;\n}\n\n.text-nowrap {\n  white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n  word-wrap: break-word !important;\n  word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n  --bs-text-opacity: 1;\n  color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n  --bs-text-opacity: 1;\n  color: var(--bs-secondary-color) !important;\n}\n\n.text-black-50 {\n  --bs-text-opacity: 1;\n  color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n  --bs-text-opacity: 1;\n  color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-body-secondary {\n  --bs-text-opacity: 1;\n  color: var(--bs-secondary-color) !important;\n}\n\n.text-body-tertiary {\n  --bs-text-opacity: 1;\n  color: var(--bs-tertiary-color) !important;\n}\n\n.text-body-emphasis {\n  --bs-text-opacity: 1;\n  color: var(--bs-emphasis-color) !important;\n}\n\n.text-reset {\n  --bs-text-opacity: 1;\n  color: inherit !important;\n}\n\n.text-opacity-25 {\n  --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n  --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n  --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n  --bs-text-opacity: 1;\n}\n\n.text-primary-emphasis {\n  color: var(--bs-primary-text-emphasis) !important;\n}\n\n.text-secondary-emphasis {\n  color: var(--bs-secondary-text-emphasis) !important;\n}\n\n.text-success-emphasis {\n  color: var(--bs-success-text-emphasis) !important;\n}\n\n.text-info-emphasis {\n  color: var(--bs-info-text-emphasis) !important;\n}\n\n.text-warning-emphasis {\n  color: var(--bs-warning-text-emphasis) !important;\n}\n\n.text-danger-emphasis {\n  color: var(--bs-danger-text-emphasis) !important;\n}\n\n.text-light-emphasis {\n  color: var(--bs-light-text-emphasis) !important;\n}\n\n.text-dark-emphasis {\n  color: var(--bs-dark-text-emphasis) !important;\n}\n\n.link-opacity-10 {\n  --bs-link-opacity: 0.1;\n}\n\n.link-opacity-10-hover:hover {\n  --bs-link-opacity: 0.1;\n}\n\n.link-opacity-25 {\n  --bs-link-opacity: 0.25;\n}\n\n.link-opacity-25-hover:hover {\n  --bs-link-opacity: 0.25;\n}\n\n.link-opacity-50 {\n  --bs-link-opacity: 0.5;\n}\n\n.link-opacity-50-hover:hover {\n  --bs-link-opacity: 0.5;\n}\n\n.link-opacity-75 {\n  --bs-link-opacity: 0.75;\n}\n\n.link-opacity-75-hover:hover {\n  --bs-link-opacity: 0.75;\n}\n\n.link-opacity-100 {\n  --bs-link-opacity: 1;\n}\n\n.link-opacity-100-hover:hover {\n  --bs-link-opacity: 1;\n}\n\n.link-offset-1 {\n  text-underline-offset: 0.125em !important;\n}\n\n.link-offset-1-hover:hover {\n  text-underline-offset: 0.125em !important;\n}\n\n.link-offset-2 {\n  text-underline-offset: 0.25em !important;\n}\n\n.link-offset-2-hover:hover {\n  text-underline-offset: 0.25em !important;\n}\n\n.link-offset-3 {\n  text-underline-offset: 0.375em !important;\n}\n\n.link-offset-3-hover:hover {\n  text-underline-offset: 0.375em !important;\n}\n\n.link-underline-primary {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-secondary {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-success {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-info {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-warning {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-danger {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-light {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-dark {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important;\n  text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline {\n  --bs-link-underline-opacity: 1;\n  -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n  text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-underline-opacity-0 {\n  --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-0-hover:hover {\n  --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-10 {\n  --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-10-hover:hover {\n  --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-25 {\n  --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-25-hover:hover {\n  --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-50 {\n  --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-50-hover:hover {\n  --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-75 {\n  --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-75-hover:hover {\n  --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-100 {\n  --bs-link-underline-opacity: 1;\n}\n\n.link-underline-opacity-100-hover:hover {\n  --bs-link-underline-opacity: 1;\n}\n\n.bg-primary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n  --bs-bg-opacity: 1;\n  background-color: transparent !important;\n}\n\n.bg-body-secondary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body-tertiary {\n  --bs-bg-opacity: 1;\n  background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-opacity-10 {\n  --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n  --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n  --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n  --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n  --bs-bg-opacity: 1;\n}\n\n.bg-primary-subtle {\n  background-color: var(--bs-primary-bg-subtle) !important;\n}\n\n.bg-secondary-subtle {\n  background-color: var(--bs-secondary-bg-subtle) !important;\n}\n\n.bg-success-subtle {\n  background-color: var(--bs-success-bg-subtle) !important;\n}\n\n.bg-info-subtle {\n  background-color: var(--bs-info-bg-subtle) !important;\n}\n\n.bg-warning-subtle {\n  background-color: var(--bs-warning-bg-subtle) !important;\n}\n\n.bg-danger-subtle {\n  background-color: var(--bs-danger-bg-subtle) !important;\n}\n\n.bg-light-subtle {\n  background-color: var(--bs-light-bg-subtle) !important;\n}\n\n.bg-dark-subtle {\n  background-color: var(--bs-dark-bg-subtle) !important;\n}\n\n.bg-gradient {\n  background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n  -webkit-user-select: all !important;\n  -moz-user-select: all !important;\n  user-select: all !important;\n}\n\n.user-select-auto {\n  -webkit-user-select: auto !important;\n  -moz-user-select: auto !important;\n  user-select: auto !important;\n}\n\n.user-select-none {\n  -webkit-user-select: none !important;\n  -moz-user-select: none !important;\n  user-select: none !important;\n}\n\n.pe-none {\n  pointer-events: none !important;\n}\n\n.pe-auto {\n  pointer-events: auto !important;\n}\n\n.rounded {\n  border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-0 {\n  border-radius: 0 !important;\n}\n\n.rounded-1 {\n  border-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-2 {\n  border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-3 {\n  border-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-4 {\n  border-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-5 {\n  border-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-circle {\n  border-radius: 50% !important;\n}\n\n.rounded-pill {\n  border-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-top {\n  border-top-left-radius: var(--bs-border-radius) !important;\n  border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-0 {\n  border-top-left-radius: 0 !important;\n  border-top-right-radius: 0 !important;\n}\n\n.rounded-top-1 {\n  border-top-left-radius: var(--bs-border-radius-sm) !important;\n  border-top-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-top-2 {\n  border-top-left-radius: var(--bs-border-radius) !important;\n  border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-3 {\n  border-top-left-radius: var(--bs-border-radius-lg) !important;\n  border-top-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-top-4 {\n  border-top-left-radius: var(--bs-border-radius-xl) !important;\n  border-top-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-top-5 {\n  border-top-left-radius: var(--bs-border-radius-xxl) !important;\n  border-top-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-top-circle {\n  border-top-left-radius: 50% !important;\n  border-top-right-radius: 50% !important;\n}\n\n.rounded-top-pill {\n  border-top-left-radius: var(--bs-border-radius-pill) !important;\n  border-top-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-end {\n  border-top-right-radius: var(--bs-border-radius) !important;\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-0 {\n  border-top-right-radius: 0 !important;\n  border-bottom-right-radius: 0 !important;\n}\n\n.rounded-end-1 {\n  border-top-right-radius: var(--bs-border-radius-sm) !important;\n  border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-end-2 {\n  border-top-right-radius: var(--bs-border-radius) !important;\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-3 {\n  border-top-right-radius: var(--bs-border-radius-lg) !important;\n  border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-end-4 {\n  border-top-right-radius: var(--bs-border-radius-xl) !important;\n  border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-end-5 {\n  border-top-right-radius: var(--bs-border-radius-xxl) !important;\n  border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-end-circle {\n  border-top-right-radius: 50% !important;\n  border-bottom-right-radius: 50% !important;\n}\n\n.rounded-end-pill {\n  border-top-right-radius: var(--bs-border-radius-pill) !important;\n  border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-bottom {\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-0 {\n  border-bottom-right-radius: 0 !important;\n  border-bottom-left-radius: 0 !important;\n}\n\n.rounded-bottom-1 {\n  border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n  border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-bottom-2 {\n  border-bottom-right-radius: var(--bs-border-radius) !important;\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-3 {\n  border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n  border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-bottom-4 {\n  border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n  border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-bottom-5 {\n  border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n  border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-bottom-circle {\n  border-bottom-right-radius: 50% !important;\n  border-bottom-left-radius: 50% !important;\n}\n\n.rounded-bottom-pill {\n  border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n  border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-start {\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n  border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-0 {\n  border-bottom-left-radius: 0 !important;\n  border-top-left-radius: 0 !important;\n}\n\n.rounded-start-1 {\n  border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n  border-top-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-start-2 {\n  border-bottom-left-radius: var(--bs-border-radius) !important;\n  border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-3 {\n  border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n  border-top-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-start-4 {\n  border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n  border-top-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-start-5 {\n  border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n  border-top-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-start-circle {\n  border-bottom-left-radius: 50% !important;\n  border-top-left-radius: 50% !important;\n}\n\n.rounded-start-pill {\n  border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n  border-top-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.visible {\n  visibility: visible !important;\n}\n\n.invisible {\n  visibility: hidden !important;\n}\n\n.z-n1 {\n  z-index: -1 !important;\n}\n\n.z-0 {\n  z-index: 0 !important;\n}\n\n.z-1 {\n  z-index: 1 !important;\n}\n\n.z-2 {\n  z-index: 2 !important;\n}\n\n.z-3 {\n  z-index: 3 !important;\n}\n\n@media (min-width: 576px) {\n  .float-sm-start {\n    float: left !important;\n  }\n  .float-sm-end {\n    float: right !important;\n  }\n  .float-sm-none {\n    float: none !important;\n  }\n  .object-fit-sm-contain {\n    -o-object-fit: contain !important;\n    object-fit: contain !important;\n  }\n  .object-fit-sm-cover {\n    -o-object-fit: cover !important;\n    object-fit: cover !important;\n  }\n  .object-fit-sm-fill {\n    -o-object-fit: fill !important;\n    object-fit: fill !important;\n  }\n  .object-fit-sm-scale {\n    -o-object-fit: scale-down !important;\n    object-fit: scale-down !important;\n  }\n  .object-fit-sm-none {\n    -o-object-fit: none !important;\n    object-fit: none !important;\n  }\n  .d-sm-inline {\n    display: inline !important;\n  }\n  .d-sm-inline-block {\n    display: inline-block !important;\n  }\n  .d-sm-block {\n    display: block !important;\n  }\n  .d-sm-grid {\n    display: grid !important;\n  }\n  .d-sm-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-sm-table {\n    display: table !important;\n  }\n  .d-sm-table-row {\n    display: table-row !important;\n  }\n  .d-sm-table-cell {\n    display: table-cell !important;\n  }\n  .d-sm-flex {\n    display: flex !important;\n  }\n  .d-sm-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-sm-none {\n    display: none !important;\n  }\n  .flex-sm-fill {\n    flex: 1 1 auto !important;\n  }\n  .flex-sm-row {\n    flex-direction: row !important;\n  }\n  .flex-sm-column {\n    flex-direction: column !important;\n  }\n  .flex-sm-row-reverse {\n    flex-direction: row-reverse !important;\n  }\n  .flex-sm-column-reverse {\n    flex-direction: column-reverse !important;\n  }\n  .flex-sm-grow-0 {\n    flex-grow: 0 !important;\n  }\n  .flex-sm-grow-1 {\n    flex-grow: 1 !important;\n  }\n  .flex-sm-shrink-0 {\n    flex-shrink: 0 !important;\n  }\n  .flex-sm-shrink-1 {\n    flex-shrink: 1 !important;\n  }\n  .flex-sm-wrap {\n    flex-wrap: wrap !important;\n  }\n  .flex-sm-nowrap {\n    flex-wrap: nowrap !important;\n  }\n  .flex-sm-wrap-reverse {\n    flex-wrap: wrap-reverse !important;\n  }\n  .justify-content-sm-start {\n    justify-content: flex-start !important;\n  }\n  .justify-content-sm-end {\n    justify-content: flex-end !important;\n  }\n  .justify-content-sm-center {\n    justify-content: center !important;\n  }\n  .justify-content-sm-between {\n    justify-content: space-between !important;\n  }\n  .justify-content-sm-around {\n    justify-content: space-around !important;\n  }\n  .justify-content-sm-evenly {\n    justify-content: space-evenly !important;\n  }\n  .align-items-sm-start {\n    align-items: flex-start !important;\n  }\n  .align-items-sm-end {\n    align-items: flex-end !important;\n  }\n  .align-items-sm-center {\n    align-items: center !important;\n  }\n  .align-items-sm-baseline {\n    align-items: baseline !important;\n  }\n  .align-items-sm-stretch {\n    align-items: stretch !important;\n  }\n  .align-content-sm-start {\n    align-content: flex-start !important;\n  }\n  .align-content-sm-end {\n    align-content: flex-end !important;\n  }\n  .align-content-sm-center {\n    align-content: center !important;\n  }\n  .align-content-sm-between {\n    align-content: space-between !important;\n  }\n  .align-content-sm-around {\n    align-content: space-around !important;\n  }\n  .align-content-sm-stretch {\n    align-content: stretch !important;\n  }\n  .align-self-sm-auto {\n    align-self: auto !important;\n  }\n  .align-self-sm-start {\n    align-self: flex-start !important;\n  }\n  .align-self-sm-end {\n    align-self: flex-end !important;\n  }\n  .align-self-sm-center {\n    align-self: center !important;\n  }\n  .align-self-sm-baseline {\n    align-self: baseline !important;\n  }\n  .align-self-sm-stretch {\n    align-self: stretch !important;\n  }\n  .order-sm-first {\n    order: -1 !important;\n  }\n  .order-sm-0 {\n    order: 0 !important;\n  }\n  .order-sm-1 {\n    order: 1 !important;\n  }\n  .order-sm-2 {\n    order: 2 !important;\n  }\n  .order-sm-3 {\n    order: 3 !important;\n  }\n  .order-sm-4 {\n    order: 4 !important;\n  }\n  .order-sm-5 {\n    order: 5 !important;\n  }\n  .order-sm-last {\n    order: 6 !important;\n  }\n  .m-sm-0 {\n    margin: 0 !important;\n  }\n  .m-sm-1 {\n    margin: 0.25rem !important;\n  }\n  .m-sm-2 {\n    margin: 0.5rem !important;\n  }\n  .m-sm-3 {\n    margin: 1rem !important;\n  }\n  .m-sm-4 {\n    margin: 1.5rem !important;\n  }\n  .m-sm-5 {\n    margin: 3rem !important;\n  }\n  .m-sm-auto {\n    margin: auto !important;\n  }\n  .mx-sm-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important;\n  }\n  .mx-sm-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important;\n  }\n  .mx-sm-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important;\n  }\n  .mx-sm-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important;\n  }\n  .mx-sm-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important;\n  }\n  .mx-sm-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important;\n  }\n  .mx-sm-auto {\n    margin-right: auto !important;\n    margin-left: auto !important;\n  }\n  .my-sm-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important;\n  }\n  .my-sm-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important;\n  }\n  .my-sm-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important;\n  }\n  .my-sm-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important;\n  }\n  .my-sm-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important;\n  }\n  .my-sm-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important;\n  }\n  .my-sm-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important;\n  }\n  .mt-sm-0 {\n    margin-top: 0 !important;\n  }\n  .mt-sm-1 {\n    margin-top: 0.25rem !important;\n  }\n  .mt-sm-2 {\n    margin-top: 0.5rem !important;\n  }\n  .mt-sm-3 {\n    margin-top: 1rem !important;\n  }\n  .mt-sm-4 {\n    margin-top: 1.5rem !important;\n  }\n  .mt-sm-5 {\n    margin-top: 3rem !important;\n  }\n  .mt-sm-auto {\n    margin-top: auto !important;\n  }\n  .me-sm-0 {\n    margin-right: 0 !important;\n  }\n  .me-sm-1 {\n    margin-right: 0.25rem !important;\n  }\n  .me-sm-2 {\n    margin-right: 0.5rem !important;\n  }\n  .me-sm-3 {\n    margin-right: 1rem !important;\n  }\n  .me-sm-4 {\n    margin-right: 1.5rem !important;\n  }\n  .me-sm-5 {\n    margin-right: 3rem !important;\n  }\n  .me-sm-auto {\n    margin-right: auto !important;\n  }\n  .mb-sm-0 {\n    margin-bottom: 0 !important;\n  }\n  .mb-sm-1 {\n    margin-bottom: 0.25rem !important;\n  }\n  .mb-sm-2 {\n    margin-bottom: 0.5rem !important;\n  }\n  .mb-sm-3 {\n    margin-bottom: 1rem !important;\n  }\n  .mb-sm-4 {\n    margin-bottom: 1.5rem !important;\n  }\n  .mb-sm-5 {\n    margin-bottom: 3rem !important;\n  }\n  .mb-sm-auto {\n    margin-bottom: auto !important;\n  }\n  .ms-sm-0 {\n    margin-left: 0 !important;\n  }\n  .ms-sm-1 {\n    margin-left: 0.25rem !important;\n  }\n  .ms-sm-2 {\n    margin-left: 0.5rem !important;\n  }\n  .ms-sm-3 {\n    margin-left: 1rem !important;\n  }\n  .ms-sm-4 {\n    margin-left: 1.5rem !important;\n  }\n  .ms-sm-5 {\n    margin-left: 3rem !important;\n  }\n  .ms-sm-auto {\n    margin-left: auto !important;\n  }\n  .p-sm-0 {\n    padding: 0 !important;\n  }\n  .p-sm-1 {\n    padding: 0.25rem !important;\n  }\n  .p-sm-2 {\n    padding: 0.5rem !important;\n  }\n  .p-sm-3 {\n    padding: 1rem !important;\n  }\n  .p-sm-4 {\n    padding: 1.5rem !important;\n  }\n  .p-sm-5 {\n    padding: 3rem !important;\n  }\n  .px-sm-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important;\n  }\n  .px-sm-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important;\n  }\n  .px-sm-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important;\n  }\n  .px-sm-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important;\n  }\n  .px-sm-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important;\n  }\n  .px-sm-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important;\n  }\n  .py-sm-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n  }\n  .py-sm-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important;\n  }\n  .py-sm-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important;\n  }\n  .py-sm-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n  }\n  .py-sm-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important;\n  }\n  .py-sm-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important;\n  }\n  .pt-sm-0 {\n    padding-top: 0 !important;\n  }\n  .pt-sm-1 {\n    padding-top: 0.25rem !important;\n  }\n  .pt-sm-2 {\n    padding-top: 0.5rem !important;\n  }\n  .pt-sm-3 {\n    padding-top: 1rem !important;\n  }\n  .pt-sm-4 {\n    padding-top: 1.5rem !important;\n  }\n  .pt-sm-5 {\n    padding-top: 3rem !important;\n  }\n  .pe-sm-0 {\n    padding-right: 0 !important;\n  }\n  .pe-sm-1 {\n    padding-right: 0.25rem !important;\n  }\n  .pe-sm-2 {\n    padding-right: 0.5rem !important;\n  }\n  .pe-sm-3 {\n    padding-right: 1rem !important;\n  }\n  .pe-sm-4 {\n    padding-right: 1.5rem !important;\n  }\n  .pe-sm-5 {\n    padding-right: 3rem !important;\n  }\n  .pb-sm-0 {\n    padding-bottom: 0 !important;\n  }\n  .pb-sm-1 {\n    padding-bottom: 0.25rem !important;\n  }\n  .pb-sm-2 {\n    padding-bottom: 0.5rem !important;\n  }\n  .pb-sm-3 {\n    padding-bottom: 1rem !important;\n  }\n  .pb-sm-4 {\n    padding-bottom: 1.5rem !important;\n  }\n  .pb-sm-5 {\n    padding-bottom: 3rem !important;\n  }\n  .ps-sm-0 {\n    padding-left: 0 !important;\n  }\n  .ps-sm-1 {\n    padding-left: 0.25rem !important;\n  }\n  .ps-sm-2 {\n    padding-left: 0.5rem !important;\n  }\n  .ps-sm-3 {\n    padding-left: 1rem !important;\n  }\n  .ps-sm-4 {\n    padding-left: 1.5rem !important;\n  }\n  .ps-sm-5 {\n    padding-left: 3rem !important;\n  }\n  .gap-sm-0 {\n    gap: 0 !important;\n  }\n  .gap-sm-1 {\n    gap: 0.25rem !important;\n  }\n  .gap-sm-2 {\n    gap: 0.5rem !important;\n  }\n  .gap-sm-3 {\n    gap: 1rem !important;\n  }\n  .gap-sm-4 {\n    gap: 1.5rem !important;\n  }\n  .gap-sm-5 {\n    gap: 3rem !important;\n  }\n  .row-gap-sm-0 {\n    row-gap: 0 !important;\n  }\n  .row-gap-sm-1 {\n    row-gap: 0.25rem !important;\n  }\n  .row-gap-sm-2 {\n    row-gap: 0.5rem !important;\n  }\n  .row-gap-sm-3 {\n    row-gap: 1rem !important;\n  }\n  .row-gap-sm-4 {\n    row-gap: 1.5rem !important;\n  }\n  .row-gap-sm-5 {\n    row-gap: 3rem !important;\n  }\n  .column-gap-sm-0 {\n    -moz-column-gap: 0 !important;\n    column-gap: 0 !important;\n  }\n  .column-gap-sm-1 {\n    -moz-column-gap: 0.25rem !important;\n    column-gap: 0.25rem !important;\n  }\n  .column-gap-sm-2 {\n    -moz-column-gap: 0.5rem !important;\n    column-gap: 0.5rem !important;\n  }\n  .column-gap-sm-3 {\n    -moz-column-gap: 1rem !important;\n    column-gap: 1rem !important;\n  }\n  .column-gap-sm-4 {\n    -moz-column-gap: 1.5rem !important;\n    column-gap: 1.5rem !important;\n  }\n  .column-gap-sm-5 {\n    -moz-column-gap: 3rem !important;\n    column-gap: 3rem !important;\n  }\n  .text-sm-start {\n    text-align: left !important;\n  }\n  .text-sm-end {\n    text-align: right !important;\n  }\n  .text-sm-center {\n    text-align: center !important;\n  }\n}\n@media (min-width: 768px) {\n  .float-md-start {\n    float: left !important;\n  }\n  .float-md-end {\n    float: right !important;\n  }\n  .float-md-none {\n    float: none !important;\n  }\n  .object-fit-md-contain {\n    -o-object-fit: contain !important;\n    object-fit: contain !important;\n  }\n  .object-fit-md-cover {\n    -o-object-fit: cover !important;\n    object-fit: cover !important;\n  }\n  .object-fit-md-fill {\n    -o-object-fit: fill !important;\n    object-fit: fill !important;\n  }\n  .object-fit-md-scale {\n    -o-object-fit: scale-down !important;\n    object-fit: scale-down !important;\n  }\n  .object-fit-md-none {\n    -o-object-fit: none !important;\n    object-fit: none !important;\n  }\n  .d-md-inline {\n    display: inline !important;\n  }\n  .d-md-inline-block {\n    display: inline-block !important;\n  }\n  .d-md-block {\n    display: block !important;\n  }\n  .d-md-grid {\n    display: grid !important;\n  }\n  .d-md-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-md-table {\n    display: table !important;\n  }\n  .d-md-table-row {\n    display: table-row !important;\n  }\n  .d-md-table-cell {\n    display: table-cell !important;\n  }\n  .d-md-flex {\n    display: flex !important;\n  }\n  .d-md-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-md-none {\n    display: none !important;\n  }\n  .flex-md-fill {\n    flex: 1 1 auto !important;\n  }\n  .flex-md-row {\n    flex-direction: row !important;\n  }\n  .flex-md-column {\n    flex-direction: column !important;\n  }\n  .flex-md-row-reverse {\n    flex-direction: row-reverse !important;\n  }\n  .flex-md-column-reverse {\n    flex-direction: column-reverse !important;\n  }\n  .flex-md-grow-0 {\n    flex-grow: 0 !important;\n  }\n  .flex-md-grow-1 {\n    flex-grow: 1 !important;\n  }\n  .flex-md-shrink-0 {\n    flex-shrink: 0 !important;\n  }\n  .flex-md-shrink-1 {\n    flex-shrink: 1 !important;\n  }\n  .flex-md-wrap {\n    flex-wrap: wrap !important;\n  }\n  .flex-md-nowrap {\n    flex-wrap: nowrap !important;\n  }\n  .flex-md-wrap-reverse {\n    flex-wrap: wrap-reverse !important;\n  }\n  .justify-content-md-start {\n    justify-content: flex-start !important;\n  }\n  .justify-content-md-end {\n    justify-content: flex-end !important;\n  }\n  .justify-content-md-center {\n    justify-content: center !important;\n  }\n  .justify-content-md-between {\n    justify-content: space-between !important;\n  }\n  .justify-content-md-around {\n    justify-content: space-around !important;\n  }\n  .justify-content-md-evenly {\n    justify-content: space-evenly !important;\n  }\n  .align-items-md-start {\n    align-items: flex-start !important;\n  }\n  .align-items-md-end {\n    align-items: flex-end !important;\n  }\n  .align-items-md-center {\n    align-items: center !important;\n  }\n  .align-items-md-baseline {\n    align-items: baseline !important;\n  }\n  .align-items-md-stretch {\n    align-items: stretch !important;\n  }\n  .align-content-md-start {\n    align-content: flex-start !important;\n  }\n  .align-content-md-end {\n    align-content: flex-end !important;\n  }\n  .align-content-md-center {\n    align-content: center !important;\n  }\n  .align-content-md-between {\n    align-content: space-between !important;\n  }\n  .align-content-md-around {\n    align-content: space-around !important;\n  }\n  .align-content-md-stretch {\n    align-content: stretch !important;\n  }\n  .align-self-md-auto {\n    align-self: auto !important;\n  }\n  .align-self-md-start {\n    align-self: flex-start !important;\n  }\n  .align-self-md-end {\n    align-self: flex-end !important;\n  }\n  .align-self-md-center {\n    align-self: center !important;\n  }\n  .align-self-md-baseline {\n    align-self: baseline !important;\n  }\n  .align-self-md-stretch {\n    align-self: stretch !important;\n  }\n  .order-md-first {\n    order: -1 !important;\n  }\n  .order-md-0 {\n    order: 0 !important;\n  }\n  .order-md-1 {\n    order: 1 !important;\n  }\n  .order-md-2 {\n    order: 2 !important;\n  }\n  .order-md-3 {\n    order: 3 !important;\n  }\n  .order-md-4 {\n    order: 4 !important;\n  }\n  .order-md-5 {\n    order: 5 !important;\n  }\n  .order-md-last {\n    order: 6 !important;\n  }\n  .m-md-0 {\n    margin: 0 !important;\n  }\n  .m-md-1 {\n    margin: 0.25rem !important;\n  }\n  .m-md-2 {\n    margin: 0.5rem !important;\n  }\n  .m-md-3 {\n    margin: 1rem !important;\n  }\n  .m-md-4 {\n    margin: 1.5rem !important;\n  }\n  .m-md-5 {\n    margin: 3rem !important;\n  }\n  .m-md-auto {\n    margin: auto !important;\n  }\n  .mx-md-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important;\n  }\n  .mx-md-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important;\n  }\n  .mx-md-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important;\n  }\n  .mx-md-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important;\n  }\n  .mx-md-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important;\n  }\n  .mx-md-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important;\n  }\n  .mx-md-auto {\n    margin-right: auto !important;\n    margin-left: auto !important;\n  }\n  .my-md-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important;\n  }\n  .my-md-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important;\n  }\n  .my-md-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important;\n  }\n  .my-md-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important;\n  }\n  .my-md-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important;\n  }\n  .my-md-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important;\n  }\n  .my-md-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important;\n  }\n  .mt-md-0 {\n    margin-top: 0 !important;\n  }\n  .mt-md-1 {\n    margin-top: 0.25rem !important;\n  }\n  .mt-md-2 {\n    margin-top: 0.5rem !important;\n  }\n  .mt-md-3 {\n    margin-top: 1rem !important;\n  }\n  .mt-md-4 {\n    margin-top: 1.5rem !important;\n  }\n  .mt-md-5 {\n    margin-top: 3rem !important;\n  }\n  .mt-md-auto {\n    margin-top: auto !important;\n  }\n  .me-md-0 {\n    margin-right: 0 !important;\n  }\n  .me-md-1 {\n    margin-right: 0.25rem !important;\n  }\n  .me-md-2 {\n    margin-right: 0.5rem !important;\n  }\n  .me-md-3 {\n    margin-right: 1rem !important;\n  }\n  .me-md-4 {\n    margin-right: 1.5rem !important;\n  }\n  .me-md-5 {\n    margin-right: 3rem !important;\n  }\n  .me-md-auto {\n    margin-right: auto !important;\n  }\n  .mb-md-0 {\n    margin-bottom: 0 !important;\n  }\n  .mb-md-1 {\n    margin-bottom: 0.25rem !important;\n  }\n  .mb-md-2 {\n    margin-bottom: 0.5rem !important;\n  }\n  .mb-md-3 {\n    margin-bottom: 1rem !important;\n  }\n  .mb-md-4 {\n    margin-bottom: 1.5rem !important;\n  }\n  .mb-md-5 {\n    margin-bottom: 3rem !important;\n  }\n  .mb-md-auto {\n    margin-bottom: auto !important;\n  }\n  .ms-md-0 {\n    margin-left: 0 !important;\n  }\n  .ms-md-1 {\n    margin-left: 0.25rem !important;\n  }\n  .ms-md-2 {\n    margin-left: 0.5rem !important;\n  }\n  .ms-md-3 {\n    margin-left: 1rem !important;\n  }\n  .ms-md-4 {\n    margin-left: 1.5rem !important;\n  }\n  .ms-md-5 {\n    margin-left: 3rem !important;\n  }\n  .ms-md-auto {\n    margin-left: auto !important;\n  }\n  .p-md-0 {\n    padding: 0 !important;\n  }\n  .p-md-1 {\n    padding: 0.25rem !important;\n  }\n  .p-md-2 {\n    padding: 0.5rem !important;\n  }\n  .p-md-3 {\n    padding: 1rem !important;\n  }\n  .p-md-4 {\n    padding: 1.5rem !important;\n  }\n  .p-md-5 {\n    padding: 3rem !important;\n  }\n  .px-md-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important;\n  }\n  .px-md-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important;\n  }\n  .px-md-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important;\n  }\n  .px-md-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important;\n  }\n  .px-md-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important;\n  }\n  .px-md-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important;\n  }\n  .py-md-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n  }\n  .py-md-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important;\n  }\n  .py-md-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important;\n  }\n  .py-md-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n  }\n  .py-md-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important;\n  }\n  .py-md-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important;\n  }\n  .pt-md-0 {\n    padding-top: 0 !important;\n  }\n  .pt-md-1 {\n    padding-top: 0.25rem !important;\n  }\n  .pt-md-2 {\n    padding-top: 0.5rem !important;\n  }\n  .pt-md-3 {\n    padding-top: 1rem !important;\n  }\n  .pt-md-4 {\n    padding-top: 1.5rem !important;\n  }\n  .pt-md-5 {\n    padding-top: 3rem !important;\n  }\n  .pe-md-0 {\n    padding-right: 0 !important;\n  }\n  .pe-md-1 {\n    padding-right: 0.25rem !important;\n  }\n  .pe-md-2 {\n    padding-right: 0.5rem !important;\n  }\n  .pe-md-3 {\n    padding-right: 1rem !important;\n  }\n  .pe-md-4 {\n    padding-right: 1.5rem !important;\n  }\n  .pe-md-5 {\n    padding-right: 3rem !important;\n  }\n  .pb-md-0 {\n    padding-bottom: 0 !important;\n  }\n  .pb-md-1 {\n    padding-bottom: 0.25rem !important;\n  }\n  .pb-md-2 {\n    padding-bottom: 0.5rem !important;\n  }\n  .pb-md-3 {\n    padding-bottom: 1rem !important;\n  }\n  .pb-md-4 {\n    padding-bottom: 1.5rem !important;\n  }\n  .pb-md-5 {\n    padding-bottom: 3rem !important;\n  }\n  .ps-md-0 {\n    padding-left: 0 !important;\n  }\n  .ps-md-1 {\n    padding-left: 0.25rem !important;\n  }\n  .ps-md-2 {\n    padding-left: 0.5rem !important;\n  }\n  .ps-md-3 {\n    padding-left: 1rem !important;\n  }\n  .ps-md-4 {\n    padding-left: 1.5rem !important;\n  }\n  .ps-md-5 {\n    padding-left: 3rem !important;\n  }\n  .gap-md-0 {\n    gap: 0 !important;\n  }\n  .gap-md-1 {\n    gap: 0.25rem !important;\n  }\n  .gap-md-2 {\n    gap: 0.5rem !important;\n  }\n  .gap-md-3 {\n    gap: 1rem !important;\n  }\n  .gap-md-4 {\n    gap: 1.5rem !important;\n  }\n  .gap-md-5 {\n    gap: 3rem !important;\n  }\n  .row-gap-md-0 {\n    row-gap: 0 !important;\n  }\n  .row-gap-md-1 {\n    row-gap: 0.25rem !important;\n  }\n  .row-gap-md-2 {\n    row-gap: 0.5rem !important;\n  }\n  .row-gap-md-3 {\n    row-gap: 1rem !important;\n  }\n  .row-gap-md-4 {\n    row-gap: 1.5rem !important;\n  }\n  .row-gap-md-5 {\n    row-gap: 3rem !important;\n  }\n  .column-gap-md-0 {\n    -moz-column-gap: 0 !important;\n    column-gap: 0 !important;\n  }\n  .column-gap-md-1 {\n    -moz-column-gap: 0.25rem !important;\n    column-gap: 0.25rem !important;\n  }\n  .column-gap-md-2 {\n    -moz-column-gap: 0.5rem !important;\n    column-gap: 0.5rem !important;\n  }\n  .column-gap-md-3 {\n    -moz-column-gap: 1rem !important;\n    column-gap: 1rem !important;\n  }\n  .column-gap-md-4 {\n    -moz-column-gap: 1.5rem !important;\n    column-gap: 1.5rem !important;\n  }\n  .column-gap-md-5 {\n    -moz-column-gap: 3rem !important;\n    column-gap: 3rem !important;\n  }\n  .text-md-start {\n    text-align: left !important;\n  }\n  .text-md-end {\n    text-align: right !important;\n  }\n  .text-md-center {\n    text-align: center !important;\n  }\n}\n@media (min-width: 992px) {\n  .float-lg-start {\n    float: left !important;\n  }\n  .float-lg-end {\n    float: right !important;\n  }\n  .float-lg-none {\n    float: none !important;\n  }\n  .object-fit-lg-contain {\n    -o-object-fit: contain !important;\n    object-fit: contain !important;\n  }\n  .object-fit-lg-cover {\n    -o-object-fit: cover !important;\n    object-fit: cover !important;\n  }\n  .object-fit-lg-fill {\n    -o-object-fit: fill !important;\n    object-fit: fill !important;\n  }\n  .object-fit-lg-scale {\n    -o-object-fit: scale-down !important;\n    object-fit: scale-down !important;\n  }\n  .object-fit-lg-none {\n    -o-object-fit: none !important;\n    object-fit: none !important;\n  }\n  .d-lg-inline {\n    display: inline !important;\n  }\n  .d-lg-inline-block {\n    display: inline-block !important;\n  }\n  .d-lg-block {\n    display: block !important;\n  }\n  .d-lg-grid {\n    display: grid !important;\n  }\n  .d-lg-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-lg-table {\n    display: table !important;\n  }\n  .d-lg-table-row {\n    display: table-row !important;\n  }\n  .d-lg-table-cell {\n    display: table-cell !important;\n  }\n  .d-lg-flex {\n    display: flex !important;\n  }\n  .d-lg-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-lg-none {\n    display: none !important;\n  }\n  .flex-lg-fill {\n    flex: 1 1 auto !important;\n  }\n  .flex-lg-row {\n    flex-direction: row !important;\n  }\n  .flex-lg-column {\n    flex-direction: column !important;\n  }\n  .flex-lg-row-reverse {\n    flex-direction: row-reverse !important;\n  }\n  .flex-lg-column-reverse {\n    flex-direction: column-reverse !important;\n  }\n  .flex-lg-grow-0 {\n    flex-grow: 0 !important;\n  }\n  .flex-lg-grow-1 {\n    flex-grow: 1 !important;\n  }\n  .flex-lg-shrink-0 {\n    flex-shrink: 0 !important;\n  }\n  .flex-lg-shrink-1 {\n    flex-shrink: 1 !important;\n  }\n  .flex-lg-wrap {\n    flex-wrap: wrap !important;\n  }\n  .flex-lg-nowrap {\n    flex-wrap: nowrap !important;\n  }\n  .flex-lg-wrap-reverse {\n    flex-wrap: wrap-reverse !important;\n  }\n  .justify-content-lg-start {\n    justify-content: flex-start !important;\n  }\n  .justify-content-lg-end {\n    justify-content: flex-end !important;\n  }\n  .justify-content-lg-center {\n    justify-content: center !important;\n  }\n  .justify-content-lg-between {\n    justify-content: space-between !important;\n  }\n  .justify-content-lg-around {\n    justify-content: space-around !important;\n  }\n  .justify-content-lg-evenly {\n    justify-content: space-evenly !important;\n  }\n  .align-items-lg-start {\n    align-items: flex-start !important;\n  }\n  .align-items-lg-end {\n    align-items: flex-end !important;\n  }\n  .align-items-lg-center {\n    align-items: center !important;\n  }\n  .align-items-lg-baseline {\n    align-items: baseline !important;\n  }\n  .align-items-lg-stretch {\n    align-items: stretch !important;\n  }\n  .align-content-lg-start {\n    align-content: flex-start !important;\n  }\n  .align-content-lg-end {\n    align-content: flex-end !important;\n  }\n  .align-content-lg-center {\n    align-content: center !important;\n  }\n  .align-content-lg-between {\n    align-content: space-between !important;\n  }\n  .align-content-lg-around {\n    align-content: space-around !important;\n  }\n  .align-content-lg-stretch {\n    align-content: stretch !important;\n  }\n  .align-self-lg-auto {\n    align-self: auto !important;\n  }\n  .align-self-lg-start {\n    align-self: flex-start !important;\n  }\n  .align-self-lg-end {\n    align-self: flex-end !important;\n  }\n  .align-self-lg-center {\n    align-self: center !important;\n  }\n  .align-self-lg-baseline {\n    align-self: baseline !important;\n  }\n  .align-self-lg-stretch {\n    align-self: stretch !important;\n  }\n  .order-lg-first {\n    order: -1 !important;\n  }\n  .order-lg-0 {\n    order: 0 !important;\n  }\n  .order-lg-1 {\n    order: 1 !important;\n  }\n  .order-lg-2 {\n    order: 2 !important;\n  }\n  .order-lg-3 {\n    order: 3 !important;\n  }\n  .order-lg-4 {\n    order: 4 !important;\n  }\n  .order-lg-5 {\n    order: 5 !important;\n  }\n  .order-lg-last {\n    order: 6 !important;\n  }\n  .m-lg-0 {\n    margin: 0 !important;\n  }\n  .m-lg-1 {\n    margin: 0.25rem !important;\n  }\n  .m-lg-2 {\n    margin: 0.5rem !important;\n  }\n  .m-lg-3 {\n    margin: 1rem !important;\n  }\n  .m-lg-4 {\n    margin: 1.5rem !important;\n  }\n  .m-lg-5 {\n    margin: 3rem !important;\n  }\n  .m-lg-auto {\n    margin: auto !important;\n  }\n  .mx-lg-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important;\n  }\n  .mx-lg-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important;\n  }\n  .mx-lg-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important;\n  }\n  .mx-lg-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important;\n  }\n  .mx-lg-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important;\n  }\n  .mx-lg-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important;\n  }\n  .mx-lg-auto {\n    margin-right: auto !important;\n    margin-left: auto !important;\n  }\n  .my-lg-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important;\n  }\n  .my-lg-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important;\n  }\n  .my-lg-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important;\n  }\n  .my-lg-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important;\n  }\n  .my-lg-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important;\n  }\n  .my-lg-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important;\n  }\n  .my-lg-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important;\n  }\n  .mt-lg-0 {\n    margin-top: 0 !important;\n  }\n  .mt-lg-1 {\n    margin-top: 0.25rem !important;\n  }\n  .mt-lg-2 {\n    margin-top: 0.5rem !important;\n  }\n  .mt-lg-3 {\n    margin-top: 1rem !important;\n  }\n  .mt-lg-4 {\n    margin-top: 1.5rem !important;\n  }\n  .mt-lg-5 {\n    margin-top: 3rem !important;\n  }\n  .mt-lg-auto {\n    margin-top: auto !important;\n  }\n  .me-lg-0 {\n    margin-right: 0 !important;\n  }\n  .me-lg-1 {\n    margin-right: 0.25rem !important;\n  }\n  .me-lg-2 {\n    margin-right: 0.5rem !important;\n  }\n  .me-lg-3 {\n    margin-right: 1rem !important;\n  }\n  .me-lg-4 {\n    margin-right: 1.5rem !important;\n  }\n  .me-lg-5 {\n    margin-right: 3rem !important;\n  }\n  .me-lg-auto {\n    margin-right: auto !important;\n  }\n  .mb-lg-0 {\n    margin-bottom: 0 !important;\n  }\n  .mb-lg-1 {\n    margin-bottom: 0.25rem !important;\n  }\n  .mb-lg-2 {\n    margin-bottom: 0.5rem !important;\n  }\n  .mb-lg-3 {\n    margin-bottom: 1rem !important;\n  }\n  .mb-lg-4 {\n    margin-bottom: 1.5rem !important;\n  }\n  .mb-lg-5 {\n    margin-bottom: 3rem !important;\n  }\n  .mb-lg-auto {\n    margin-bottom: auto !important;\n  }\n  .ms-lg-0 {\n    margin-left: 0 !important;\n  }\n  .ms-lg-1 {\n    margin-left: 0.25rem !important;\n  }\n  .ms-lg-2 {\n    margin-left: 0.5rem !important;\n  }\n  .ms-lg-3 {\n    margin-left: 1rem !important;\n  }\n  .ms-lg-4 {\n    margin-left: 1.5rem !important;\n  }\n  .ms-lg-5 {\n    margin-left: 3rem !important;\n  }\n  .ms-lg-auto {\n    margin-left: auto !important;\n  }\n  .p-lg-0 {\n    padding: 0 !important;\n  }\n  .p-lg-1 {\n    padding: 0.25rem !important;\n  }\n  .p-lg-2 {\n    padding: 0.5rem !important;\n  }\n  .p-lg-3 {\n    padding: 1rem !important;\n  }\n  .p-lg-4 {\n    padding: 1.5rem !important;\n  }\n  .p-lg-5 {\n    padding: 3rem !important;\n  }\n  .px-lg-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important;\n  }\n  .px-lg-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important;\n  }\n  .px-lg-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important;\n  }\n  .px-lg-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important;\n  }\n  .px-lg-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important;\n  }\n  .px-lg-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important;\n  }\n  .py-lg-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n  }\n  .py-lg-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important;\n  }\n  .py-lg-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important;\n  }\n  .py-lg-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n  }\n  .py-lg-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important;\n  }\n  .py-lg-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important;\n  }\n  .pt-lg-0 {\n    padding-top: 0 !important;\n  }\n  .pt-lg-1 {\n    padding-top: 0.25rem !important;\n  }\n  .pt-lg-2 {\n    padding-top: 0.5rem !important;\n  }\n  .pt-lg-3 {\n    padding-top: 1rem !important;\n  }\n  .pt-lg-4 {\n    padding-top: 1.5rem !important;\n  }\n  .pt-lg-5 {\n    padding-top: 3rem !important;\n  }\n  .pe-lg-0 {\n    padding-right: 0 !important;\n  }\n  .pe-lg-1 {\n    padding-right: 0.25rem !important;\n  }\n  .pe-lg-2 {\n    padding-right: 0.5rem !important;\n  }\n  .pe-lg-3 {\n    padding-right: 1rem !important;\n  }\n  .pe-lg-4 {\n    padding-right: 1.5rem !important;\n  }\n  .pe-lg-5 {\n    padding-right: 3rem !important;\n  }\n  .pb-lg-0 {\n    padding-bottom: 0 !important;\n  }\n  .pb-lg-1 {\n    padding-bottom: 0.25rem !important;\n  }\n  .pb-lg-2 {\n    padding-bottom: 0.5rem !important;\n  }\n  .pb-lg-3 {\n    padding-bottom: 1rem !important;\n  }\n  .pb-lg-4 {\n    padding-bottom: 1.5rem !important;\n  }\n  .pb-lg-5 {\n    padding-bottom: 3rem !important;\n  }\n  .ps-lg-0 {\n    padding-left: 0 !important;\n  }\n  .ps-lg-1 {\n    padding-left: 0.25rem !important;\n  }\n  .ps-lg-2 {\n    padding-left: 0.5rem !important;\n  }\n  .ps-lg-3 {\n    padding-left: 1rem !important;\n  }\n  .ps-lg-4 {\n    padding-left: 1.5rem !important;\n  }\n  .ps-lg-5 {\n    padding-left: 3rem !important;\n  }\n  .gap-lg-0 {\n    gap: 0 !important;\n  }\n  .gap-lg-1 {\n    gap: 0.25rem !important;\n  }\n  .gap-lg-2 {\n    gap: 0.5rem !important;\n  }\n  .gap-lg-3 {\n    gap: 1rem !important;\n  }\n  .gap-lg-4 {\n    gap: 1.5rem !important;\n  }\n  .gap-lg-5 {\n    gap: 3rem !important;\n  }\n  .row-gap-lg-0 {\n    row-gap: 0 !important;\n  }\n  .row-gap-lg-1 {\n    row-gap: 0.25rem !important;\n  }\n  .row-gap-lg-2 {\n    row-gap: 0.5rem !important;\n  }\n  .row-gap-lg-3 {\n    row-gap: 1rem !important;\n  }\n  .row-gap-lg-4 {\n    row-gap: 1.5rem !important;\n  }\n  .row-gap-lg-5 {\n    row-gap: 3rem !important;\n  }\n  .column-gap-lg-0 {\n    -moz-column-gap: 0 !important;\n    column-gap: 0 !important;\n  }\n  .column-gap-lg-1 {\n    -moz-column-gap: 0.25rem !important;\n    column-gap: 0.25rem !important;\n  }\n  .column-gap-lg-2 {\n    -moz-column-gap: 0.5rem !important;\n    column-gap: 0.5rem !important;\n  }\n  .column-gap-lg-3 {\n    -moz-column-gap: 1rem !important;\n    column-gap: 1rem !important;\n  }\n  .column-gap-lg-4 {\n    -moz-column-gap: 1.5rem !important;\n    column-gap: 1.5rem !important;\n  }\n  .column-gap-lg-5 {\n    -moz-column-gap: 3rem !important;\n    column-gap: 3rem !important;\n  }\n  .text-lg-start {\n    text-align: left !important;\n  }\n  .text-lg-end {\n    text-align: right !important;\n  }\n  .text-lg-center {\n    text-align: center !important;\n  }\n}\n@media (min-width: 1200px) {\n  .float-xl-start {\n    float: left !important;\n  }\n  .float-xl-end {\n    float: right !important;\n  }\n  .float-xl-none {\n    float: none !important;\n  }\n  .object-fit-xl-contain {\n    -o-object-fit: contain !important;\n    object-fit: contain !important;\n  }\n  .object-fit-xl-cover {\n    -o-object-fit: cover !important;\n    object-fit: cover !important;\n  }\n  .object-fit-xl-fill {\n    -o-object-fit: fill !important;\n    object-fit: fill !important;\n  }\n  .object-fit-xl-scale {\n    -o-object-fit: scale-down !important;\n    object-fit: scale-down !important;\n  }\n  .object-fit-xl-none {\n    -o-object-fit: none !important;\n    object-fit: none !important;\n  }\n  .d-xl-inline {\n    display: inline !important;\n  }\n  .d-xl-inline-block {\n    display: inline-block !important;\n  }\n  .d-xl-block {\n    display: block !important;\n  }\n  .d-xl-grid {\n    display: grid !important;\n  }\n  .d-xl-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-xl-table {\n    display: table !important;\n  }\n  .d-xl-table-row {\n    display: table-row !important;\n  }\n  .d-xl-table-cell {\n    display: table-cell !important;\n  }\n  .d-xl-flex {\n    display: flex !important;\n  }\n  .d-xl-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-xl-none {\n    display: none !important;\n  }\n  .flex-xl-fill {\n    flex: 1 1 auto !important;\n  }\n  .flex-xl-row {\n    flex-direction: row !important;\n  }\n  .flex-xl-column {\n    flex-direction: column !important;\n  }\n  .flex-xl-row-reverse {\n    flex-direction: row-reverse !important;\n  }\n  .flex-xl-column-reverse {\n    flex-direction: column-reverse !important;\n  }\n  .flex-xl-grow-0 {\n    flex-grow: 0 !important;\n  }\n  .flex-xl-grow-1 {\n    flex-grow: 1 !important;\n  }\n  .flex-xl-shrink-0 {\n    flex-shrink: 0 !important;\n  }\n  .flex-xl-shrink-1 {\n    flex-shrink: 1 !important;\n  }\n  .flex-xl-wrap {\n    flex-wrap: wrap !important;\n  }\n  .flex-xl-nowrap {\n    flex-wrap: nowrap !important;\n  }\n  .flex-xl-wrap-reverse {\n    flex-wrap: wrap-reverse !important;\n  }\n  .justify-content-xl-start {\n    justify-content: flex-start !important;\n  }\n  .justify-content-xl-end {\n    justify-content: flex-end !important;\n  }\n  .justify-content-xl-center {\n    justify-content: center !important;\n  }\n  .justify-content-xl-between {\n    justify-content: space-between !important;\n  }\n  .justify-content-xl-around {\n    justify-content: space-around !important;\n  }\n  .justify-content-xl-evenly {\n    justify-content: space-evenly !important;\n  }\n  .align-items-xl-start {\n    align-items: flex-start !important;\n  }\n  .align-items-xl-end {\n    align-items: flex-end !important;\n  }\n  .align-items-xl-center {\n    align-items: center !important;\n  }\n  .align-items-xl-baseline {\n    align-items: baseline !important;\n  }\n  .align-items-xl-stretch {\n    align-items: stretch !important;\n  }\n  .align-content-xl-start {\n    align-content: flex-start !important;\n  }\n  .align-content-xl-end {\n    align-content: flex-end !important;\n  }\n  .align-content-xl-center {\n    align-content: center !important;\n  }\n  .align-content-xl-between {\n    align-content: space-between !important;\n  }\n  .align-content-xl-around {\n    align-content: space-around !important;\n  }\n  .align-content-xl-stretch {\n    align-content: stretch !important;\n  }\n  .align-self-xl-auto {\n    align-self: auto !important;\n  }\n  .align-self-xl-start {\n    align-self: flex-start !important;\n  }\n  .align-self-xl-end {\n    align-self: flex-end !important;\n  }\n  .align-self-xl-center {\n    align-self: center !important;\n  }\n  .align-self-xl-baseline {\n    align-self: baseline !important;\n  }\n  .align-self-xl-stretch {\n    align-self: stretch !important;\n  }\n  .order-xl-first {\n    order: -1 !important;\n  }\n  .order-xl-0 {\n    order: 0 !important;\n  }\n  .order-xl-1 {\n    order: 1 !important;\n  }\n  .order-xl-2 {\n    order: 2 !important;\n  }\n  .order-xl-3 {\n    order: 3 !important;\n  }\n  .order-xl-4 {\n    order: 4 !important;\n  }\n  .order-xl-5 {\n    order: 5 !important;\n  }\n  .order-xl-last {\n    order: 6 !important;\n  }\n  .m-xl-0 {\n    margin: 0 !important;\n  }\n  .m-xl-1 {\n    margin: 0.25rem !important;\n  }\n  .m-xl-2 {\n    margin: 0.5rem !important;\n  }\n  .m-xl-3 {\n    margin: 1rem !important;\n  }\n  .m-xl-4 {\n    margin: 1.5rem !important;\n  }\n  .m-xl-5 {\n    margin: 3rem !important;\n  }\n  .m-xl-auto {\n    margin: auto !important;\n  }\n  .mx-xl-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important;\n  }\n  .mx-xl-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important;\n  }\n  .mx-xl-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important;\n  }\n  .mx-xl-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important;\n  }\n  .mx-xl-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important;\n  }\n  .mx-xl-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important;\n  }\n  .mx-xl-auto {\n    margin-right: auto !important;\n    margin-left: auto !important;\n  }\n  .my-xl-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important;\n  }\n  .my-xl-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important;\n  }\n  .my-xl-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important;\n  }\n  .my-xl-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important;\n  }\n  .my-xl-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important;\n  }\n  .my-xl-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important;\n  }\n  .my-xl-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important;\n  }\n  .mt-xl-0 {\n    margin-top: 0 !important;\n  }\n  .mt-xl-1 {\n    margin-top: 0.25rem !important;\n  }\n  .mt-xl-2 {\n    margin-top: 0.5rem !important;\n  }\n  .mt-xl-3 {\n    margin-top: 1rem !important;\n  }\n  .mt-xl-4 {\n    margin-top: 1.5rem !important;\n  }\n  .mt-xl-5 {\n    margin-top: 3rem !important;\n  }\n  .mt-xl-auto {\n    margin-top: auto !important;\n  }\n  .me-xl-0 {\n    margin-right: 0 !important;\n  }\n  .me-xl-1 {\n    margin-right: 0.25rem !important;\n  }\n  .me-xl-2 {\n    margin-right: 0.5rem !important;\n  }\n  .me-xl-3 {\n    margin-right: 1rem !important;\n  }\n  .me-xl-4 {\n    margin-right: 1.5rem !important;\n  }\n  .me-xl-5 {\n    margin-right: 3rem !important;\n  }\n  .me-xl-auto {\n    margin-right: auto !important;\n  }\n  .mb-xl-0 {\n    margin-bottom: 0 !important;\n  }\n  .mb-xl-1 {\n    margin-bottom: 0.25rem !important;\n  }\n  .mb-xl-2 {\n    margin-bottom: 0.5rem !important;\n  }\n  .mb-xl-3 {\n    margin-bottom: 1rem !important;\n  }\n  .mb-xl-4 {\n    margin-bottom: 1.5rem !important;\n  }\n  .mb-xl-5 {\n    margin-bottom: 3rem !important;\n  }\n  .mb-xl-auto {\n    margin-bottom: auto !important;\n  }\n  .ms-xl-0 {\n    margin-left: 0 !important;\n  }\n  .ms-xl-1 {\n    margin-left: 0.25rem !important;\n  }\n  .ms-xl-2 {\n    margin-left: 0.5rem !important;\n  }\n  .ms-xl-3 {\n    margin-left: 1rem !important;\n  }\n  .ms-xl-4 {\n    margin-left: 1.5rem !important;\n  }\n  .ms-xl-5 {\n    margin-left: 3rem !important;\n  }\n  .ms-xl-auto {\n    margin-left: auto !important;\n  }\n  .p-xl-0 {\n    padding: 0 !important;\n  }\n  .p-xl-1 {\n    padding: 0.25rem !important;\n  }\n  .p-xl-2 {\n    padding: 0.5rem !important;\n  }\n  .p-xl-3 {\n    padding: 1rem !important;\n  }\n  .p-xl-4 {\n    padding: 1.5rem !important;\n  }\n  .p-xl-5 {\n    padding: 3rem !important;\n  }\n  .px-xl-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important;\n  }\n  .px-xl-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important;\n  }\n  .px-xl-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important;\n  }\n  .px-xl-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important;\n  }\n  .px-xl-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important;\n  }\n  .px-xl-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important;\n  }\n  .py-xl-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n  }\n  .py-xl-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important;\n  }\n  .py-xl-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important;\n  }\n  .py-xl-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n  }\n  .py-xl-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important;\n  }\n  .py-xl-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important;\n  }\n  .pt-xl-0 {\n    padding-top: 0 !important;\n  }\n  .pt-xl-1 {\n    padding-top: 0.25rem !important;\n  }\n  .pt-xl-2 {\n    padding-top: 0.5rem !important;\n  }\n  .pt-xl-3 {\n    padding-top: 1rem !important;\n  }\n  .pt-xl-4 {\n    padding-top: 1.5rem !important;\n  }\n  .pt-xl-5 {\n    padding-top: 3rem !important;\n  }\n  .pe-xl-0 {\n    padding-right: 0 !important;\n  }\n  .pe-xl-1 {\n    padding-right: 0.25rem !important;\n  }\n  .pe-xl-2 {\n    padding-right: 0.5rem !important;\n  }\n  .pe-xl-3 {\n    padding-right: 1rem !important;\n  }\n  .pe-xl-4 {\n    padding-right: 1.5rem !important;\n  }\n  .pe-xl-5 {\n    padding-right: 3rem !important;\n  }\n  .pb-xl-0 {\n    padding-bottom: 0 !important;\n  }\n  .pb-xl-1 {\n    padding-bottom: 0.25rem !important;\n  }\n  .pb-xl-2 {\n    padding-bottom: 0.5rem !important;\n  }\n  .pb-xl-3 {\n    padding-bottom: 1rem !important;\n  }\n  .pb-xl-4 {\n    padding-bottom: 1.5rem !important;\n  }\n  .pb-xl-5 {\n    padding-bottom: 3rem !important;\n  }\n  .ps-xl-0 {\n    padding-left: 0 !important;\n  }\n  .ps-xl-1 {\n    padding-left: 0.25rem !important;\n  }\n  .ps-xl-2 {\n    padding-left: 0.5rem !important;\n  }\n  .ps-xl-3 {\n    padding-left: 1rem !important;\n  }\n  .ps-xl-4 {\n    padding-left: 1.5rem !important;\n  }\n  .ps-xl-5 {\n    padding-left: 3rem !important;\n  }\n  .gap-xl-0 {\n    gap: 0 !important;\n  }\n  .gap-xl-1 {\n    gap: 0.25rem !important;\n  }\n  .gap-xl-2 {\n    gap: 0.5rem !important;\n  }\n  .gap-xl-3 {\n    gap: 1rem !important;\n  }\n  .gap-xl-4 {\n    gap: 1.5rem !important;\n  }\n  .gap-xl-5 {\n    gap: 3rem !important;\n  }\n  .row-gap-xl-0 {\n    row-gap: 0 !important;\n  }\n  .row-gap-xl-1 {\n    row-gap: 0.25rem !important;\n  }\n  .row-gap-xl-2 {\n    row-gap: 0.5rem !important;\n  }\n  .row-gap-xl-3 {\n    row-gap: 1rem !important;\n  }\n  .row-gap-xl-4 {\n    row-gap: 1.5rem !important;\n  }\n  .row-gap-xl-5 {\n    row-gap: 3rem !important;\n  }\n  .column-gap-xl-0 {\n    -moz-column-gap: 0 !important;\n    column-gap: 0 !important;\n  }\n  .column-gap-xl-1 {\n    -moz-column-gap: 0.25rem !important;\n    column-gap: 0.25rem !important;\n  }\n  .column-gap-xl-2 {\n    -moz-column-gap: 0.5rem !important;\n    column-gap: 0.5rem !important;\n  }\n  .column-gap-xl-3 {\n    -moz-column-gap: 1rem !important;\n    column-gap: 1rem !important;\n  }\n  .column-gap-xl-4 {\n    -moz-column-gap: 1.5rem !important;\n    column-gap: 1.5rem !important;\n  }\n  .column-gap-xl-5 {\n    -moz-column-gap: 3rem !important;\n    column-gap: 3rem !important;\n  }\n  .text-xl-start {\n    text-align: left !important;\n  }\n  .text-xl-end {\n    text-align: right !important;\n  }\n  .text-xl-center {\n    text-align: center !important;\n  }\n}\n@media (min-width: 1400px) {\n  .float-xxl-start {\n    float: left !important;\n  }\n  .float-xxl-end {\n    float: right !important;\n  }\n  .float-xxl-none {\n    float: none !important;\n  }\n  .object-fit-xxl-contain {\n    -o-object-fit: contain !important;\n    object-fit: contain !important;\n  }\n  .object-fit-xxl-cover {\n    -o-object-fit: cover !important;\n    object-fit: cover !important;\n  }\n  .object-fit-xxl-fill {\n    -o-object-fit: fill !important;\n    object-fit: fill !important;\n  }\n  .object-fit-xxl-scale {\n    -o-object-fit: scale-down !important;\n    object-fit: scale-down !important;\n  }\n  .object-fit-xxl-none {\n    -o-object-fit: none !important;\n    object-fit: none !important;\n  }\n  .d-xxl-inline {\n    display: inline !important;\n  }\n  .d-xxl-inline-block {\n    display: inline-block !important;\n  }\n  .d-xxl-block {\n    display: block !important;\n  }\n  .d-xxl-grid {\n    display: grid !important;\n  }\n  .d-xxl-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-xxl-table {\n    display: table !important;\n  }\n  .d-xxl-table-row {\n    display: table-row !important;\n  }\n  .d-xxl-table-cell {\n    display: table-cell !important;\n  }\n  .d-xxl-flex {\n    display: flex !important;\n  }\n  .d-xxl-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-xxl-none {\n    display: none !important;\n  }\n  .flex-xxl-fill {\n    flex: 1 1 auto !important;\n  }\n  .flex-xxl-row {\n    flex-direction: row !important;\n  }\n  .flex-xxl-column {\n    flex-direction: column !important;\n  }\n  .flex-xxl-row-reverse {\n    flex-direction: row-reverse !important;\n  }\n  .flex-xxl-column-reverse {\n    flex-direction: column-reverse !important;\n  }\n  .flex-xxl-grow-0 {\n    flex-grow: 0 !important;\n  }\n  .flex-xxl-grow-1 {\n    flex-grow: 1 !important;\n  }\n  .flex-xxl-shrink-0 {\n    flex-shrink: 0 !important;\n  }\n  .flex-xxl-shrink-1 {\n    flex-shrink: 1 !important;\n  }\n  .flex-xxl-wrap {\n    flex-wrap: wrap !important;\n  }\n  .flex-xxl-nowrap {\n    flex-wrap: nowrap !important;\n  }\n  .flex-xxl-wrap-reverse {\n    flex-wrap: wrap-reverse !important;\n  }\n  .justify-content-xxl-start {\n    justify-content: flex-start !important;\n  }\n  .justify-content-xxl-end {\n    justify-content: flex-end !important;\n  }\n  .justify-content-xxl-center {\n    justify-content: center !important;\n  }\n  .justify-content-xxl-between {\n    justify-content: space-between !important;\n  }\n  .justify-content-xxl-around {\n    justify-content: space-around !important;\n  }\n  .justify-content-xxl-evenly {\n    justify-content: space-evenly !important;\n  }\n  .align-items-xxl-start {\n    align-items: flex-start !important;\n  }\n  .align-items-xxl-end {\n    align-items: flex-end !important;\n  }\n  .align-items-xxl-center {\n    align-items: center !important;\n  }\n  .align-items-xxl-baseline {\n    align-items: baseline !important;\n  }\n  .align-items-xxl-stretch {\n    align-items: stretch !important;\n  }\n  .align-content-xxl-start {\n    align-content: flex-start !important;\n  }\n  .align-content-xxl-end {\n    align-content: flex-end !important;\n  }\n  .align-content-xxl-center {\n    align-content: center !important;\n  }\n  .align-content-xxl-between {\n    align-content: space-between !important;\n  }\n  .align-content-xxl-around {\n    align-content: space-around !important;\n  }\n  .align-content-xxl-stretch {\n    align-content: stretch !important;\n  }\n  .align-self-xxl-auto {\n    align-self: auto !important;\n  }\n  .align-self-xxl-start {\n    align-self: flex-start !important;\n  }\n  .align-self-xxl-end {\n    align-self: flex-end !important;\n  }\n  .align-self-xxl-center {\n    align-self: center !important;\n  }\n  .align-self-xxl-baseline {\n    align-self: baseline !important;\n  }\n  .align-self-xxl-stretch {\n    align-self: stretch !important;\n  }\n  .order-xxl-first {\n    order: -1 !important;\n  }\n  .order-xxl-0 {\n    order: 0 !important;\n  }\n  .order-xxl-1 {\n    order: 1 !important;\n  }\n  .order-xxl-2 {\n    order: 2 !important;\n  }\n  .order-xxl-3 {\n    order: 3 !important;\n  }\n  .order-xxl-4 {\n    order: 4 !important;\n  }\n  .order-xxl-5 {\n    order: 5 !important;\n  }\n  .order-xxl-last {\n    order: 6 !important;\n  }\n  .m-xxl-0 {\n    margin: 0 !important;\n  }\n  .m-xxl-1 {\n    margin: 0.25rem !important;\n  }\n  .m-xxl-2 {\n    margin: 0.5rem !important;\n  }\n  .m-xxl-3 {\n    margin: 1rem !important;\n  }\n  .m-xxl-4 {\n    margin: 1.5rem !important;\n  }\n  .m-xxl-5 {\n    margin: 3rem !important;\n  }\n  .m-xxl-auto {\n    margin: auto !important;\n  }\n  .mx-xxl-0 {\n    margin-right: 0 !important;\n    margin-left: 0 !important;\n  }\n  .mx-xxl-1 {\n    margin-right: 0.25rem !important;\n    margin-left: 0.25rem !important;\n  }\n  .mx-xxl-2 {\n    margin-right: 0.5rem !important;\n    margin-left: 0.5rem !important;\n  }\n  .mx-xxl-3 {\n    margin-right: 1rem !important;\n    margin-left: 1rem !important;\n  }\n  .mx-xxl-4 {\n    margin-right: 1.5rem !important;\n    margin-left: 1.5rem !important;\n  }\n  .mx-xxl-5 {\n    margin-right: 3rem !important;\n    margin-left: 3rem !important;\n  }\n  .mx-xxl-auto {\n    margin-right: auto !important;\n    margin-left: auto !important;\n  }\n  .my-xxl-0 {\n    margin-top: 0 !important;\n    margin-bottom: 0 !important;\n  }\n  .my-xxl-1 {\n    margin-top: 0.25rem !important;\n    margin-bottom: 0.25rem !important;\n  }\n  .my-xxl-2 {\n    margin-top: 0.5rem !important;\n    margin-bottom: 0.5rem !important;\n  }\n  .my-xxl-3 {\n    margin-top: 1rem !important;\n    margin-bottom: 1rem !important;\n  }\n  .my-xxl-4 {\n    margin-top: 1.5rem !important;\n    margin-bottom: 1.5rem !important;\n  }\n  .my-xxl-5 {\n    margin-top: 3rem !important;\n    margin-bottom: 3rem !important;\n  }\n  .my-xxl-auto {\n    margin-top: auto !important;\n    margin-bottom: auto !important;\n  }\n  .mt-xxl-0 {\n    margin-top: 0 !important;\n  }\n  .mt-xxl-1 {\n    margin-top: 0.25rem !important;\n  }\n  .mt-xxl-2 {\n    margin-top: 0.5rem !important;\n  }\n  .mt-xxl-3 {\n    margin-top: 1rem !important;\n  }\n  .mt-xxl-4 {\n    margin-top: 1.5rem !important;\n  }\n  .mt-xxl-5 {\n    margin-top: 3rem !important;\n  }\n  .mt-xxl-auto {\n    margin-top: auto !important;\n  }\n  .me-xxl-0 {\n    margin-right: 0 !important;\n  }\n  .me-xxl-1 {\n    margin-right: 0.25rem !important;\n  }\n  .me-xxl-2 {\n    margin-right: 0.5rem !important;\n  }\n  .me-xxl-3 {\n    margin-right: 1rem !important;\n  }\n  .me-xxl-4 {\n    margin-right: 1.5rem !important;\n  }\n  .me-xxl-5 {\n    margin-right: 3rem !important;\n  }\n  .me-xxl-auto {\n    margin-right: auto !important;\n  }\n  .mb-xxl-0 {\n    margin-bottom: 0 !important;\n  }\n  .mb-xxl-1 {\n    margin-bottom: 0.25rem !important;\n  }\n  .mb-xxl-2 {\n    margin-bottom: 0.5rem !important;\n  }\n  .mb-xxl-3 {\n    margin-bottom: 1rem !important;\n  }\n  .mb-xxl-4 {\n    margin-bottom: 1.5rem !important;\n  }\n  .mb-xxl-5 {\n    margin-bottom: 3rem !important;\n  }\n  .mb-xxl-auto {\n    margin-bottom: auto !important;\n  }\n  .ms-xxl-0 {\n    margin-left: 0 !important;\n  }\n  .ms-xxl-1 {\n    margin-left: 0.25rem !important;\n  }\n  .ms-xxl-2 {\n    margin-left: 0.5rem !important;\n  }\n  .ms-xxl-3 {\n    margin-left: 1rem !important;\n  }\n  .ms-xxl-4 {\n    margin-left: 1.5rem !important;\n  }\n  .ms-xxl-5 {\n    margin-left: 3rem !important;\n  }\n  .ms-xxl-auto {\n    margin-left: auto !important;\n  }\n  .p-xxl-0 {\n    padding: 0 !important;\n  }\n  .p-xxl-1 {\n    padding: 0.25rem !important;\n  }\n  .p-xxl-2 {\n    padding: 0.5rem !important;\n  }\n  .p-xxl-3 {\n    padding: 1rem !important;\n  }\n  .p-xxl-4 {\n    padding: 1.5rem !important;\n  }\n  .p-xxl-5 {\n    padding: 3rem !important;\n  }\n  .px-xxl-0 {\n    padding-right: 0 !important;\n    padding-left: 0 !important;\n  }\n  .px-xxl-1 {\n    padding-right: 0.25rem !important;\n    padding-left: 0.25rem !important;\n  }\n  .px-xxl-2 {\n    padding-right: 0.5rem !important;\n    padding-left: 0.5rem !important;\n  }\n  .px-xxl-3 {\n    padding-right: 1rem !important;\n    padding-left: 1rem !important;\n  }\n  .px-xxl-4 {\n    padding-right: 1.5rem !important;\n    padding-left: 1.5rem !important;\n  }\n  .px-xxl-5 {\n    padding-right: 3rem !important;\n    padding-left: 3rem !important;\n  }\n  .py-xxl-0 {\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n  }\n  .py-xxl-1 {\n    padding-top: 0.25rem !important;\n    padding-bottom: 0.25rem !important;\n  }\n  .py-xxl-2 {\n    padding-top: 0.5rem !important;\n    padding-bottom: 0.5rem !important;\n  }\n  .py-xxl-3 {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n  }\n  .py-xxl-4 {\n    padding-top: 1.5rem !important;\n    padding-bottom: 1.5rem !important;\n  }\n  .py-xxl-5 {\n    padding-top: 3rem !important;\n    padding-bottom: 3rem !important;\n  }\n  .pt-xxl-0 {\n    padding-top: 0 !important;\n  }\n  .pt-xxl-1 {\n    padding-top: 0.25rem !important;\n  }\n  .pt-xxl-2 {\n    padding-top: 0.5rem !important;\n  }\n  .pt-xxl-3 {\n    padding-top: 1rem !important;\n  }\n  .pt-xxl-4 {\n    padding-top: 1.5rem !important;\n  }\n  .pt-xxl-5 {\n    padding-top: 3rem !important;\n  }\n  .pe-xxl-0 {\n    padding-right: 0 !important;\n  }\n  .pe-xxl-1 {\n    padding-right: 0.25rem !important;\n  }\n  .pe-xxl-2 {\n    padding-right: 0.5rem !important;\n  }\n  .pe-xxl-3 {\n    padding-right: 1rem !important;\n  }\n  .pe-xxl-4 {\n    padding-right: 1.5rem !important;\n  }\n  .pe-xxl-5 {\n    padding-right: 3rem !important;\n  }\n  .pb-xxl-0 {\n    padding-bottom: 0 !important;\n  }\n  .pb-xxl-1 {\n    padding-bottom: 0.25rem !important;\n  }\n  .pb-xxl-2 {\n    padding-bottom: 0.5rem !important;\n  }\n  .pb-xxl-3 {\n    padding-bottom: 1rem !important;\n  }\n  .pb-xxl-4 {\n    padding-bottom: 1.5rem !important;\n  }\n  .pb-xxl-5 {\n    padding-bottom: 3rem !important;\n  }\n  .ps-xxl-0 {\n    padding-left: 0 !important;\n  }\n  .ps-xxl-1 {\n    padding-left: 0.25rem !important;\n  }\n  .ps-xxl-2 {\n    padding-left: 0.5rem !important;\n  }\n  .ps-xxl-3 {\n    padding-left: 1rem !important;\n  }\n  .ps-xxl-4 {\n    padding-left: 1.5rem !important;\n  }\n  .ps-xxl-5 {\n    padding-left: 3rem !important;\n  }\n  .gap-xxl-0 {\n    gap: 0 !important;\n  }\n  .gap-xxl-1 {\n    gap: 0.25rem !important;\n  }\n  .gap-xxl-2 {\n    gap: 0.5rem !important;\n  }\n  .gap-xxl-3 {\n    gap: 1rem !important;\n  }\n  .gap-xxl-4 {\n    gap: 1.5rem !important;\n  }\n  .gap-xxl-5 {\n    gap: 3rem !important;\n  }\n  .row-gap-xxl-0 {\n    row-gap: 0 !important;\n  }\n  .row-gap-xxl-1 {\n    row-gap: 0.25rem !important;\n  }\n  .row-gap-xxl-2 {\n    row-gap: 0.5rem !important;\n  }\n  .row-gap-xxl-3 {\n    row-gap: 1rem !important;\n  }\n  .row-gap-xxl-4 {\n    row-gap: 1.5rem !important;\n  }\n  .row-gap-xxl-5 {\n    row-gap: 3rem !important;\n  }\n  .column-gap-xxl-0 {\n    -moz-column-gap: 0 !important;\n    column-gap: 0 !important;\n  }\n  .column-gap-xxl-1 {\n    -moz-column-gap: 0.25rem !important;\n    column-gap: 0.25rem !important;\n  }\n  .column-gap-xxl-2 {\n    -moz-column-gap: 0.5rem !important;\n    column-gap: 0.5rem !important;\n  }\n  .column-gap-xxl-3 {\n    -moz-column-gap: 1rem !important;\n    column-gap: 1rem !important;\n  }\n  .column-gap-xxl-4 {\n    -moz-column-gap: 1.5rem !important;\n    column-gap: 1.5rem !important;\n  }\n  .column-gap-xxl-5 {\n    -moz-column-gap: 3rem !important;\n    column-gap: 3rem !important;\n  }\n  .text-xxl-start {\n    text-align: left !important;\n  }\n  .text-xxl-end {\n    text-align: right !important;\n  }\n  .text-xxl-center {\n    text-align: center !important;\n  }\n}\n@media (min-width: 1200px) {\n  .fs-1 {\n    font-size: 2.5rem !important;\n  }\n  .fs-2 {\n    font-size: 2rem !important;\n  }\n  .fs-3 {\n    font-size: 1.75rem !important;\n  }\n  .fs-4 {\n    font-size: 1.5rem !important;\n  }\n}\n@media print {\n  .d-print-inline {\n    display: inline !important;\n  }\n  .d-print-inline-block {\n    display: inline-block !important;\n  }\n  .d-print-block {\n    display: block !important;\n  }\n  .d-print-grid {\n    display: grid !important;\n  }\n  .d-print-inline-grid {\n    display: inline-grid !important;\n  }\n  .d-print-table {\n    display: table !important;\n  }\n  .d-print-table-row {\n    display: table-row !important;\n  }\n  .d-print-table-cell {\n    display: table-cell !important;\n  }\n  .d-print-flex {\n    display: flex !important;\n  }\n  .d-print-inline-flex {\n    display: inline-flex !important;\n  }\n  .d-print-none {\n    display: none !important;\n  }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */"
  },
  {
    "path": "src/static/scripts/datatables.css",
    "content": "/*\n * This combined file was created by the DataTables downloader builder:\n *   https://datatables.net/download\n *\n * To rebuild or modify this file with the latest versions of the included\n * software please visit:\n *   https://datatables.net/download/#bs5/dt-2.3.7\n *\n * Included libraries:\n *   DataTables 2.3.7\n */\n\n:root {\n  --dt-row-selected: 13, 110, 253;\n  --dt-row-selected-text: 255, 255, 255;\n  --dt-row-selected-link: 228, 228, 228;\n  --dt-row-stripe: 0, 0, 0;\n  --dt-row-hover: 0, 0, 0;\n  --dt-column-ordering: 0, 0, 0;\n  --dt-header-align-items: center;\n  --dt-header-vertical-align: middle;\n  --dt-html-background: white;\n}\n:root.dark {\n  --dt-html-background: rgb(33, 37, 41);\n}\n\ntable.dataTable tbody td.dt-control {\n  text-align: center;\n  cursor: pointer;\n}\ntable.dataTable tbody td.dt-control:before {\n  display: inline-block;\n  box-sizing: border-box;\n  content: \"\";\n  border-top: 5px solid transparent;\n  border-left: 10px solid rgba(0, 0, 0, 0.5);\n  border-bottom: 5px solid transparent;\n  border-right: 0px solid transparent;\n}\ntable.dataTable tbody tr.dt-hasChild td.dt-control:before {\n  border-top: 10px solid rgba(0, 0, 0, 0.5);\n  border-left: 5px solid transparent;\n  border-bottom: 0px solid transparent;\n  border-right: 5px solid transparent;\n}\ntable.dataTable tfoot:empty {\n  display: none;\n}\n\nhtml.dark table.dataTable td.dt-control:before,\n:root[data-bs-theme=dark] table.dataTable td.dt-control:before,\n:root[data-theme=dark] table.dataTable td.dt-control:before {\n  border-left-color: rgba(255, 255, 255, 0.5);\n}\nhtml.dark table.dataTable tr.dt-hasChild td.dt-control:before,\n:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,\n:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before {\n  border-top-color: rgba(255, 255, 255, 0.5);\n  border-left-color: transparent;\n}\n\ndiv.dt-scroll {\n  width: 100%;\n}\n\ndiv.dt-scroll-body thead tr,\ndiv.dt-scroll-body tfoot tr {\n  height: 0;\n}\ndiv.dt-scroll-body thead tr th, div.dt-scroll-body thead tr td,\ndiv.dt-scroll-body tfoot tr th,\ndiv.dt-scroll-body tfoot tr td {\n  height: 0 !important;\n  padding-top: 0px !important;\n  padding-bottom: 0px !important;\n  border-top-width: 0px !important;\n  border-bottom-width: 0px !important;\n}\ndiv.dt-scroll-body thead tr th div.dt-scroll-sizing, div.dt-scroll-body thead tr td div.dt-scroll-sizing,\ndiv.dt-scroll-body tfoot tr th div.dt-scroll-sizing,\ndiv.dt-scroll-body tfoot tr td div.dt-scroll-sizing {\n  height: 0 !important;\n  overflow: hidden !important;\n}\n\ntable.dataTable thead > tr > th:active,\ntable.dataTable thead > tr > td:active {\n  outline: none;\n}\ntable.dataTable thead > tr > th.dt-orderable-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before {\n  position: absolute;\n  display: block;\n  bottom: 50%;\n  content: \"\\25B2\";\n  content: \"\\25B2\"/\"\";\n}\ntable.dataTable thead > tr > th.dt-orderable-desc .dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {\n  position: absolute;\n  display: block;\n  top: 50%;\n  content: \"\\25BC\";\n  content: \"\\25BC\"/\"\";\n}\ntable.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,\ntable.dataTable thead > tr > td.dt-orderable-asc .dt-column-order,\ntable.dataTable thead > tr > td.dt-orderable-desc .dt-column-order,\ntable.dataTable thead > tr > td.dt-ordering-asc .dt-column-order,\ntable.dataTable thead > tr > td.dt-ordering-desc .dt-column-order {\n  position: relative;\n  width: 12px;\n  height: 20px;\n}\ntable.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,\ntable.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-orderable-asc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-orderable-desc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {\n  left: 0;\n  opacity: 0.125;\n  line-height: 9px;\n  font-size: 0.8em;\n}\ntable.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc,\ntable.dataTable thead > tr > td.dt-orderable-asc,\ntable.dataTable thead > tr > td.dt-orderable-desc {\n  cursor: pointer;\n}\ntable.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover,\ntable.dataTable thead > tr > td.dt-orderable-asc:hover,\ntable.dataTable thead > tr > td.dt-orderable-desc:hover {\n  outline: 2px solid rgba(0, 0, 0, 0.05);\n  outline-offset: -2px;\n}\ntable.dataTable thead > tr > th.dt-ordering-asc .dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc .dt-column-order:after,\ntable.dataTable thead > tr > td.dt-ordering-asc .dt-column-order:before,\ntable.dataTable thead > tr > td.dt-ordering-desc .dt-column-order:after {\n  opacity: 0.6;\n}\ntable.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,\ntable.dataTable thead > tr > td.dt-orderable-none:not(.dt-ordering-asc, .dt-ordering-desc) .dt-column-order:empty,\ntable.dataTable thead > tr > td.sorting_desc_disabled .dt-column-order:after,\ntable.dataTable thead > tr > td.sorting_asc_disabled .dt-column-order:before {\n  display: none;\n}\ntable.dataTable thead > tr > th:active,\ntable.dataTable thead > tr > td:active {\n  outline: none;\n}\n\ntable.dataTable thead > tr > th div.dt-column-header,\ntable.dataTable thead > tr > th div.dt-column-footer,\ntable.dataTable thead > tr > td div.dt-column-header,\ntable.dataTable thead > tr > td div.dt-column-footer,\ntable.dataTable tfoot > tr > th div.dt-column-header,\ntable.dataTable tfoot > tr > th div.dt-column-footer,\ntable.dataTable tfoot > tr > td div.dt-column-header,\ntable.dataTable tfoot > tr > td div.dt-column-footer {\n  display: flex;\n  justify-content: space-between;\n  align-items: var(--dt-header-align-items);\n  gap: 4px;\n}\ntable.dataTable thead > tr > th div.dt-column-header .dt-column-title,\ntable.dataTable thead > tr > th div.dt-column-footer .dt-column-title,\ntable.dataTable thead > tr > td div.dt-column-header .dt-column-title,\ntable.dataTable thead > tr > td div.dt-column-footer .dt-column-title,\ntable.dataTable tfoot > tr > th div.dt-column-header .dt-column-title,\ntable.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title,\ntable.dataTable tfoot > tr > td div.dt-column-header .dt-column-title,\ntable.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title {\n  flex-grow: 1;\n}\ntable.dataTable thead > tr > th div.dt-column-header .dt-column-title:empty,\ntable.dataTable thead > tr > th div.dt-column-footer .dt-column-title:empty,\ntable.dataTable thead > tr > td div.dt-column-header .dt-column-title:empty,\ntable.dataTable thead > tr > td div.dt-column-footer .dt-column-title:empty,\ntable.dataTable tfoot > tr > th div.dt-column-header .dt-column-title:empty,\ntable.dataTable tfoot > tr > th div.dt-column-footer .dt-column-title:empty,\ntable.dataTable tfoot > tr > td div.dt-column-header .dt-column-title:empty,\ntable.dataTable tfoot > tr > td div.dt-column-footer .dt-column-title:empty {\n  display: none;\n}\n\ndiv.dt-scroll-body > table.dataTable > thead > tr > th,\ndiv.dt-scroll-body > table.dataTable > thead > tr > td {\n  overflow: hidden;\n}\n\n:root.dark table.dataTable thead > tr > th.dt-orderable-asc:hover, :root.dark table.dataTable thead > tr > th.dt-orderable-desc:hover,\n:root.dark table.dataTable thead > tr > td.dt-orderable-asc:hover,\n:root.dark table.dataTable thead > tr > td.dt-orderable-desc:hover,\n:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-asc:hover,\n:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover,\n:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover,\n:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover {\n  outline: 2px solid rgba(255, 255, 255, 0.05);\n}\n\ndiv.dt-processing {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 200px;\n  margin-left: -100px;\n  margin-top: -22px;\n  text-align: center;\n  padding: 2px;\n  z-index: 10;\n}\ndiv.dt-processing > div:last-child {\n  position: relative;\n  width: 80px;\n  height: 15px;\n  margin: 1em auto;\n}\ndiv.dt-processing > div:last-child > div {\n  position: absolute;\n  top: 0;\n  width: 13px;\n  height: 13px;\n  border-radius: 50%;\n  background: rgb(13, 110, 253);\n  background: rgb(var(--dt-row-selected));\n  animation-timing-function: cubic-bezier(0, 1, 1, 0);\n}\ndiv.dt-processing > div:last-child > div:nth-child(1) {\n  left: 8px;\n  animation: datatables-loader-1 0.6s infinite;\n}\ndiv.dt-processing > div:last-child > div:nth-child(2) {\n  left: 8px;\n  animation: datatables-loader-2 0.6s infinite;\n}\ndiv.dt-processing > div:last-child > div:nth-child(3) {\n  left: 32px;\n  animation: datatables-loader-2 0.6s infinite;\n}\ndiv.dt-processing > div:last-child > div:nth-child(4) {\n  left: 56px;\n  animation: datatables-loader-3 0.6s infinite;\n}\n\n@keyframes datatables-loader-1 {\n  0% {\n    transform: scale(0);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n@keyframes datatables-loader-3 {\n  0% {\n    transform: scale(1);\n  }\n  100% {\n    transform: scale(0);\n  }\n}\n@keyframes datatables-loader-2 {\n  0% {\n    transform: translate(0, 0);\n  }\n  100% {\n    transform: translate(24px, 0);\n  }\n}\ntable.dataTable.nowrap th, table.dataTable.nowrap td {\n  white-space: nowrap;\n}\ntable.dataTable th,\ntable.dataTable td {\n  box-sizing: border-box;\n}\ntable.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date,\ntable.dataTable td.dt-type-numeric,\ntable.dataTable td.dt-type-date {\n  text-align: right;\n}\ntable.dataTable th.dt-type-numeric div.dt-column-header,\ntable.dataTable th.dt-type-numeric div.dt-column-footer, table.dataTable th.dt-type-date div.dt-column-header,\ntable.dataTable th.dt-type-date div.dt-column-footer,\ntable.dataTable td.dt-type-numeric div.dt-column-header,\ntable.dataTable td.dt-type-numeric div.dt-column-footer,\ntable.dataTable td.dt-type-date div.dt-column-header,\ntable.dataTable td.dt-type-date div.dt-column-footer {\n  flex-direction: row-reverse;\n}\ntable.dataTable th.dt-left,\ntable.dataTable td.dt-left {\n  text-align: left;\n}\ntable.dataTable th.dt-left div.dt-column-header,\ntable.dataTable th.dt-left div.dt-column-footer,\ntable.dataTable td.dt-left div.dt-column-header,\ntable.dataTable td.dt-left div.dt-column-footer {\n  flex-direction: row;\n}\ntable.dataTable th.dt-center,\ntable.dataTable td.dt-center {\n  text-align: center;\n}\ntable.dataTable th.dt-right,\ntable.dataTable td.dt-right {\n  text-align: right;\n}\ntable.dataTable th.dt-right div.dt-column-header,\ntable.dataTable th.dt-right div.dt-column-footer,\ntable.dataTable td.dt-right div.dt-column-header,\ntable.dataTable td.dt-right div.dt-column-footer {\n  flex-direction: row-reverse;\n}\ntable.dataTable th.dt-justify,\ntable.dataTable td.dt-justify {\n  text-align: justify;\n}\ntable.dataTable th.dt-justify div.dt-column-header,\ntable.dataTable th.dt-justify div.dt-column-footer,\ntable.dataTable td.dt-justify div.dt-column-header,\ntable.dataTable td.dt-justify div.dt-column-footer {\n  flex-direction: row;\n}\ntable.dataTable th.dt-nowrap,\ntable.dataTable td.dt-nowrap {\n  white-space: nowrap;\n}\ntable.dataTable th.dt-empty,\ntable.dataTable td.dt-empty {\n  text-align: center;\n  vertical-align: top;\n}\ntable.dataTable thead th,\ntable.dataTable thead td,\ntable.dataTable tfoot th,\ntable.dataTable tfoot td {\n  text-align: left;\n  vertical-align: var(--dt-header-vertical-align);\n}\ntable.dataTable thead th.dt-head-left,\ntable.dataTable thead td.dt-head-left,\ntable.dataTable tfoot th.dt-head-left,\ntable.dataTable tfoot td.dt-head-left {\n  text-align: left;\n}\ntable.dataTable thead th.dt-head-left div.dt-column-header,\ntable.dataTable thead th.dt-head-left div.dt-column-footer,\ntable.dataTable thead td.dt-head-left div.dt-column-header,\ntable.dataTable thead td.dt-head-left div.dt-column-footer,\ntable.dataTable tfoot th.dt-head-left div.dt-column-header,\ntable.dataTable tfoot th.dt-head-left div.dt-column-footer,\ntable.dataTable tfoot td.dt-head-left div.dt-column-header,\ntable.dataTable tfoot td.dt-head-left div.dt-column-footer {\n  flex-direction: row;\n}\ntable.dataTable thead th.dt-head-center,\ntable.dataTable thead td.dt-head-center,\ntable.dataTable tfoot th.dt-head-center,\ntable.dataTable tfoot td.dt-head-center {\n  text-align: center;\n}\ntable.dataTable thead th.dt-head-right,\ntable.dataTable thead td.dt-head-right,\ntable.dataTable tfoot th.dt-head-right,\ntable.dataTable tfoot td.dt-head-right {\n  text-align: right;\n}\ntable.dataTable thead th.dt-head-right div.dt-column-header,\ntable.dataTable thead th.dt-head-right div.dt-column-footer,\ntable.dataTable thead td.dt-head-right div.dt-column-header,\ntable.dataTable thead td.dt-head-right div.dt-column-footer,\ntable.dataTable tfoot th.dt-head-right div.dt-column-header,\ntable.dataTable tfoot th.dt-head-right div.dt-column-footer,\ntable.dataTable tfoot td.dt-head-right div.dt-column-header,\ntable.dataTable tfoot td.dt-head-right div.dt-column-footer {\n  flex-direction: row-reverse;\n}\ntable.dataTable thead th.dt-head-justify,\ntable.dataTable thead td.dt-head-justify,\ntable.dataTable tfoot th.dt-head-justify,\ntable.dataTable tfoot td.dt-head-justify {\n  text-align: justify;\n}\ntable.dataTable thead th.dt-head-justify div.dt-column-header,\ntable.dataTable thead th.dt-head-justify div.dt-column-footer,\ntable.dataTable thead td.dt-head-justify div.dt-column-header,\ntable.dataTable thead td.dt-head-justify div.dt-column-footer,\ntable.dataTable tfoot th.dt-head-justify div.dt-column-header,\ntable.dataTable tfoot th.dt-head-justify div.dt-column-footer,\ntable.dataTable tfoot td.dt-head-justify div.dt-column-header,\ntable.dataTable tfoot td.dt-head-justify div.dt-column-footer {\n  flex-direction: row;\n}\ntable.dataTable thead th.dt-head-nowrap,\ntable.dataTable thead td.dt-head-nowrap,\ntable.dataTable tfoot th.dt-head-nowrap,\ntable.dataTable tfoot td.dt-head-nowrap {\n  white-space: nowrap;\n}\ntable.dataTable tbody th.dt-body-left,\ntable.dataTable tbody td.dt-body-left {\n  text-align: left;\n}\ntable.dataTable tbody th.dt-body-center,\ntable.dataTable tbody td.dt-body-center {\n  text-align: center;\n}\ntable.dataTable tbody th.dt-body-right,\ntable.dataTable tbody td.dt-body-right {\n  text-align: right;\n}\ntable.dataTable tbody th.dt-body-justify,\ntable.dataTable tbody td.dt-body-justify {\n  text-align: justify;\n}\ntable.dataTable tbody th.dt-body-nowrap,\ntable.dataTable tbody td.dt-body-nowrap {\n  white-space: nowrap;\n}\n\n/*! Bootstrap 5 integration for DataTables\n *\n * ©2020 SpryMedia Ltd, all rights reserved.\n * License: MIT datatables.net/license/mit\n */\ntable.table.dataTable {\n  clear: both;\n  margin-bottom: 0;\n  max-width: none;\n  border-spacing: 0;\n}\ntable.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {\n  box-shadow: none;\n}\ntable.table.dataTable > :not(caption) > * > * {\n  background-color: var(--bs-table-bg);\n}\ntable.table.dataTable > tbody > tr {\n  background-color: transparent;\n}\ntable.table.dataTable > tbody > tr.selected > * {\n  box-shadow: inset 0 0 0 9999px rgb(13, 110, 253);\n  box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected));\n  color: rgb(255, 255, 255);\n  color: rgb(var(--dt-row-selected-text));\n}\ntable.table.dataTable > tbody > tr.selected a {\n  color: rgb(228, 228, 228);\n  color: rgb(var(--dt-row-selected-link));\n}\ntable.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {\n  box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05);\n}\ntable.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * {\n  box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);\n  box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95);\n}\ntable.table.dataTable.table-hover > tbody > tr:hover > * {\n  box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075);\n}\ntable.table.dataTable.table-hover > tbody > tr.selected:hover > * {\n  box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);\n  box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975);\n}\n\ndiv.dt-container div.dt-layout-start > *:not(:last-child) {\n  margin-right: 1em;\n}\ndiv.dt-container div.dt-layout-end > *:not(:first-child) {\n  margin-left: 1em;\n}\ndiv.dt-container div.dt-layout-full {\n  width: 100%;\n}\ndiv.dt-container div.dt-layout-full > *:only-child {\n  margin-left: auto;\n  margin-right: auto;\n}\ndiv.dt-container div.dt-layout-table > div {\n  display: block !important;\n}\n\n@media screen and (max-width: 767px) {\n  div.dt-container div.dt-layout-start > *:not(:last-child) {\n    margin-right: 0;\n  }\n  div.dt-container div.dt-layout-end > *:not(:first-child) {\n    margin-left: 0;\n  }\n}\ndiv.dt-container {\n  position: relative;\n}\ndiv.dt-container div.dt-length label {\n  font-weight: normal;\n  text-align: left;\n  white-space: nowrap;\n}\ndiv.dt-container div.dt-length select {\n  width: auto;\n  display: inline-block;\n  margin-right: 0.5em;\n}\ndiv.dt-container div.dt-search {\n  text-align: right;\n}\ndiv.dt-container div.dt-search label {\n  font-weight: normal;\n  white-space: nowrap;\n  text-align: left;\n}\ndiv.dt-container div.dt-search input {\n  margin-left: 0.5em;\n  display: inline-block;\n  width: auto;\n}\ndiv.dt-container div.dt-paging {\n  margin: 0;\n}\ndiv.dt-container div.dt-paging ul.pagination {\n  margin: 2px 0;\n  flex-wrap: wrap;\n}\ndiv.dt-container div.dt-row {\n  position: relative;\n}\n\ndiv.dt-scroll-head table.dataTable {\n  margin-bottom: 0 !important;\n}\n\ndiv.dt-scroll-body {\n  border-bottom-color: var(--bs-border-color);\n  border-bottom-width: var(--bs-border-width);\n  border-bottom-style: solid;\n}\ndiv.dt-scroll-body > table {\n  border-top: none;\n  margin-top: 0 !important;\n  margin-bottom: 0 !important;\n}\ndiv.dt-scroll-body > table > tbody > tr:first-child {\n  border-top-width: 0;\n}\ndiv.dt-scroll-body > table > thead > tr {\n  border-width: 0 !important;\n}\ndiv.dt-scroll-body > table > tbody > tr:last-child > * {\n  border-bottom: none;\n}\n\ndiv.dt-scroll-foot > .dt-scroll-footInner {\n  box-sizing: content-box;\n}\ndiv.dt-scroll-foot > .dt-scroll-footInner > table {\n  margin-top: 0 !important;\n  border-top: none;\n}\ndiv.dt-scroll-foot > .dt-scroll-footInner > table > tfoot > tr:first-child {\n  border-top-width: 0 !important;\n}\n\n@media screen and (max-width: 767px) {\n  div.dt-container div.dt-length,\n  div.dt-container div.dt-search,\n  div.dt-container div.dt-info,\n  div.dt-container div.dt-paging {\n    text-align: center;\n  }\n  div.dt-container .row {\n    --bs-gutter-y: 0.5rem;\n  }\n  div.dt-container div.dt-paging ul.pagination {\n    justify-content: center !important;\n  }\n}\ntable.dataTable.table-sm > thead > tr th.dt-orderable-asc, table.dataTable.table-sm > thead > tr th.dt-orderable-desc, table.dataTable.table-sm > thead > tr th.dt-ordering-asc, table.dataTable.table-sm > thead > tr th.dt-ordering-desc,\ntable.dataTable.table-sm > thead > tr td.dt-orderable-asc,\ntable.dataTable.table-sm > thead > tr td.dt-orderable-desc,\ntable.dataTable.table-sm > thead > tr td.dt-ordering-asc,\ntable.dataTable.table-sm > thead > tr td.dt-ordering-desc {\n  padding-right: 0.25rem;\n}\ntable.dataTable.table-sm > thead > tr th.dt-orderable-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-orderable-asc .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-orderable-desc .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-ordering-asc .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-ordering-desc .dt-column-order {\n  right: 0.25rem;\n}\ntable.dataTable.table-sm > thead > tr th.dt-type-date .dt-column-order, table.dataTable.table-sm > thead > tr th.dt-type-numeric .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-type-date .dt-column-order,\ntable.dataTable.table-sm > thead > tr td.dt-type-numeric .dt-column-order {\n  left: 0.25rem;\n}\n\ndiv.dt-scroll-head table.table-bordered {\n  border-bottom-width: 0;\n}\n\ndiv.table-responsive > div.dt-container > div.row {\n  margin-left: 0;\n  margin-right: 0;\n}\ndiv.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child {\n  padding-left: 0;\n}\ndiv.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child {\n  padding-right: 0;\n}\n\n:root[data-bs-theme=dark] {\n  --dt-row-hover: 255, 255, 255;\n  --dt-row-stripe: 255, 255, 255;\n  --dt-column-ordering: 255, 255, 255;\n}\n\n\n"
  },
  {
    "path": "src/static/scripts/datatables.js",
    "content": "/*\n * This combined file was created by the DataTables downloader builder:\n *   https://datatables.net/download\n *\n * To rebuild or modify this file with the latest versions of the included\n * software please visit:\n *   https://datatables.net/download/#bs5/dt-2.3.7\n *\n * Included libraries:\n *   DataTables 2.3.7\n */\n\n/*! DataTables 2.3.7\n * © SpryMedia Ltd - datatables.net/license\n */\n\n(function( factory ) {\n\t\"use strict\";\n\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD\n\t\tdefine( ['jquery'], function ( $ ) {\n\t\t\treturn factory( $, window, document );\n\t\t} );\n\t}\n\telse if ( typeof exports === 'object' ) {\n\t\t// CommonJS\n\t\t// jQuery's factory checks for a global window - if it isn't present then it\n\t\t// returns a factory function that expects the window object\n\t\tvar jq = require('jquery');\n\n\t\tif (typeof window === 'undefined') {\n\t\t\tmodule.exports = function (root, $) {\n\t\t\t\tif ( ! root ) {\n\t\t\t\t\t// CommonJS environments without a window global must pass a\n\t\t\t\t\t// root. This will give an error otherwise\n\t\t\t\t\troot = window;\n\t\t\t\t}\n\n\t\t\t\tif ( ! $ ) {\n\t\t\t\t\t$ = jq( root );\n\t\t\t\t}\n\n\t\t\t\treturn factory( $, root, root.document );\n\t\t\t};\n\t\t}\n\t\telse {\n\t\t\tmodule.exports = factory( jq, window, window.document );\n\t\t}\n\t}\n\telse {\n\t\t// Browser\n\t\twindow.DataTable = factory( jQuery, window, document );\n\t}\n}(function( $, window, document ) {\n\t\"use strict\";\n\n\t\n\tvar DataTable = function ( selector, options )\n\t{\n\t\t// Check if called with a window or jQuery object for DOM less applications\n\t\t// This is for backwards compatibility\n\t\tif (DataTable.factory(selector, options)) {\n\t\t\treturn DataTable;\n\t\t}\n\t\n\t\t// When creating with `new`, create a new DataTable, returning the API instance\n\t\tif (this instanceof DataTable) {\n\t\t\treturn $(selector).DataTable(options);\n\t\t}\n\t\telse {\n\t\t\t// Argument switching\n\t\t\toptions = selector;\n\t\t}\n\t\n\t\tvar _that = this;\n\t\tvar emptyInit = options === undefined;\n\t\tvar len = this.length;\n\t\n\t\tif ( emptyInit ) {\n\t\t\toptions = {};\n\t\t}\n\t\n\t\t// Method to get DT API instance from jQuery object\n\t\tthis.api = function ()\n\t\t{\n\t\t\treturn new _Api( this );\n\t\t};\n\t\n\t\tthis.each(function() {\n\t\t\t// For each initialisation we want to give it a clean initialisation\n\t\t\t// object that can be bashed around\n\t\t\tvar o = {};\n\t\t\tvar oInit = len > 1 ? // optimisation for single table case\n\t\t\t\t_fnExtend( o, options, true ) :\n\t\t\t\toptions;\n\t\n\t\t\t\n\t\t\tvar i=0, iLen;\n\t\t\tvar sId = this.getAttribute( 'id' );\n\t\t\tvar defaults = DataTable.defaults;\n\t\t\tvar $this = $(this);\n\t\t\t\n\t\t\t// Sanity check\n\t\t\tif ( this.nodeName.toLowerCase() != 'table' )\n\t\t\t{\n\t\t\t\t_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t// Special case for options\n\t\t\tif (oInit.on && oInit.on.options) {\n\t\t\t\t_fnListener($this, 'options', oInit.on.options);\t\n\t\t\t}\n\t\t\t\n\t\t\t$this.trigger( 'options.dt', oInit );\n\t\t\t\n\t\t\t/* Backwards compatibility for the defaults */\n\t\t\t_fnCompatOpts( defaults );\n\t\t\t_fnCompatCols( defaults.column );\n\t\t\t\n\t\t\t/* Convert the camel-case defaults to Hungarian */\n\t\t\t_fnCamelToHungarian( defaults, defaults, true );\n\t\t\t_fnCamelToHungarian( defaults.column, defaults.column, true );\n\t\t\t\n\t\t\t/* Setting up the initialisation object */\n\t\t\t_fnCamelToHungarian( defaults, $.extend( oInit, _fnEscapeObject($this.data()) ), true );\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t/* Check to see if we are re-initialising a table */\n\t\t\tvar allSettings = DataTable.settings;\n\t\t\tfor ( i=0, iLen=allSettings.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\tvar s = allSettings[i];\n\t\t\t\n\t\t\t\t/* Base check on table node */\n\t\t\t\tif (\n\t\t\t\t\ts.nTable == this ||\n\t\t\t\t\t(s.nTHead && s.nTHead.parentNode == this) ||\n\t\t\t\t\t(s.nTFoot && s.nTFoot.parentNode == this)\n\t\t\t\t) {\n\t\t\t\t\tvar bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve;\n\t\t\t\t\tvar bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy;\n\t\t\t\n\t\t\t\t\tif ( emptyInit || bRetrieve )\n\t\t\t\t\t{\n\t\t\t\t\t\treturn s.oInstance;\n\t\t\t\t\t}\n\t\t\t\t\telse if ( bDestroy )\n\t\t\t\t\t{\n\t\t\t\t\t\tnew DataTable.Api(s).destroy();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t_fnLog( s, 0, 'Cannot reinitialise DataTable', 3 );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t/* If the element we are initialising has the same ID as a table which was previously\n\t\t\t\t * initialised, but the table nodes don't match (from before) then we destroy the old\n\t\t\t\t * instance by simply deleting it. This is under the assumption that the table has been\n\t\t\t\t * destroyed by other methods. Anyone using non-id selectors will need to do this manually\n\t\t\t\t */\n\t\t\t\tif ( s.sTableId == this.id )\n\t\t\t\t{\n\t\t\t\t\tallSettings.splice( i, 1 );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t/* Ensure the table has an ID - required for accessibility */\n\t\t\tif ( sId === null || sId === \"\" )\n\t\t\t{\n\t\t\t\tsId = \"DataTables_Table_\"+(DataTable.ext._unique++);\n\t\t\t\tthis.id = sId;\n\t\t\t}\n\t\t\t\n\t\t\t// Replacing an existing colgroup with our own. Not ideal, but a merge could take a lot of code\n\t\t\t$this.children('colgroup').remove();\n\t\t\t\n\t\t\t/* Create the settings object for this table and set some of the default parameters */\n\t\t\tvar oSettings = $.extend( true, {}, DataTable.models.oSettings, {\n\t\t\t\t\"sDestroyWidth\": $this[0].style.width,\n\t\t\t\t\"sInstance\":     sId,\n\t\t\t\t\"sTableId\":      sId,\n\t\t\t\tcolgroup: $('<colgroup>'),\n\t\t\t\tfastData: function (row, column, type) {\n\t\t\t\t\treturn _fnGetCellData(oSettings, row, column, type);\n\t\t\t\t}\n\t\t\t} );\n\t\t\toSettings.nTable = this;\n\t\t\toSettings.oInit  = oInit;\n\t\t\t\n\t\t\tallSettings.push( oSettings );\n\t\t\t\n\t\t\t// Make a single API instance available for internal handling\n\t\t\toSettings.api = new _Api( oSettings );\n\t\t\t\n\t\t\t// Need to add the instance after the instance after the settings object has been added\n\t\t\t// to the settings array, so we can self reference the table instance if more than one\n\t\t\toSettings.oInstance = (_that.length===1) ? _that : $this.dataTable();\n\t\t\t\n\t\t\t// Backwards compatibility, before we apply all the defaults\n\t\t\t_fnCompatOpts( oInit );\n\t\t\t\n\t\t\t// If the length menu is given, but the init display length is not, use the length menu\n\t\t\tif ( oInit.aLengthMenu && ! oInit.iDisplayLength )\n\t\t\t{\n\t\t\t\toInit.iDisplayLength = Array.isArray(oInit.aLengthMenu[0])\n\t\t\t\t\t? oInit.aLengthMenu[0][0]\n\t\t\t\t\t: $.isPlainObject( oInit.aLengthMenu[0] )\n\t\t\t\t\t\t? oInit.aLengthMenu[0].value\n\t\t\t\t\t\t: oInit.aLengthMenu[0];\n\t\t\t}\n\t\t\t\n\t\t\t// Apply the defaults and init options to make a single init object will all\n\t\t\t// options defined from defaults and instance options.\n\t\t\toInit = _fnExtend( $.extend( true, {}, defaults ), oInit );\n\t\t\t\n\t\t\t\n\t\t\t// Map the initialisation options onto the settings object\n\t\t\t_fnMap( oSettings.oFeatures, oInit, [\n\t\t\t\t\"bPaginate\",\n\t\t\t\t\"bLengthChange\",\n\t\t\t\t\"bFilter\",\n\t\t\t\t\"bSort\",\n\t\t\t\t\"bSortMulti\",\n\t\t\t\t\"bInfo\",\n\t\t\t\t\"bProcessing\",\n\t\t\t\t\"bAutoWidth\",\n\t\t\t\t\"bSortClasses\",\n\t\t\t\t\"bServerSide\",\n\t\t\t\t\"bDeferRender\"\n\t\t\t] );\n\t\t\t_fnMap( oSettings, oInit, [\n\t\t\t\t\"ajax\",\n\t\t\t\t\"fnFormatNumber\",\n\t\t\t\t\"sServerMethod\",\n\t\t\t\t\"aaSorting\",\n\t\t\t\t\"aaSortingFixed\",\n\t\t\t\t\"aLengthMenu\",\n\t\t\t\t\"sPaginationType\",\n\t\t\t\t\"iStateDuration\",\n\t\t\t\t\"bSortCellsTop\",\n\t\t\t\t\"iTabIndex\",\n\t\t\t\t\"sDom\",\n\t\t\t\t\"fnStateLoadCallback\",\n\t\t\t\t\"fnStateSaveCallback\",\n\t\t\t\t\"renderer\",\n\t\t\t\t\"searchDelay\",\n\t\t\t\t\"rowId\",\n\t\t\t\t\"caption\",\n\t\t\t\t\"layout\",\n\t\t\t\t\"orderDescReverse\",\n\t\t\t\t\"orderIndicators\",\n\t\t\t\t\"orderHandler\",\n\t\t\t\t\"titleRow\",\n\t\t\t\t\"typeDetect\",\n\t\t\t\t\"columnTitleTag\",\n\t\t\t\t[ \"iCookieDuration\", \"iStateDuration\" ], // backwards compat\n\t\t\t\t[ \"oSearch\", \"oPreviousSearch\" ],\n\t\t\t\t[ \"aoSearchCols\", \"aoPreSearchCols\" ],\n\t\t\t\t[ \"iDisplayLength\", \"_iDisplayLength\" ]\n\t\t\t] );\n\t\t\t_fnMap( oSettings.oScroll, oInit, [\n\t\t\t\t[ \"sScrollX\", \"sX\" ],\n\t\t\t\t[ \"sScrollXInner\", \"sXInner\" ],\n\t\t\t\t[ \"sScrollY\", \"sY\" ],\n\t\t\t\t[ \"bScrollCollapse\", \"bCollapse\" ]\n\t\t\t] );\n\t\t\t_fnMap( oSettings.oLanguage, oInit, \"fnInfoCallback\" );\n\t\t\t\n\t\t\t/* Callback functions which are array driven */\n\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams );\n\t\t\t_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded );\n\t\t\t_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback );\n\t\t\t_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow );\n\t\t\t_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback );\n\t\t\t_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback );\n\t\t\t_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete );\n\t\t\t_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback );\n\t\t\t\n\t\t\toSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );\n\t\t\t\n\t\t\t// Add event listeners\n\t\t\tif (oInit.on) {\n\t\t\t\tObject.keys(oInit.on).forEach(function (key) {\n\t\t\t\t\t_fnListener($this, key, oInit.on[key]);\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t/* Browser support detection */\n\t\t\t_fnBrowserDetect( oSettings );\n\t\t\t\n\t\t\tvar oClasses = oSettings.oClasses;\n\t\t\t\n\t\t\t$.extend( oClasses, DataTable.ext.classes, oInit.oClasses );\n\t\t\t$this.addClass( oClasses.table );\n\t\t\t\n\t\t\tif (! oSettings.oFeatures.bPaginate) {\n\t\t\t\toInit.iDisplayStart = 0;\n\t\t\t}\n\t\t\t\n\t\t\tif ( oSettings.iInitDisplayStart === undefined )\n\t\t\t{\n\t\t\t\t/* Display start point, taking into account the save saving */\n\t\t\t\toSettings.iInitDisplayStart = oInit.iDisplayStart;\n\t\t\t\toSettings._iDisplayStart = oInit.iDisplayStart;\n\t\t\t}\n\t\t\t\n\t\t\tvar defer = oInit.iDeferLoading;\n\t\t\tif ( defer !== null )\n\t\t\t{\n\t\t\t\toSettings.deferLoading = true;\n\t\t\t\n\t\t\t\tvar tmp = Array.isArray(defer);\n\t\t\t\toSettings._iRecordsDisplay = tmp ? defer[0] : defer;\n\t\t\t\toSettings._iRecordsTotal = tmp ? defer[1] : defer;\n\t\t\t}\n\t\t\t\n\t\t\t/*\n\t\t\t * Columns\n\t\t\t * See if we should load columns automatically or use defined ones\n\t\t\t */\n\t\t\tvar columnsInit = [];\n\t\t\tvar thead = this.getElementsByTagName('thead');\n\t\t\tvar initHeaderLayout = _fnDetectHeader( oSettings, thead[0] );\n\t\t\t\n\t\t\t// If we don't have a columns array, then generate one with nulls\n\t\t\tif ( oInit.aoColumns ) {\n\t\t\t\tcolumnsInit = oInit.aoColumns;\n\t\t\t}\n\t\t\telse if ( initHeaderLayout.length ) {\n\t\t\t\tfor ( i=0, iLen=initHeaderLayout[0].length ; i<iLen ; i++ ) {\n\t\t\t\t\tcolumnsInit.push( null );\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Add the columns\n\t\t\tfor ( i=0, iLen=columnsInit.length ; i<iLen ; i++ ) {\n\t\t\t\t_fnAddColumn( oSettings );\n\t\t\t}\n\t\t\t\n\t\t\t// Apply the column definitions\n\t\t\t_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, columnsInit, initHeaderLayout, function (iCol, oDef) {\n\t\t\t\t_fnColumnOptions( oSettings, iCol, oDef );\n\t\t\t} );\n\t\t\t\n\t\t\t/* HTML5 attribute detection - build an mData object automatically if the\n\t\t\t * attributes are found\n\t\t\t */\n\t\t\tvar rowOne = $this.children('tbody').find('tr:first-child').eq(0);\n\t\t\t\n\t\t\tif ( rowOne.length ) {\n\t\t\t\tvar a = function ( cell, name ) {\n\t\t\t\t\treturn cell.getAttribute( 'data-'+name ) !== null ? name : null;\n\t\t\t\t};\n\t\t\t\n\t\t\t\t$( rowOne[0] ).children('th, td').each( function (i, cell) {\n\t\t\t\t\tvar col = oSettings.aoColumns[i];\n\t\t\t\n\t\t\t\t\tif (! col) {\n\t\t\t\t\t\t_fnLog( oSettings, 0, 'Incorrect column count', 18 );\n\t\t\t\t\t}\n\t\t\t\n\t\t\t\t\tif ( col.mData === i ) {\n\t\t\t\t\t\tvar sort = a( cell, 'sort' ) || a( cell, 'order' );\n\t\t\t\t\t\tvar filter = a( cell, 'filter' ) || a( cell, 'search' );\n\t\t\t\n\t\t\t\t\t\tif ( sort !== null || filter !== null ) {\n\t\t\t\t\t\t\tcol.mData = {\n\t\t\t\t\t\t\t\t_:      i+'.display',\n\t\t\t\t\t\t\t\tsort:   sort !== null   ? i+'.@data-'+sort   : undefined,\n\t\t\t\t\t\t\t\ttype:   sort !== null   ? i+'.@data-'+sort   : undefined,\n\t\t\t\t\t\t\t\tfilter: filter !== null ? i+'.@data-'+filter : undefined\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tcol._isArrayHost = true;\n\t\t\t\n\t\t\t\t\t\t\t_fnColumnOptions( oSettings, i );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t\t\n\t\t\t// Must be done after everything which can be overridden by the state saving!\n\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState );\n\t\t\t\n\t\t\tvar features = oSettings.oFeatures;\n\t\t\tif ( oInit.bStateSave )\n\t\t\t{\n\t\t\t\tfeatures.bStateSave = true;\n\t\t\t}\n\t\t\t\n\t\t\t// If aaSorting is not defined, then we use the first indicator in asSorting\n\t\t\t// in case that has been altered, so the default sort reflects that option\n\t\t\tif ( oInit.aaSorting === undefined ) {\n\t\t\t\tvar sorting = oSettings.aaSorting;\n\t\t\t\tfor ( i=0, iLen=sorting.length ; i<iLen ; i++ ) {\n\t\t\t\t\tsorting[i][1] = oSettings.aoColumns[ i ].asSorting[0];\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Do a first pass on the sorting classes (allows any size changes to be taken into\n\t\t\t// account, and also will apply sorting disabled classes if disabled\n\t\t\t_fnSortingClasses( oSettings );\n\t\t\t\n\t\t\t_fnCallbackReg( oSettings, 'aoDrawCallback', function () {\n\t\t\t\tif ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) {\n\t\t\t\t\t_fnSortingClasses( oSettings );\n\t\t\t\t}\n\t\t\t} );\n\t\t\t\n\t\t\t\n\t\t\t/*\n\t\t\t * Table HTML init\n\t\t\t * Cache the header, body and footer as required, creating them if needed\n\t\t\t */\n\t\t\tvar caption = $this.children('caption');\n\t\t\t\n\t\t\tif ( oSettings.caption ) {\n\t\t\t\tif ( caption.length === 0 ) {\n\t\t\t\t\tcaption = $('<caption/>').prependTo( $this );\n\t\t\t\t}\n\t\t\t\n\t\t\t\tcaption.html( oSettings.caption );\n\t\t\t}\n\t\t\t\n\t\t\t// Store the caption side, so we can remove the element from the document\n\t\t\t// when creating the element\n\t\t\tif (caption.length) {\n\t\t\t\tcaption[0]._captionSide = caption.css('caption-side');\n\t\t\t\toSettings.captionNode = caption[0];\n\t\t\t}\n\t\t\t\n\t\t\t// Place the colgroup element in the correct location for the HTML structure\n\t\t\tif (caption.length) {\n\t\t\t\toSettings.colgroup.insertAfter(caption);\n\t\t\t}\n\t\t\telse {\n\t\t\t\toSettings.colgroup.prependTo(oSettings.nTable);\n\t\t\t}\n\t\t\t\n\t\t\tif ( thead.length === 0 ) {\n\t\t\t\tthead = $('<thead/>').appendTo($this);\n\t\t\t}\n\t\t\toSettings.nTHead = thead[0];\n\t\t\t\n\t\t\tvar tbody = $this.children('tbody');\n\t\t\tif ( tbody.length === 0 ) {\n\t\t\t\ttbody = $('<tbody/>').insertAfter(thead);\n\t\t\t}\n\t\t\toSettings.nTBody = tbody[0];\n\t\t\t\n\t\t\tvar tfoot = $this.children('tfoot');\n\t\t\tif ( tfoot.length === 0 ) {\n\t\t\t\t// If we are a scrolling table, and no footer has been given, then we need to create\n\t\t\t\t// a tfoot element for the caption element to be appended to\n\t\t\t\ttfoot = $('<tfoot/>').appendTo($this);\n\t\t\t}\n\t\t\toSettings.nTFoot = tfoot[0];\n\t\t\t\n\t\t\t// Copy the data index array\n\t\t\toSettings.aiDisplay = oSettings.aiDisplayMaster.slice();\n\t\t\t\n\t\t\t// Initialisation complete - table can be drawn\n\t\t\toSettings.bInitialised = true;\n\t\t\t\n\t\t\t// Language definitions\n\t\t\tvar oLanguage = oSettings.oLanguage;\n\t\t\t$.extend( true, oLanguage, oInit.oLanguage );\n\t\t\t\n\t\t\tif ( oLanguage.sUrl ) {\n\t\t\t\t// Get the language definitions from a file\n\t\t\t\t$.ajax( {\n\t\t\t\t\tdataType: 'json',\n\t\t\t\t\turl: oLanguage.sUrl,\n\t\t\t\t\tsuccess: function ( json ) {\n\t\t\t\t\t\t_fnCamelToHungarian( defaults.oLanguage, json );\n\t\t\t\t\t\t$.extend( true, oLanguage, json, oSettings.oInit.oLanguage );\n\t\t\t\n\t\t\t\t\t\t_fnCallbackFire( oSettings, null, 'i18n', [oSettings], true);\n\t\t\t\t\t\t_fnInitialise( oSettings );\n\t\t\t\t\t},\n\t\t\t\t\terror: function () {\n\t\t\t\t\t\t// Error occurred loading language file\n\t\t\t\t\t\t_fnLog( oSettings, 0, 'i18n file loading error', 21 );\n\t\t\t\n\t\t\t\t\t\t// Continue on as best we can\n\t\t\t\t\t\t_fnInitialise( oSettings );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t_fnCallbackFire( oSettings, null, 'i18n', [oSettings], true);\n\t\t\t\t_fnInitialise( oSettings );\n\t\t\t}\n\t\t} );\n\t\t_that = null;\n\t\treturn this;\n\t};\n\t\n\t\n\t\n\t/**\n\t * DataTables extensions\n\t * \n\t * This namespace acts as a collection area for plug-ins that can be used to\n\t * extend DataTables capabilities. Indeed many of the build in methods\n\t * use this method to provide their own capabilities (sorting methods for\n\t * example).\n\t *\n\t * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy\n\t * reasons\n\t *\n\t *  @namespace\n\t */\n\tDataTable.ext = _ext = {\n\t\t/**\n\t\t * DataTables build type (expanded by the download builder)\n\t\t *\n\t\t *  @type string\n\t\t */\n\t\tbuilder: \"bs5/dt-2.3.7\",\n\t\n\t\t/**\n\t\t * Buttons. For use with the Buttons extension for DataTables. This is\n\t\t * defined here so other extensions can define buttons regardless of load\n\t\t * order. It is _not_ used by DataTables core.\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\tbuttons: {},\n\t\n\t\n\t\t/**\n\t\t * ColumnControl buttons and content\n\t\t *\n\t\t *  @type object\n\t\t */\n\t\tccContent: {},\n\t\n\t\n\t\t/**\n\t\t * Element class names\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t */\n\t\tclasses: {},\n\t\n\t\n\t\t/**\n\t\t * Error reporting.\n\t\t * \n\t\t * How should DataTables report an error. Can take the value 'alert',\n\t\t * 'throw', 'none' or a function.\n\t\t *\n\t\t *  @type string|function\n\t\t *  @default alert\n\t\t */\n\t\terrMode: \"alert\",\n\t\n\t\t/** HTML entity escaping */\n\t\tescape: {\n\t\t\t/** When reading data-* attributes for initialisation options */\n\t\t\tattributes: false\n\t\t},\n\t\n\t\t/**\n\t\t * Legacy so v1 plug-ins don't throw js errors on load\n\t\t */\n\t\tfeature: [],\n\t\n\t\t/**\n\t\t * Feature plug-ins.\n\t\t * \n\t\t * This is an object of callbacks which provide the features for DataTables\n\t\t * to be initialised via the `layout` option.\n\t\t */\n\t\tfeatures: {},\n\t\n\t\n\t\t/**\n\t\t * Row searching.\n\t\t * \n\t\t * This method of searching is complimentary to the default type based\n\t\t * searching, and a lot more comprehensive as it allows you complete control\n\t\t * over the searching logic. Each element in this array is a function\n\t\t * (parameters described below) that is called for every row in the table,\n\t\t * and your logic decides if it should be included in the searching data set\n\t\t * or not.\n\t\t *\n\t\t * Searching functions have the following input parameters:\n\t\t *\n\t\t * 1. `{object}` DataTables settings object: see\n\t\t *    {@link DataTable.models.oSettings}\n\t\t * 2. `{array|object}` Data for the row to be processed (same as the\n\t\t *    original format that was passed in as the data source, or an array\n\t\t *    from a DOM data source\n\t\t * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which\n\t\t *    can be useful to retrieve the `TR` element if you need DOM interaction.\n\t\t *\n\t\t * And the following return is expected:\n\t\t *\n\t\t * * {boolean} Include the row in the searched result set (true) or not\n\t\t *   (false)\n\t\t *\n\t\t * Note that as with the main search ability in DataTables, technically this\n\t\t * is \"filtering\", since it is subtractive. However, for consistency in\n\t\t * naming we call it searching here.\n\t\t *\n\t\t *  @type array\n\t\t *  @default []\n\t\t *\n\t\t *  @example\n\t\t *    // The following example shows custom search being applied to the\n\t\t *    // fourth column (i.e. the data[3] index) based on two input values\n\t\t *    // from the end-user, matching the data in a certain range.\n\t\t *    $.fn.dataTable.ext.search.push(\n\t\t *      function( settings, data, dataIndex ) {\n\t\t *        var min = document.getElementById('min').value * 1;\n\t\t *        var max = document.getElementById('max').value * 1;\n\t\t *        var version = data[3] == \"-\" ? 0 : data[3]*1;\n\t\t *\n\t\t *        if ( min == \"\" && max == \"\" ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min == \"\" && version < max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min < version && \"\" == max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        else if ( min < version && version < max ) {\n\t\t *          return true;\n\t\t *        }\n\t\t *        return false;\n\t\t *      }\n\t\t *    );\n\t\t */\n\t\tsearch: [],\n\t\n\t\n\t\t/**\n\t\t * Selector extensions\n\t\t *\n\t\t * The `selector` option can be used to extend the options available for the\n\t\t * selector modifier options (`selector-modifier` object data type) that\n\t\t * each of the three built in selector types offer (row, column and cell +\n\t\t * their plural counterparts). For example the Select extension uses this\n\t\t * mechanism to provide an option to select only rows, columns and cells\n\t\t * that have been marked as selected by the end user (`{selected: true}`),\n\t\t * which can be used in conjunction with the existing built in selector\n\t\t * options.\n\t\t *\n\t\t * Each property is an array to which functions can be pushed. The functions\n\t\t * take three attributes:\n\t\t *\n\t\t * * Settings object for the host table\n\t\t * * Options object (`selector-modifier` object type)\n\t\t * * Array of selected item indexes\n\t\t *\n\t\t * The return is an array of the resulting item indexes after the custom\n\t\t * selector has been applied.\n\t\t *\n\t\t *  @type object\n\t\t */\n\t\tselector: {\n\t\t\tcell: [],\n\t\t\tcolumn: [],\n\t\t\trow: []\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Legacy configuration options. Enable and disable legacy options that\n\t\t * are available in DataTables.\n\t\t *\n\t\t *  @type object\n\t\t */\n\t\tlegacy: {\n\t\t\t/**\n\t\t\t * Enable / disable DataTables 1.9 compatible server-side processing\n\t\t\t * requests\n\t\t\t *\n\t\t\t *  @type boolean\n\t\t\t *  @default null\n\t\t\t */\n\t\t\tajax: null\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Pagination plug-in methods.\n\t\t * \n\t\t * Each entry in this object is a function and defines which buttons should\n\t\t * be shown by the pagination rendering method that is used for the table:\n\t\t * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the\n\t\t * buttons are displayed in the document, while the functions here tell it\n\t\t * what buttons to display. This is done by returning an array of button\n\t\t * descriptions (what each button will do).\n\t\t *\n\t\t * Pagination types (the four built in options and any additional plug-in\n\t\t * options defined here) can be used through the `paginationType`\n\t\t * initialisation parameter.\n\t\t *\n\t\t * The functions defined take two parameters:\n\t\t *\n\t\t * 1. `{int} page` The current page index\n\t\t * 2. `{int} pages` The number of pages in the table\n\t\t *\n\t\t * Each function is expected to return an array where each element of the\n\t\t * array can be one of:\n\t\t *\n\t\t * * `first` - Jump to first page when activated\n\t\t * * `last` - Jump to last page when activated\n\t\t * * `previous` - Show previous page when activated\n\t\t * * `next` - Show next page when activated\n\t\t * * `{int}` - Show page of the index given\n\t\t * * `{array}` - A nested array containing the above elements to add a\n\t\t *   containing 'DIV' element (might be useful for styling).\n\t\t *\n\t\t * Note that DataTables v1.9- used this object slightly differently whereby\n\t\t * an object with two functions would be defined for each plug-in. That\n\t\t * ability is still supported by DataTables 1.10+ to provide backwards\n\t\t * compatibility, but this option of use is now decremented and no longer\n\t\t * documented in DataTables 1.10+.\n\t\t *\n\t\t *  @type object\n\t\t *  @default {}\n\t\t *\n\t\t *  @example\n\t\t *    // Show previous, next and current page buttons only\n\t\t *    $.fn.dataTableExt.oPagination.current = function ( page, pages ) {\n\t\t *      return [ 'previous', page, 'next' ];\n\t\t *    };\n\t\t */\n\t\tpager: {},\n\t\n\t\n\t\trenderer: {\n\t\t\tpageButton: {},\n\t\t\theader: {}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Ordering plug-ins - custom data source\n\t\t * \n\t\t * The extension options for ordering of data available here is complimentary\n\t\t * to the default type based ordering that DataTables typically uses. It\n\t\t * allows much greater control over the data that is being used to\n\t\t * order a column, but is necessarily therefore more complex.\n\t\t * \n\t\t * This type of ordering is useful if you want to do ordering based on data\n\t\t * live from the DOM (for example the contents of an 'input' element) rather\n\t\t * than just the static string that DataTables knows of.\n\t\t * \n\t\t * The way these plug-ins work is that you create an array of the values you\n\t\t * wish to be ordering for the column in question and then return that\n\t\t * array. The data in the array much be in the index order of the rows in\n\t\t * the table (not the currently ordering order!). Which order data gathering\n\t\t * function is run here depends on the `dt-init columns.orderDataType`\n\t\t * parameter that is used for the column (if any).\n\t\t *\n\t\t * The functions defined take two parameters:\n\t\t *\n\t\t * 1. `{object}` DataTables settings object: see\n\t\t *    {@link DataTable.models.oSettings}\n\t\t * 2. `{int}` Target column index\n\t\t *\n\t\t * Each function is expected to return an array:\n\t\t *\n\t\t * * `{array}` Data for the column to be ordering upon\n\t\t *\n\t\t *  @type array\n\t\t *\n\t\t *  @example\n\t\t *    // Ordering using `input` node values\n\t\t *    $.fn.dataTable.ext.order['dom-text'] = function  ( settings, col )\n\t\t *    {\n\t\t *      return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {\n\t\t *        return $('input', td).val();\n\t\t *      } );\n\t\t *    }\n\t\t */\n\t\torder: {},\n\t\n\t\n\t\t/**\n\t\t * Type based plug-ins.\n\t\t *\n\t\t * Each column in DataTables has a type assigned to it, either by automatic\n\t\t * detection or by direct assignment using the `type` option for the column.\n\t\t * The type of a column will effect how it is ordering and search (plug-ins\n\t\t * can also make use of the column type if required).\n\t\t *\n\t\t * @namespace\n\t\t */\n\t\ttype: {\n\t\t\t/**\n\t\t\t * Automatic column class assignment\n\t\t\t */\n\t\t\tclassName: {},\n\t\n\t\t\t/**\n\t\t\t * Type detection functions.\n\t\t\t *\n\t\t\t * The functions defined in this object are used to automatically detect\n\t\t\t * a column's type, making initialisation of DataTables super easy, even\n\t\t\t * when complex data is in the table.\n\t\t\t *\n\t\t\t * The functions defined take two parameters:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be analysed\n\t\t     *  2. `{settings}` DataTables settings object. This can be used to\n\t\t     *     perform context specific type detection - for example detection\n\t\t     *     based on language settings such as using a comma for a decimal\n\t\t     *     place. Generally speaking the options from the settings will not\n\t\t     *     be required\n\t\t\t *\n\t\t\t * Each function is expected to return:\n\t\t\t *\n\t\t\t * * `{string|null}` Data type detected, or null if unknown (and thus\n\t\t\t *   pass it on to the other type detection functions.\n\t\t\t *\n\t\t\t *  @type array\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Currency type detection plug-in:\n\t\t\t *    $.fn.dataTable.ext.type.detect.push(\n\t\t\t *      function ( data, settings ) {\n\t\t\t *        // Check the numeric part\n\t\t\t *        if ( ! data.substring(1).match(/[0-9]/) ) {\n\t\t\t *          return null;\n\t\t\t *        }\n\t\t\t *\n\t\t\t *        // Check prefixed by currency\n\t\t\t *        if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) {\n\t\t\t *          return 'currency';\n\t\t\t *        }\n\t\t\t *        return null;\n\t\t\t *      }\n\t\t\t *    );\n\t\t\t */\n\t\t\tdetect: [],\n\t\n\t\t\t/**\n\t\t\t * Automatic renderer assignment\n\t\t\t */\n\t\t\trender: {},\n\t\n\t\n\t\t\t/**\n\t\t\t * Type based search formatting.\n\t\t\t *\n\t\t\t * The type based searching functions can be used to pre-format the\n\t\t\t * data to be search on. For example, it can be used to strip HTML\n\t\t\t * tags or to de-format telephone numbers for numeric only searching.\n\t\t\t *\n\t\t\t * Note that is a search is not defined for a column of a given type,\n\t\t\t * no search formatting will be performed.\n\t\t\t * \n\t\t\t * Pre-processing of searching data plug-ins - When you assign the sType\n\t\t\t * for a column (or have it automatically detected for you by DataTables\n\t\t\t * or a type detection plug-in), you will typically be using this for\n\t\t\t * custom sorting, but it can also be used to provide custom searching\n\t\t\t * by allowing you to pre-processing the data and returning the data in\n\t\t\t * the format that should be searched upon. This is done by adding\n\t\t\t * functions this object with a parameter name which matches the sType\n\t\t\t * for that target column. This is the corollary of <i>afnSortData</i>\n\t\t\t * for searching data.\n\t\t\t *\n\t\t\t * The functions defined take a single parameter:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be prepared for searching\n\t\t\t *\n\t\t\t * Each function is expected to return:\n\t\t\t *\n\t\t\t * * `{string|null}` Formatted string that will be used for the searching.\n\t\t\t *\n\t\t\t *  @type object\n\t\t\t *  @default {}\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) {\n\t\t\t *      return d.replace(/\\n/g,\" \").replace( /<.*?>/g, \"\" );\n\t\t\t *    }\n\t\t\t */\n\t\t\tsearch: {},\n\t\n\t\n\t\t\t/**\n\t\t\t * Type based ordering.\n\t\t\t *\n\t\t\t * The column type tells DataTables what ordering to apply to the table\n\t\t\t * when a column is sorted upon. The order for each type that is defined,\n\t\t\t * is defined by the functions available in this object.\n\t\t\t *\n\t\t\t * Each ordering option can be described by three properties added to\n\t\t\t * this object:\n\t\t\t *\n\t\t\t * * `{type}-pre` - Pre-formatting function\n\t\t\t * * `{type}-asc` - Ascending order function\n\t\t\t * * `{type}-desc` - Descending order function\n\t\t\t *\n\t\t\t * All three can be used together, only `{type}-pre` or only\n\t\t\t * `{type}-asc` and `{type}-desc` together. It is generally recommended\n\t\t\t * that only `{type}-pre` is used, as this provides the optimal\n\t\t\t * implementation in terms of speed, although the others are provided\n\t\t\t * for compatibility with existing JavaScript sort functions.\n\t\t\t *\n\t\t\t * `{type}-pre`: Functions defined take a single parameter:\n\t\t\t *\n\t\t     *  1. `{*}` Data from the column cell to be prepared for ordering\n\t\t\t *\n\t\t\t * And return:\n\t\t\t *\n\t\t\t * * `{*}` Data to be sorted upon\n\t\t\t *\n\t\t\t * `{type}-asc` and `{type}-desc`: Functions are typical JavaScript sort\n\t\t\t * functions, taking two parameters:\n\t\t\t *\n\t\t     *  1. `{*}` Data to compare to the second parameter\n\t\t     *  2. `{*}` Data to compare to the first parameter\n\t\t\t *\n\t\t\t * And returning:\n\t\t\t *\n\t\t\t * * `{*}` Ordering match: <0 if first parameter should be sorted lower\n\t\t\t *   than the second parameter, ===0 if the two parameters are equal and\n\t\t\t *   >0 if the first parameter should be sorted height than the second\n\t\t\t *   parameter.\n\t\t\t * \n\t\t\t *  @type object\n\t\t\t *  @default {}\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Numeric ordering of formatted numbers with a pre-formatter\n\t\t\t *    $.extend( $.fn.dataTable.ext.type.order, {\n\t\t\t *      \"string-pre\": function(x) {\n\t\t\t *        a = (a === \"-\" || a === \"\") ? 0 : a.replace( /[^\\d\\-\\.]/g, \"\" );\n\t\t\t *        return parseFloat( a );\n\t\t\t *      }\n\t\t\t *    } );\n\t\t\t *\n\t\t\t *  @example\n\t\t\t *    // Case-sensitive string ordering, with no pre-formatting method\n\t\t\t *    $.extend( $.fn.dataTable.ext.order, {\n\t\t\t *      \"string-case-asc\": function(x,y) {\n\t\t\t *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));\n\t\t\t *      },\n\t\t\t *      \"string-case-desc\": function(x,y) {\n\t\t\t *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));\n\t\t\t *      }\n\t\t\t *    } );\n\t\t\t */\n\t\t\torder: {}\n\t\t},\n\t\n\t\t/**\n\t\t * Unique DataTables instance counter\n\t\t *\n\t\t * @type int\n\t\t * @private\n\t\t */\n\t\t_unique: 0,\n\t\n\t\n\t\t//\n\t\t// Depreciated\n\t\t// The following properties are retained for backwards compatibility only.\n\t\t// The should not be used in new projects and will be removed in a future\n\t\t// version\n\t\t//\n\t\n\t\t/**\n\t\t * Version check function.\n\t\t *  @type function\n\t\t *  @depreciated Since 1.10\n\t\t */\n\t\tfnVersionCheck: DataTable.fnVersionCheck,\n\t\n\t\n\t\t/**\n\t\t * Index for what 'this' index API functions should use\n\t\t *  @type int\n\t\t *  @deprecated Since v1.10\n\t\t */\n\t\tiApiIndex: 0,\n\t\n\t\n\t\t/**\n\t\t * Software version\n\t\t *  @type string\n\t\t *  @deprecated Since v1.10\n\t\t */\n\t\tsVersion: DataTable.version\n\t};\n\t\n\t\n\t//\n\t// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts\n\t//\n\t$.extend( _ext, {\n\t\tafnFiltering: _ext.search,\n\t\taTypes:       _ext.type.detect,\n\t\tofnSearch:    _ext.type.search,\n\t\toSort:        _ext.type.order,\n\t\tafnSortData:  _ext.order,\n\t\taoFeatures:   _ext.feature,\n\t\toStdClasses:  _ext.classes,\n\t\toPagination:  _ext.pager\n\t} );\n\t\n\t\n\t$.extend( DataTable.ext.classes, {\n\t\tcontainer: 'dt-container',\n\t\tempty: {\n\t\t\trow: 'dt-empty'\n\t\t},\n\t\tinfo: {\n\t\t\tcontainer: 'dt-info'\n\t\t},\n\t\tlayout: {\n\t\t\trow: 'dt-layout-row',\n\t\t\tcell: 'dt-layout-cell',\n\t\t\ttableRow: 'dt-layout-table',\n\t\t\ttableCell: '',\n\t\t\tstart: 'dt-layout-start',\n\t\t\tend: 'dt-layout-end',\n\t\t\tfull: 'dt-layout-full'\n\t\t},\n\t\tlength: {\n\t\t\tcontainer: 'dt-length',\n\t\t\tselect: 'dt-input'\n\t\t},\n\t\torder: {\n\t\t\tcanAsc: 'dt-orderable-asc',\n\t\t\tcanDesc: 'dt-orderable-desc',\n\t\t\tisAsc: 'dt-ordering-asc',\n\t\t\tisDesc: 'dt-ordering-desc',\n\t\t\tnone: 'dt-orderable-none',\n\t\t\tposition: 'sorting_'\n\t\t},\n\t\tprocessing: {\n\t\t\tcontainer: 'dt-processing'\n\t\t},\n\t\tscrolling: {\n\t\t\tbody: 'dt-scroll-body',\n\t\t\tcontainer: 'dt-scroll',\n\t\t\tfooter: {\n\t\t\t\tself: 'dt-scroll-foot',\n\t\t\t\tinner: 'dt-scroll-footInner'\n\t\t\t},\n\t\t\theader: {\n\t\t\t\tself: 'dt-scroll-head',\n\t\t\t\tinner: 'dt-scroll-headInner'\n\t\t\t}\n\t\t},\n\t\tsearch: {\n\t\t\tcontainer: 'dt-search',\n\t\t\tinput: 'dt-input'\n\t\t},\n\t\ttable: 'dataTable',\t\n\t\ttbody: {\n\t\t\tcell: '',\n\t\t\trow: ''\n\t\t},\n\t\tthead: {\n\t\t\tcell: '',\n\t\t\trow: ''\n\t\t},\n\t\ttfoot: {\n\t\t\tcell: '',\n\t\t\trow: ''\n\t\t},\n\t\tpaging: {\n\t\t\tactive: 'current',\n\t\t\tbutton: 'dt-paging-button',\n\t\t\tcontainer: 'dt-paging',\n\t\t\tdisabled: 'disabled',\n\t\t\tnav: ''\n\t\t}\n\t} );\n\t\n\t\n\t/*\n\t * It is useful to have variables which are scoped locally so only the\n\t * DataTables functions can access them and they don't leak into global space.\n\t * At the same time these functions are often useful over multiple files in the\n\t * core and API, so we list, or at least document, all variables which are used\n\t * by DataTables as private variables here. This also ensures that there is no\n\t * clashing of variable names and that they can easily referenced for reuse.\n\t */\n\t\n\t\n\t// Defined else where\n\t//  _selector_run\n\t//  _selector_opts\n\t//  _selector_row_indexes\n\t\n\tvar _ext; // DataTable.ext\n\tvar _Api; // DataTable.Api\n\tvar _api_register; // DataTable.Api.register\n\tvar _api_registerPlural; // DataTable.Api.registerPlural\n\t\n\tvar _re_dic = {};\n\tvar _re_new_lines = /[\\r\\n\\u2028]/g;\n\tvar _re_html = /<([^>]*>)/g;\n\tvar _max_str_len = Math.pow(2, 28);\n\t\n\t// This is not strict ISO8601 - Date.parse() is quite lax, although\n\t// implementations differ between browsers.\n\tvar _re_date = /^\\d{2,4}[./-]\\d{1,2}[./-]\\d{1,2}([T ]{1}\\d{1,2}[:.]\\d{2}([.:]\\d{2})?)?$/;\n\t\n\t// Escape regular expression special characters\n\tvar _re_escape_regex = new RegExp( '(\\\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\\\', '$', '^', '-' ].join('|\\\\') + ')', 'g' );\n\t\n\t// https://en.wikipedia.org/wiki/Foreign_exchange_market\n\t// - \\u20BD - Russian ruble.\n\t// - \\u20a9 - South Korean Won\n\t// - \\u20BA - Turkish Lira\n\t// - \\u20B9 - Indian Rupee\n\t// - R - Brazil (R$) and South Africa\n\t// - fr - Swiss Franc\n\t// - kr - Swedish krona, Norwegian krone and Danish krone\n\t// - \\u2009 is thin space and \\u202F is narrow no-break space, both used in many\n\t// - Ƀ - Bitcoin\n\t// - Ξ - Ethereum\n\t//   standards as thousands separators.\n\tvar _re_formatted_numeric = /['\\u00A0,$£€¥%\\u2009\\u202F\\u20BD\\u20a9\\u20BArfkɃΞ]/gi;\n\t\n\t\n\tvar _empty = function ( d ) {\n\t\treturn !d || d === true || d === '-' ? true : false;\n\t};\n\t\n\t\n\tvar _intVal = function ( s ) {\n\t\tvar integer = parseInt( s, 10 );\n\t\treturn !isNaN(integer) && isFinite(s) ? integer : null;\n\t};\n\t\n\t// Convert from a formatted number with characters other than `.` as the\n\t// decimal place, to a JavaScript number\n\tvar _numToDecimal = function ( num, decimalPoint ) {\n\t\t// Cache created regular expressions for speed as this function is called often\n\t\tif ( ! _re_dic[ decimalPoint ] ) {\n\t\t\t_re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' );\n\t\t}\n\t\treturn typeof num === 'string' && decimalPoint !== '.' ?\n\t\t\tnum.replace( /\\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) :\n\t\t\tnum;\n\t};\n\t\n\t\n\tvar _isNumber = function ( d, decimalPoint, formatted, allowEmpty ) {\n\t\tvar type = typeof d;\n\t\tvar strType = type === 'string';\n\t\n\t\tif ( type === 'number' || type === 'bigint') {\n\t\t\treturn true;\n\t\t}\n\t\n\t\t// If empty return immediately so there must be a number if it is a\n\t\t// formatted string (this stops the string \"k\", or \"kr\", etc being detected\n\t\t// as a formatted number for currency\n\t\tif ( allowEmpty && _empty( d ) ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\tif ( decimalPoint && strType ) {\n\t\t\td = _numToDecimal( d, decimalPoint );\n\t\t}\n\t\n\t\tif ( formatted && strType ) {\n\t\t\td = d.replace( _re_formatted_numeric, '' );\n\t\t}\n\t\n\t\treturn !isNaN( parseFloat(d) ) && isFinite( d );\n\t};\n\t\n\t\n\t// A string without HTML in it can be considered to be HTML still\n\tvar _isHtml = function ( d ) {\n\t\treturn _empty( d ) || typeof d === 'string';\n\t};\n\t\n\t// Is a string a number surrounded by HTML?\n\tvar _htmlNumeric = function ( d, decimalPoint, formatted, allowEmpty ) {\n\t\tif ( allowEmpty && _empty( d ) ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\t// input and select strings mean that this isn't just a number\n\t\tif (typeof d === 'string' && d.match(/<(input|select)/i)) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\tvar html = _isHtml( d );\n\t\treturn ! html ?\n\t\t\tnull :\n\t\t\t_isNumber( _stripHtml( d ), decimalPoint, formatted, allowEmpty ) ?\n\t\t\t\ttrue :\n\t\t\t\tnull;\n\t};\n\t\n\t\n\tvar _pluck = function ( a, prop, prop2 ) {\n\t\tvar out = [];\n\t\tvar i=0, iLen=a.length;\n\t\n\t\t// Could have the test in the loop for slightly smaller code, but speed\n\t\t// is essential here\n\t\tif ( prop2 !== undefined ) {\n\t\t\tfor ( ; i<iLen ; i++ ) {\n\t\t\t\tif ( a[i] && a[i][ prop ] ) {\n\t\t\t\t\tout.push( a[i][ prop ][ prop2 ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tfor ( ; i<iLen ; i++ ) {\n\t\t\t\tif ( a[i] ) {\n\t\t\t\t\tout.push( a[i][ prop ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\t// Basically the same as _pluck, but rather than looping over `a` we use `order`\n\t// as the indexes to pick from `a`\n\tvar _pluck_order = function ( a, order, prop, prop2 )\n\t{\n\t\tvar out = [];\n\t\tvar i=0, iLen=order.length;\n\t\n\t\t// Could have the test in the loop for slightly smaller code, but speed\n\t\t// is essential here\n\t\tif ( prop2 !== undefined ) {\n\t\t\tfor ( ; i<iLen ; i++ ) {\n\t\t\t\tif ( a[ order[i] ] && a[ order[i] ][ prop ] ) {\n\t\t\t\t\tout.push( a[ order[i] ][ prop ][ prop2 ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tfor ( ; i<iLen ; i++ ) {\n\t\t\t\tif ( a[ order[i] ] ) {\n\t\t\t\t\tout.push( a[ order[i] ][ prop ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\tvar _range = function ( len, start )\n\t{\n\t\tvar out = [];\n\t\tvar end;\n\t\n\t\tif ( start === undefined ) {\n\t\t\tstart = 0;\n\t\t\tend = len;\n\t\t}\n\t\telse {\n\t\t\tend = start;\n\t\t\tstart = len;\n\t\t}\n\t\n\t\tfor ( var i=start ; i<end ; i++ ) {\n\t\t\tout.push( i );\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t\n\tvar _removeEmpty = function ( a )\n\t{\n\t\tvar out = [];\n\t\n\t\tfor ( var i=0, iLen=a.length ; i<iLen ; i++ ) {\n\t\t\tif ( a[i] ) { // careful - will remove all falsy values!\n\t\t\t\tout.push( a[i] );\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t// Replaceable function in api.util\n\tvar _stripHtml = function (input, replacement) {\n\t\tif (! input || typeof input !== 'string') {\n\t\t\treturn input;\n\t\t}\n\t\n\t\t// Irrelevant check to workaround CodeQL's false positive on the regex\n\t\tif (input.length > _max_str_len) {\n\t\t\tthrow new Error('Exceeded max str len');\n\t\t}\n\t\n\t\tvar previous;\n\t\n\t\tinput = input.replace(_re_html, replacement || ''); // Complete tags\n\t\n\t\t// Safety for incomplete script tag - use do / while to ensure that\n\t\t// we get all instances\n\t\tdo {\n\t\t\tprevious = input;\n\t\t\tinput = input.replace(/<script/i, '');\n\t\t} while (input !== previous);\n\t\n\t\treturn previous;\n\t};\n\t\n\t// Replaceable function in api.util\n\tvar _escapeHtml = function ( d ) {\n\t\tif (Array.isArray(d)) {\n\t\t\td = d.join(',');\n\t\t}\n\t\n\t\treturn typeof d === 'string' ?\n\t\t\td\n\t\t\t\t.replace(/&/g, '&amp;')\n\t\t\t\t.replace(/</g, '&lt;')\n\t\t\t\t.replace(/>/g, '&gt;')\n\t\t\t\t.replace(/\"/g, '&quot;') :\n\t\t\td;\n\t};\n\t\n\t// Remove diacritics from a string by decomposing it and then removing\n\t// non-ascii characters\n\tvar _normalize = function (str, both) {\n\t\tif (typeof str !== 'string') {\n\t\t\treturn str;\n\t\t}\n\t\n\t\t// It is faster to just run `normalize` than it is to check if\n\t\t// we need to with a regex! (Check as it isn't available in old\n\t\t// Safari)\n\t\tvar res = str.normalize\n\t\t\t? str.normalize(\"NFD\")\n\t\t\t: str;\n\t\n\t\t// Equally, here we check if a regex is needed or not\n\t\treturn res.length !== str.length\n\t\t\t? (both === true ? str + ' ' : '' ) + res.replace(/[\\u0300-\\u036f]/g, \"\")\n\t\t\t: res;\n\t}\n\t\n\t/**\n\t * Determine if all values in the array are unique. This means we can short\n\t * cut the _unique method at the cost of a single loop. A sorted array is used\n\t * to easily check the values.\n\t *\n\t * @param  {array} src Source array\n\t * @return {boolean} true if all unique, false otherwise\n\t * @ignore\n\t */\n\tvar _areAllUnique = function ( src ) {\n\t\tif ( src.length < 2 ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\tvar sorted = src.slice().sort();\n\t\tvar last = sorted[0];\n\t\n\t\tfor ( var i=1, iLen=sorted.length ; i<iLen ; i++ ) {\n\t\t\tif ( sorted[i] === last ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\n\t\t\tlast = sorted[i];\n\t\t}\n\t\n\t\treturn true;\n\t};\n\t\n\t\n\t/**\n\t * Find the unique elements in a source array.\n\t *\n\t * @param  {array} src Source array\n\t * @return {array} Array of unique items\n\t * @ignore\n\t */\n\tvar _unique = function ( src )\n\t{\n\t\tif (Array.from && Set) {\n\t\t\treturn Array.from(new Set(src));\n\t\t}\n\t\n\t\tif ( _areAllUnique( src ) ) {\n\t\t\treturn src.slice();\n\t\t}\n\t\n\t\t// A faster unique method is to use object keys to identify used values,\n\t\t// but this doesn't work with arrays or objects, which we must also\n\t\t// consider. See jsperf.app/compare-array-unique-versions/4 for more\n\t\t// information.\n\t\tvar\n\t\t\tout = [],\n\t\t\tval,\n\t\t\ti, iLen=src.length,\n\t\t\tj, k=0;\n\t\n\t\tagain: for ( i=0 ; i<iLen ; i++ ) {\n\t\t\tval = src[i];\n\t\n\t\t\tfor ( j=0 ; j<k ; j++ ) {\n\t\t\t\tif ( out[j] === val ) {\n\t\t\t\t\tcontinue again;\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tout.push( val );\n\t\t\tk++;\n\t\t}\n\t\n\t\treturn out;\n\t};\n\t\n\t// Surprisingly this is faster than [].concat.apply\n\t// https://jsperf.com/flatten-an-array-loop-vs-reduce/2\n\tvar _flatten = function (out, val) {\n\t\tif (Array.isArray(val)) {\n\t\t\tfor (var i=0 ; i<val.length ; i++) {\n\t\t\t\t_flatten(out, val[i]);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tout.push(val);\n\t\t}\n\t\n\t\treturn out;\n\t}\n\t\n\t// Similar to jQuery's addClass, but use classList.add\n\tfunction _addClass(el, name) {\n\t\tif (name) {\n\t\t\tname.split(' ').forEach(function (n) {\n\t\t\t\tif (n) {\n\t\t\t\t\t// `add` does deduplication, so no need to check `contains`\n\t\t\t\t\tel.classList.add(n);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\t\n\t/**\n\t * DataTables utility methods\n\t * \n\t * This namespace provides helper methods that DataTables uses internally to\n\t * create a DataTable, but which are not exclusively used only for DataTables.\n\t * These methods can be used by extension authors to save the duplication of\n\t * code.\n\t *\n\t *  @namespace\n\t */\n\tDataTable.util = {\n\t\t/**\n\t\t * Return a string with diacritic characters decomposed\n\t\t * @param {*} mixed Function or string to normalize\n\t\t * @param {*} both Return original string and the normalized string\n\t\t * @returns String or undefined\n\t\t */\n\t\tdiacritics: function (mixed, both) {\n\t\t\tvar type = typeof mixed;\n\t\n\t\t\tif (type !== 'function') {\n\t\t\t\treturn _normalize(mixed, both);\n\t\t\t}\n\t\t\t_normalize = mixed;\n\t\t},\n\t\n\t\t/**\n\t\t * Debounce a function\n\t\t *\n\t\t * @param {function} fn Function to be called\n\t\t * @param {integer} freq Call frequency in mS\n\t\t * @return {function} Wrapped function\n\t\t */\n\t\tdebounce: function ( fn, timeout ) {\n\t\t\tvar timer;\n\t\n\t\t\treturn function () {\n\t\t\t\tvar that = this;\n\t\t\t\tvar args = arguments;\n\t\n\t\t\t\tclearTimeout(timer);\n\t\n\t\t\t\ttimer = setTimeout( function () {\n\t\t\t\t\tfn.apply(that, args);\n\t\t\t\t}, timeout || 250 );\n\t\t\t};\n\t\t},\n\t\n\t\t/**\n\t\t * Throttle the calls to a function. Arguments and context are maintained\n\t\t * for the throttled function.\n\t\t *\n\t\t * @param {function} fn Function to be called\n\t\t * @param {integer} freq Call frequency in mS\n\t\t * @return {function} Wrapped function\n\t\t */\n\t\tthrottle: function ( fn, freq ) {\n\t\t\tvar\n\t\t\t\tfrequency = freq !== undefined ? freq : 200,\n\t\t\t\tlast,\n\t\t\t\ttimer;\n\t\n\t\t\treturn function () {\n\t\t\t\tvar\n\t\t\t\t\tthat = this,\n\t\t\t\t\tnow  = +new Date(),\n\t\t\t\t\targs = arguments;\n\t\n\t\t\t\tif ( last && now < last + frequency ) {\n\t\t\t\t\tclearTimeout( timer );\n\t\n\t\t\t\t\ttimer = setTimeout( function () {\n\t\t\t\t\t\tlast = undefined;\n\t\t\t\t\t\tfn.apply( that, args );\n\t\t\t\t\t}, frequency );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tlast = now;\n\t\t\t\t\tfn.apply( that, args );\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\n\t\t/**\n\t\t * Escape a string such that it can be used in a regular expression\n\t\t *\n\t\t *  @param {string} val string to escape\n\t\t *  @returns {string} escaped string\n\t\t */\n\t\tescapeRegex: function ( val ) {\n\t\t\treturn val.replace( _re_escape_regex, '\\\\$1' );\n\t\t},\n\t\n\t\t/**\n\t\t * Create a function that will write to a nested object or array\n\t\t * @param {*} source JSON notation string\n\t\t * @returns Write function\n\t\t */\n\t\tset: function ( source ) {\n\t\t\tif ( $.isPlainObject( source ) ) {\n\t\t\t\t/* Unlike get, only the underscore (global) option is used for for\n\t\t\t\t * setting data since we don't know the type here. This is why an object\n\t\t\t\t * option is not documented for `mData` (which is read/write), but it is\n\t\t\t\t * for `mRender` which is read only.\n\t\t\t\t */\n\t\t\t\treturn DataTable.util.set( source._ );\n\t\t\t}\n\t\t\telse if ( source === null ) {\n\t\t\t\t// Nothing to do when the data source is null\n\t\t\t\treturn function () {};\n\t\t\t}\n\t\t\telse if ( typeof source === 'function' ) {\n\t\t\t\treturn function (data, val, meta) {\n\t\t\t\t\tsource( data, 'set', val, meta );\n\t\t\t\t};\n\t\t\t}\n\t\t\telse if (\n\t\t\t\ttypeof source === 'string' && (source.indexOf('.') !== -1 ||\n\t\t\t\tsource.indexOf('[') !== -1 || source.indexOf('(') !== -1)\n\t\t\t) {\n\t\t\t\t// Like the get, we need to get data from a nested object\n\t\t\t\tvar setData = function (data, val, src) {\n\t\t\t\t\tvar a = _fnSplitObjNotation( src ), b;\n\t\t\t\t\tvar aLast = a[a.length-1];\n\t\t\t\t\tvar arrayNotation, funcNotation, o, innerSrc;\n\t\t\n\t\t\t\t\tfor ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) {\n\t\t\t\t\t\t// Protect against prototype pollution\n\t\t\t\t\t\tif (a[i] === '__proto__' || a[i] === 'constructor') {\n\t\t\t\t\t\t\tthrow new Error('Cannot set prototype values');\n\t\t\t\t\t\t}\n\t\t\n\t\t\t\t\t\t// Check if we are dealing with an array notation request\n\t\t\t\t\t\tarrayNotation = a[i].match(__reArray);\n\t\t\t\t\t\tfuncNotation = a[i].match(__reFn);\n\t\t\n\t\t\t\t\t\tif ( arrayNotation ) {\n\t\t\t\t\t\t\ta[i] = a[i].replace(__reArray, '');\n\t\t\t\t\t\t\tdata[ a[i] ] = [];\n\t\t\n\t\t\t\t\t\t\t// Get the remainder of the nested object to set so we can recurse\n\t\t\t\t\t\t\tb = a.slice();\n\t\t\t\t\t\t\tb.splice( 0, i+1 );\n\t\t\t\t\t\t\tinnerSrc = b.join('.');\n\t\t\n\t\t\t\t\t\t\t// Traverse each entry in the array setting the properties requested\n\t\t\t\t\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\t\t\t\t\tfor ( var j=0, jLen=val.length ; j<jLen ; j++ ) {\n\t\t\t\t\t\t\t\t\to = {};\n\t\t\t\t\t\t\t\t\tsetData( o, val[j], innerSrc );\n\t\t\t\t\t\t\t\t\tdata[ a[i] ].push( o );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t// We've been asked to save data to an array, but it\n\t\t\t\t\t\t\t\t// isn't array data to be saved. Best that can be done\n\t\t\t\t\t\t\t\t// is to just save the value.\n\t\t\t\t\t\t\t\tdata[ a[i] ] = val;\n\t\t\t\t\t\t\t}\n\t\t\n\t\t\t\t\t\t\t// The inner call to setData has already traversed through the remainder\n\t\t\t\t\t\t\t// of the source and has set the data, thus we can exit here\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if ( funcNotation ) {\n\t\t\t\t\t\t\t// Function call\n\t\t\t\t\t\t\ta[i] = a[i].replace(__reFn, '');\n\t\t\t\t\t\t\tdata = data[ a[i] ]( val );\n\t\t\t\t\t\t}\n\t\t\n\t\t\t\t\t\t// If the nested object doesn't currently exist - since we are\n\t\t\t\t\t\t// trying to set the value - create it\n\t\t\t\t\t\tif ( data[ a[i] ] === null || data[ a[i] ] === undefined ) {\n\t\t\t\t\t\t\tdata[ a[i] ] = {};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t\t}\n\t\t\n\t\t\t\t\t// Last item in the input - i.e, the actual set\n\t\t\t\t\tif ( aLast.match(__reFn ) ) {\n\t\t\t\t\t\t// Function call\n\t\t\t\t\t\tdata = data[ aLast.replace(__reFn, '') ]( val );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// If array notation is used, we just want to strip it and use the property name\n\t\t\t\t\t\t// and assign the value. If it isn't used, then we get the result we want anyway\n\t\t\t\t\t\tdata[ aLast.replace(__reArray, '') ] = val;\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\n\t\t\t\treturn function (data, val) { // meta is also passed in, but not used\n\t\t\t\t\treturn setData( data, val, source );\n\t\t\t\t};\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Array or flat object mapping\n\t\t\t\treturn function (data, val) { // meta is also passed in, but not used\n\t\t\t\t\tdata[source] = val;\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\n\t\t/**\n\t\t * Create a function that will read nested objects from arrays, based on JSON notation\n\t\t * @param {*} source JSON notation string\n\t\t * @returns Value read\n\t\t */\n\t\tget: function ( source ) {\n\t\t\tif ( $.isPlainObject( source ) ) {\n\t\t\t\t// Build an object of get functions, and wrap them in a single call\n\t\t\t\tvar o = {};\n\t\t\t\t$.each( source, function (key, val) {\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\to[key] = DataTable.util.get( val );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\n\t\t\t\treturn function (data, type, row, meta) {\n\t\t\t\t\tvar t = o[type] || o._;\n\t\t\t\t\treturn t !== undefined ?\n\t\t\t\t\t\tt(data, type, row, meta) :\n\t\t\t\t\t\tdata;\n\t\t\t\t};\n\t\t\t}\n\t\t\telse if ( source === null ) {\n\t\t\t\t// Give an empty string for rendering / sorting etc\n\t\t\t\treturn function (data) { // type, row and meta also passed, but not used\n\t\t\t\t\treturn data;\n\t\t\t\t};\n\t\t\t}\n\t\t\telse if ( typeof source === 'function' ) {\n\t\t\t\treturn function (data, type, row, meta) {\n\t\t\t\t\treturn source( data, type, row, meta );\n\t\t\t\t};\n\t\t\t}\n\t\t\telse if (\n\t\t\t\ttypeof source === 'string' && (source.indexOf('.') !== -1 ||\n\t\t\t\tsource.indexOf('[') !== -1 || source.indexOf('(') !== -1)\n\t\t\t) {\n\t\t\t\t/* If there is a . in the source string then the data source is in a\n\t\t\t\t * nested object so we loop over the data for each level to get the next\n\t\t\t\t * level down. On each loop we test for undefined, and if found immediately\n\t\t\t\t * return. This allows entire objects to be missing and sDefaultContent to\n\t\t\t\t * be used if defined, rather than throwing an error\n\t\t\t\t */\n\t\t\t\tvar fetchData = function (data, type, src) {\n\t\t\t\t\tvar arrayNotation, funcNotation, out, innerSrc;\n\t\t\n\t\t\t\t\tif ( src !== \"\" ) {\n\t\t\t\t\t\tvar a = _fnSplitObjNotation( src );\n\t\t\n\t\t\t\t\t\tfor ( var i=0, iLen=a.length ; i<iLen ; i++ ) {\n\t\t\t\t\t\t\t// Check if we are dealing with special notation\n\t\t\t\t\t\t\tarrayNotation = a[i].match(__reArray);\n\t\t\t\t\t\t\tfuncNotation = a[i].match(__reFn);\n\t\t\n\t\t\t\t\t\t\tif ( arrayNotation ) {\n\t\t\t\t\t\t\t\t// Array notation\n\t\t\t\t\t\t\t\ta[i] = a[i].replace(__reArray, '');\n\t\t\n\t\t\t\t\t\t\t\t// Condition allows simply [] to be passed in\n\t\t\t\t\t\t\t\tif ( a[i] !== \"\" ) {\n\t\t\t\t\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tout = [];\n\t\t\n\t\t\t\t\t\t\t\t// Get the remainder of the nested object to get\n\t\t\t\t\t\t\t\ta.splice( 0, i+1 );\n\t\t\t\t\t\t\t\tinnerSrc = a.join('.');\n\t\t\n\t\t\t\t\t\t\t\t// Traverse each entry in the array getting the properties requested\n\t\t\t\t\t\t\t\tif ( Array.isArray( data ) ) {\n\t\t\t\t\t\t\t\t\tfor ( var j=0, jLen=data.length ; j<jLen ; j++ ) {\n\t\t\t\t\t\t\t\t\t\tout.push( fetchData( data[j], type, innerSrc ) );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\n\t\t\t\t\t\t\t\t// If a string is given in between the array notation indicators, that\n\t\t\t\t\t\t\t\t// is used to join the strings together, otherwise an array is returned\n\t\t\t\t\t\t\t\tvar join = arrayNotation[0].substring(1, arrayNotation[0].length-1);\n\t\t\t\t\t\t\t\tdata = (join===\"\") ? out : out.join(join);\n\t\t\n\t\t\t\t\t\t\t\t// The inner call to fetchData has already traversed through the remainder\n\t\t\t\t\t\t\t\t// of the source requested, so we exit from the loop\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if ( funcNotation ) {\n\t\t\t\t\t\t\t\t// Function call\n\t\t\t\t\t\t\t\ta[i] = a[i].replace(__reFn, '');\n\t\t\t\t\t\t\t\tdata = data[ a[i] ]();\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\n\t\t\t\t\t\t\tif (data === null || data[ a[i] ] === null) {\n\t\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if ( data === undefined || data[ a[i] ] === undefined ) {\n\t\t\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\tdata = data[ a[i] ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\n\t\t\t\t\treturn data;\n\t\t\t\t};\n\t\t\n\t\t\t\treturn function (data, type) { // row and meta also passed, but not used\n\t\t\t\t\treturn fetchData( data, type, source );\n\t\t\t\t};\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Array or flat object mapping\n\t\t\t\treturn function (data) { // row and meta also passed, but not used\n\t\t\t\t\treturn data[source];\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\n\t\tstripHtml: function (mixed, replacement) {\n\t\t\tvar type = typeof mixed;\n\t\n\t\t\tif (type === 'function') {\n\t\t\t\t_stripHtml = mixed;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\telse if (type === 'string') {\n\t\t\t\treturn _stripHtml(mixed, replacement);\n\t\t\t}\n\t\t\treturn mixed;\n\t\t},\n\t\n\t\tescapeHtml: function (mixed) {\n\t\t\tvar type = typeof mixed;\n\t\n\t\t\tif (type === 'function') {\n\t\t\t\t_escapeHtml = mixed;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\telse if (type === 'string' || Array.isArray(mixed)) {\n\t\t\t\treturn _escapeHtml(mixed);\n\t\t\t}\n\t\t\treturn mixed;\n\t\t},\n\t\n\t\tunique: _unique\n\t};\n\t\n\t\n\t\n\t/**\n\t * Create a mapping object that allows camel case parameters to be looked up\n\t * for their Hungarian counterparts. The mapping is stored in a private\n\t * parameter called `_hungarianMap` which can be accessed on the source object.\n\t *  @param {object} o\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnHungarianMap ( o )\n\t{\n\t\tvar\n\t\t\thungarian = 'a aa ai ao as b fn i m o s ',\n\t\t\tmatch,\n\t\t\tnewKey,\n\t\t\tmap = {};\n\t\n\t\t$.each( o, function (key) {\n\t\t\tmatch = key.match(/^([^A-Z]+?)([A-Z])/);\n\t\n\t\t\tif ( match && hungarian.indexOf(match[1]+' ') !== -1 )\n\t\t\t{\n\t\t\t\tnewKey = key.replace( match[0], match[2].toLowerCase() );\n\t\t\t\tmap[ newKey ] = key;\n\t\n\t\t\t\tif ( match[1] === 'o' )\n\t\t\t\t{\n\t\t\t\t\t_fnHungarianMap( o[key] );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t\n\t\to._hungarianMap = map;\n\t}\n\t\n\t\n\t/**\n\t * Convert from camel case parameters to Hungarian, based on a Hungarian map\n\t * created by _fnHungarianMap.\n\t *  @param {object} src The model object which holds all parameters that can be\n\t *    mapped.\n\t *  @param {object} user The object to convert from camel case to Hungarian.\n\t *  @param {boolean} force When set to `true`, properties which already have a\n\t *    Hungarian value in the `user` object will be overwritten. Otherwise they\n\t *    won't be.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCamelToHungarian ( src, user, force )\n\t{\n\t\tif ( ! src._hungarianMap ) {\n\t\t\t_fnHungarianMap( src );\n\t\t}\n\t\n\t\tvar hungarianKey;\n\t\n\t\t$.each( user, function (key) {\n\t\t\thungarianKey = src._hungarianMap[ key ];\n\t\n\t\t\tif ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) )\n\t\t\t{\n\t\t\t\t// For objects, we need to buzz down into the object to copy parameters\n\t\t\t\tif ( hungarianKey.charAt(0) === 'o' )\n\t\t\t\t{\n\t\t\t\t\t// Copy the camelCase options over to the hungarian\n\t\t\t\t\tif ( ! user[ hungarianKey ] ) {\n\t\t\t\t\t\tuser[ hungarianKey ] = {};\n\t\t\t\t\t}\n\t\t\t\t\t$.extend( true, user[hungarianKey], user[key] );\n\t\n\t\t\t\t\t_fnCamelToHungarian( src[hungarianKey], user[hungarianKey], force );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tuser[hungarianKey] = user[ key ];\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t}\n\t\n\t/**\n\t * Map one parameter onto another\n\t *  @param {object} o Object to map\n\t *  @param {*} knew The new parameter name\n\t *  @param {*} old The old parameter name\n\t */\n\tvar _fnCompatMap = function ( o, knew, old ) {\n\t\tif ( o[ knew ] !== undefined ) {\n\t\t\to[ old ] = o[ knew ];\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * Provide backwards compatibility for the main DT options. Note that the new\n\t * options are mapped onto the old parameters, so this is an external interface\n\t * change only.\n\t *  @param {object} init Object to map\n\t */\n\tfunction _fnCompatOpts ( init )\n\t{\n\t\t_fnCompatMap( init, 'ordering',      'bSort' );\n\t\t_fnCompatMap( init, 'orderMulti',    'bSortMulti' );\n\t\t_fnCompatMap( init, 'orderClasses',  'bSortClasses' );\n\t\t_fnCompatMap( init, 'orderCellsTop', 'bSortCellsTop' );\n\t\t_fnCompatMap( init, 'order',         'aaSorting' );\n\t\t_fnCompatMap( init, 'orderFixed',    'aaSortingFixed' );\n\t\t_fnCompatMap( init, 'paging',        'bPaginate' );\n\t\t_fnCompatMap( init, 'pagingType',    'sPaginationType' );\n\t\t_fnCompatMap( init, 'pageLength',    'iDisplayLength' );\n\t\t_fnCompatMap( init, 'searching',     'bFilter' );\n\t\n\t\t// Boolean initialisation of x-scrolling\n\t\tif ( typeof init.sScrollX === 'boolean' ) {\n\t\t\tinit.sScrollX = init.sScrollX ? '100%' : '';\n\t\t}\n\t\tif ( typeof init.scrollX === 'boolean' ) {\n\t\t\tinit.scrollX = init.scrollX ? '100%' : '';\n\t\t}\n\t\n\t\t// Objects for ordering\n\t\tif ( typeof init.bSort === 'object' ) {\n\t\t\tinit.orderIndicators = init.bSort.indicators !== undefined ? init.bSort.indicators : true;\n\t\t\tinit.orderHandler = init.bSort.handler !== undefined ? init.bSort.handler : true;\n\t\t\tinit.bSort = true;\n\t\t}\n\t\telse if (init.bSort === false) {\n\t\t\tinit.orderIndicators = false;\n\t\t\tinit.orderHandler = false;\n\t\t}\n\t\telse if (init.bSort === true) {\n\t\t\tinit.orderIndicators = true;\n\t\t\tinit.orderHandler = true;\n\t\t}\n\t\n\t\t// Which cells are the title cells?\n\t\tif (typeof init.bSortCellsTop === 'boolean') {\n\t\t\tinit.titleRow = init.bSortCellsTop;\n\t\t}\n\t\n\t\t// Column search objects are in an array, so it needs to be converted\n\t\t// element by element\n\t\tvar searchCols = init.aoSearchCols;\n\t\n\t\tif ( searchCols ) {\n\t\t\tfor ( var i=0, iLen=searchCols.length ; i<iLen ; i++ ) {\n\t\t\t\tif ( searchCols[i] ) {\n\t\t\t\t\t_fnCamelToHungarian( DataTable.models.oSearch, searchCols[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Enable search delay if server-side processing is enabled\n\t\tif (init.serverSide && ! init.searchDelay) {\n\t\t\tinit.searchDelay = 400;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Provide backwards compatibility for column options. Note that the new options\n\t * are mapped onto the old parameters, so this is an external interface change\n\t * only.\n\t *  @param {object} init Object to map\n\t */\n\tfunction _fnCompatCols ( init )\n\t{\n\t\t_fnCompatMap( init, 'orderable',     'bSortable' );\n\t\t_fnCompatMap( init, 'orderData',     'aDataSort' );\n\t\t_fnCompatMap( init, 'orderSequence', 'asSorting' );\n\t\t_fnCompatMap( init, 'orderDataType', 'sortDataType' );\n\t\n\t\t// orderData can be given as an integer\n\t\tvar dataSort = init.aDataSort;\n\t\tif ( typeof dataSort === 'number' && ! Array.isArray( dataSort ) ) {\n\t\t\tinit.aDataSort = [ dataSort ];\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Browser feature detection for capabilities, quirks\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBrowserDetect( settings )\n\t{\n\t\t// We don't need to do this every time DataTables is constructed, the values\n\t\t// calculated are specific to the browser and OS configuration which we\n\t\t// don't expect to change between initialisations\n\t\tif ( ! DataTable.__browser ) {\n\t\t\tvar browser = {};\n\t\t\tDataTable.__browser = browser;\n\t\n\t\t\t// Scrolling feature / quirks detection\n\t\t\tvar n = $('<div/>')\n\t\t\t\t.css( {\n\t\t\t\t\tposition: 'fixed',\n\t\t\t\t\ttop: 0,\n\t\t\t\t\tleft: -1 * window.pageXOffset, // allow for scrolling\n\t\t\t\t\theight: 1,\n\t\t\t\t\twidth: 1,\n\t\t\t\t\toverflow: 'hidden'\n\t\t\t\t} )\n\t\t\t\t.append(\n\t\t\t\t\t$('<div/>')\n\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\t\t\ttop: 1,\n\t\t\t\t\t\t\tleft: 1,\n\t\t\t\t\t\t\twidth: 100,\n\t\t\t\t\t\t\toverflow: 'scroll'\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t$('<div/>')\n\t\t\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t\t\t\theight: 10\n\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\t.appendTo( 'body' );\n\t\n\t\t\tvar outer = n.children();\n\t\t\tvar inner = outer.children();\n\t\n\t\t\t// Get scrollbar width\n\t\t\tbrowser.barWidth = outer[0].offsetWidth - outer[0].clientWidth;\n\t\n\t\t\t// In rtl text layout, some browsers (most, but not all) will place the\n\t\t\t// scrollbar on the left, rather than the right.\n\t\t\tbrowser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1;\n\t\n\t\t\tn.remove();\n\t\t}\n\t\n\t\t$.extend( settings.oBrowser, DataTable.__browser );\n\t\tsettings.oScroll.iBarWidth = DataTable.__browser.barWidth;\n\t}\n\t\n\t/**\n\t * Add a column to the list used for the table with default values\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddColumn( oSettings )\n\t{\n\t\t// Add column to aoColumns array\n\t\tvar oDefaults = DataTable.defaults.column;\n\t\tvar iCol = oSettings.aoColumns.length;\n\t\tvar oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {\n\t\t\t\"aDataSort\": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],\n\t\t\t\"mData\": oDefaults.mData ? oDefaults.mData : iCol,\n\t\t\tidx: iCol,\n\t\t\tsearchFixed: {},\n\t\t\tcolEl: $('<col>').attr('data-dt-column', iCol)\n\t\t} );\n\t\toSettings.aoColumns.push( oCol );\n\t\n\t\t// Add search object for column specific search. Note that the `searchCols[ iCol ]`\n\t\t// passed into extend can be undefined. This allows the user to give a default\n\t\t// with only some of the parameters defined, and also not give a default\n\t\tvar searchCols = oSettings.aoPreSearchCols;\n\t\tsearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] );\n\t}\n\t\n\t\n\t/**\n\t * Apply options for a column\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iCol column index to consider\n\t *  @param {object} oOptions object with sType, bVisible and bSearchable etc\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnOptions( oSettings, iCol, oOptions )\n\t{\n\t\tvar oCol = oSettings.aoColumns[ iCol ];\n\t\n\t\t/* User specified column options */\n\t\tif ( oOptions !== undefined && oOptions !== null )\n\t\t{\n\t\t\t// Backwards compatibility\n\t\t\t_fnCompatCols( oOptions );\n\t\n\t\t\t// Map camel case parameters to their Hungarian counterparts\n\t\t\t_fnCamelToHungarian( DataTable.defaults.column, oOptions, true );\n\t\n\t\t\t/* Backwards compatibility for mDataProp */\n\t\t\tif ( oOptions.mDataProp !== undefined && !oOptions.mData )\n\t\t\t{\n\t\t\t\toOptions.mData = oOptions.mDataProp;\n\t\t\t}\n\t\n\t\t\tif ( oOptions.sType )\n\t\t\t{\n\t\t\t\toCol._sManualType = oOptions.sType;\n\t\t\t}\n\t\t\n\t\t\t// `class` is a reserved word in JavaScript, so we need to provide\n\t\t\t// the ability to use a valid name for the camel case input\n\t\t\tif ( oOptions.className && ! oOptions.sClass )\n\t\t\t{\n\t\t\t\toOptions.sClass = oOptions.className;\n\t\t\t}\n\t\n\t\t\tvar origClass = oCol.sClass;\n\t\n\t\t\t$.extend( oCol, oOptions );\n\t\t\t_fnMap( oCol, oOptions, \"sWidth\", \"sWidthOrig\" );\n\t\n\t\t\t// Merge class from previously defined classes with this one, rather than just\n\t\t\t// overwriting it in the extend above\n\t\t\tif (origClass !== oCol.sClass) {\n\t\t\t\toCol.sClass = origClass + ' ' + oCol.sClass;\n\t\t\t}\n\t\n\t\t\t/* iDataSort to be applied (backwards compatibility), but aDataSort will take\n\t\t\t * priority if defined\n\t\t\t */\n\t\t\tif ( oOptions.iDataSort !== undefined )\n\t\t\t{\n\t\t\t\toCol.aDataSort = [ oOptions.iDataSort ];\n\t\t\t}\n\t\t\t_fnMap( oCol, oOptions, \"aDataSort\" );\n\t\t}\n\t\n\t\t/* Cache the data get and set functions for speed */\n\t\tvar mDataSrc = oCol.mData;\n\t\tvar mData = _fnGetObjectDataFn( mDataSrc );\n\t\n\t\t// The `render` option can be given as an array to access the helper rendering methods.\n\t\t// The first element is the rendering method to use, the rest are the parameters to pass\n\t\tif ( oCol.mRender && Array.isArray( oCol.mRender ) ) {\n\t\t\tvar copy = oCol.mRender.slice();\n\t\t\tvar name = copy.shift();\n\t\n\t\t\toCol.mRender = DataTable.render[name].apply(window, copy);\n\t\t}\n\t\n\t\toCol._render = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;\n\t\n\t\tvar attrTest = function( src ) {\n\t\t\treturn typeof src === 'string' && src.indexOf('@') !== -1;\n\t\t};\n\t\toCol._bAttrSrc = $.isPlainObject( mDataSrc ) && (\n\t\t\tattrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter)\n\t\t);\n\t\toCol._setter = null;\n\t\n\t\toCol.fnGetData = function (rowData, type, meta) {\n\t\t\tvar innerData = mData( rowData, type, undefined, meta );\n\t\n\t\t\treturn oCol._render && type ?\n\t\t\t\toCol._render( innerData, type, rowData, meta ) :\n\t\t\t\tinnerData;\n\t\t};\n\t\toCol.fnSetData = function ( rowData, val, meta ) {\n\t\t\treturn _fnSetObjectDataFn( mDataSrc )( rowData, val, meta );\n\t\t};\n\t\n\t\t// Indicate if DataTables should read DOM data as an object or array\n\t\t// Used in _fnGetRowElements\n\t\tif ( typeof mDataSrc !== 'number' && ! oCol._isArrayHost ) {\n\t\t\toSettings._rowReadObject = true;\n\t\t}\n\t\n\t\t/* Feature sorting overrides column specific when off */\n\t\tif ( !oSettings.oFeatures.bSort )\n\t\t{\n\t\t\toCol.bSortable = false;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Adjust the table column widths for new data. Note: you would probably want to\n\t * do a redraw after calling this function!\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAdjustColumnSizing ( settings )\n\t{\n\t\t_fnCalculateColumnWidths( settings );\n\t\t_fnColumnSizes( settings );\n\t\n\t\tvar scroll = settings.oScroll;\n\t\tif ( scroll.sY !== '' || scroll.sX !== '') {\n\t\t\t_fnScrollDraw( settings );\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'column-sizing', [settings] );\n\t}\n\t\n\t/**\n\t * Apply column sizes\n\t *\n\t * @param {*} settings DataTables settings object\n\t */\n\tfunction _fnColumnSizes ( settings )\n\t{\n\t\tvar cols = settings.aoColumns;\n\t\n\t\tfor (var i=0 ; i<cols.length ; i++) {\n\t\t\tvar width = _fnColumnsSumWidth(settings, [i], false, false);\n\t\n\t\t\tcols[i].colEl.css('width', width);\n\t\n\t\t\tif (settings.oScroll.sX) {\n\t\t\t\tcols[i].colEl.css('min-width', width);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Convert the index of a visible column to the index in the data array (take account\n\t * of hidden columns)\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iMatch Visible column index to lookup\n\t *  @returns {int} i the data index\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnVisibleToColumnIndex( oSettings, iMatch )\n\t{\n\t\tvar aiVis = _fnGetColumns( oSettings, 'bVisible' );\n\t\n\t\treturn typeof aiVis[iMatch] === 'number' ?\n\t\t\taiVis[iMatch] :\n\t\t\tnull;\n\t}\n\t\n\t\n\t/**\n\t * Convert the index of an index in the data array and convert it to the visible\n\t *   column index (take account of hidden columns)\n\t *  @param {int} iMatch Column index to lookup\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {int} i the data index\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnIndexToVisible( oSettings, iMatch )\n\t{\n\t\tvar aiVis = _fnGetColumns( oSettings, 'bVisible' );\n\t\tvar iPos = aiVis.indexOf(iMatch);\n\t\n\t\treturn iPos !== -1 ? iPos : null;\n\t}\n\t\n\t\n\t/**\n\t * Get the number of visible columns\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {int} i the number of visible columns\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnVisibleColumns( settings )\n\t{\n\t\tvar layout = settings.aoHeader;\n\t\tvar columns = settings.aoColumns;\n\t\tvar vis = 0;\n\t\n\t\tif ( layout.length ) {\n\t\t\tfor ( var i=0, iLen=layout[0].length ; i<iLen ; i++ ) {\n\t\t\t\tif ( columns[i].bVisible && $(layout[0][i].cell).css('display') !== 'none' ) {\n\t\t\t\t\tvis++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn vis;\n\t}\n\t\n\t\n\t/**\n\t * Get an array of column indexes that match a given property\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {string} sParam Parameter in aoColumns to look for - typically\n\t *    bVisible or bSearchable\n\t *  @returns {array} Array of indexes with matched properties\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetColumns( oSettings, sParam )\n\t{\n\t\tvar a = [];\n\t\n\t\toSettings.aoColumns.map( function(val, i) {\n\t\t\tif ( val[sParam] ) {\n\t\t\t\ta.push( i );\n\t\t\t}\n\t\t} );\n\t\n\t\treturn a;\n\t}\n\t\n\t/**\n\t * Allow the result from a type detection function to be `true` while\n\t * translating that into a string. Old type detection functions will\n\t * return the type name if it passes. An object store would be better,\n\t * but not backwards compatible.\n\t *\n\t * @param {*} typeDetect Object or function for type detection\n\t * @param {*} res Result from the type detection function\n\t * @returns Type name or false\n\t */\n\tfunction _typeResult (typeDetect, res) {\n\t\treturn res === true\n\t\t\t? typeDetect._name\n\t\t\t: res;\n\t}\n\t\n\t/**\n\t * Calculate the 'type' of a column\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnColumnTypes ( settings )\n\t{\n\t\tvar columns = settings.aoColumns;\n\t\tvar data = settings.aoData;\n\t\tvar types = DataTable.ext.type.detect;\n\t\tvar i, iLen, j, jen, k, ken;\n\t\tvar col, detectedType, cache;\n\t\n\t\t// For each column, spin over the data type detection functions, seeing if one matches\n\t\tfor ( i=0, iLen=columns.length ; i<iLen ; i++ ) {\n\t\t\tcol = columns[i];\n\t\t\tcache = [];\n\t\n\t\t\tif ( ! col.sType && col._sManualType ) {\n\t\t\t\tcol.sType = col._sManualType;\n\t\t\t}\n\t\t\telse if ( ! col.sType ) {\n\t\t\t\t// With SSP type detection can be unreliable and error prone, so we provide a way\n\t\t\t\t// to turn it off.\n\t\t\t\tif (! settings.typeDetect) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tfor ( j=0, jen=types.length ; j<jen ; j++ ) {\n\t\t\t\t\tvar typeDetect = types[j];\n\t\n\t\t\t\t\t// There can be either one, or three type detection functions\n\t\t\t\t\tvar oneOf = typeDetect.oneOf;\n\t\t\t\t\tvar allOf = typeDetect.allOf || typeDetect;\n\t\t\t\t\tvar init = typeDetect.init;\n\t\t\t\t\tvar one = false;\n\t\n\t\t\t\t\tdetectedType = null;\n\t\n\t\t\t\t\t// Fast detect based on column assignment\n\t\t\t\t\tif (init) {\n\t\t\t\t\t\tdetectedType = _typeResult(typeDetect, init(settings, col, i));\n\t\n\t\t\t\t\t\tif (detectedType) {\n\t\t\t\t\t\t\tcol.sType = detectedType;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\tfor ( k=0, ken=data.length ; k<ken ; k++ ) {\n\t\t\t\t\t\tif (! data[k]) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Use a cache array so we only need to get the type data\n\t\t\t\t\t\t// from the formatter once (when using multiple detectors)\n\t\t\t\t\t\tif ( cache[k] === undefined ) {\n\t\t\t\t\t\t\tcache[k] = _fnGetCellData( settings, k, i, 'type' );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Only one data point in the column needs to match this function\n\t\t\t\t\t\tif (oneOf && ! one) {\n\t\t\t\t\t\t\tone = _typeResult(typeDetect, oneOf( cache[k], settings ));\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// All data points need to match this function\n\t\t\t\t\t\tdetectedType = _typeResult(typeDetect, allOf( cache[k], settings ));\n\t\n\t\t\t\t\t\t// If null, then this type can't apply to this column, so\n\t\t\t\t\t\t// rather than testing all cells, break out. There is an\n\t\t\t\t\t\t// exception for the last type which is `html`. We need to\n\t\t\t\t\t\t// scan all rows since it is possible to mix string and HTML\n\t\t\t\t\t\t// types\n\t\t\t\t\t\tif ( ! detectedType && j !== types.length-3 ) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Only a single match is needed for html type since it is\n\t\t\t\t\t\t// bottom of the pile and very similar to string - but it\n\t\t\t\t\t\t// must not be empty\n\t\t\t\t\t\tif ( detectedType === 'html' && ! _empty(cache[k]) ) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Type is valid for all data points in the column - use this\n\t\t\t\t\t// type\n\t\t\t\t\tif ( (oneOf && one && detectedType) || (!oneOf && detectedType) ) {\n\t\t\t\t\t\tcol.sType = detectedType;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t// Fall back - if no type was detected, always use string\n\t\t\t\tif ( ! col.sType ) {\n\t\t\t\t\tcol.sType = 'string';\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Set class names for header / footer for auto type classes\n\t\t\tvar autoClass = _ext.type.className[col.sType];\n\t\n\t\t\tif (autoClass) {\n\t\t\t\t_columnAutoClass(settings.aoHeader, i, autoClass);\n\t\t\t\t_columnAutoClass(settings.aoFooter, i, autoClass);\n\t\t\t}\n\t\n\t\t\tvar renderer = _ext.type.render[col.sType];\n\t\n\t\t\t// This can only happen once! There is no way to remove\n\t\t\t// a renderer. After the first time the renderer has\n\t\t\t// already been set so createTr will run the renderer itself.\n\t\t\tif (renderer && ! col._render) {\n\t\t\t\tcol._render = DataTable.util.get(renderer);\n\t\n\t\t\t\t_columnAutoRender(settings, i);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Apply an auto detected renderer to data which doesn't yet have\n\t * a renderer\n\t */\n\tfunction _columnAutoRender(settings, colIdx) {\n\t\tvar data = settings.aoData;\n\t\n\t\tfor (var i=0 ; i<data.length ; i++) {\n\t\t\tif (data[i].nTr) {\n\t\t\t\t// We have to update the display here since there is no\n\t\t\t\t// invalidation check for the data\n\t\t\t\tvar display = _fnGetCellData( settings, i, colIdx, 'display' );\n\t\n\t\t\t\tdata[i].displayData[colIdx] = display;\n\t\t\t\t_fnWriteCell(data[i].anCells[colIdx], display);\n\t\n\t\t\t\t// No need to update sort / filter data since it has\n\t\t\t\t// been invalidated and will be re-read with the\n\t\t\t\t// renderer now applied\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Apply a class name to a column's header cells\n\t */\n\tfunction _columnAutoClass(container, colIdx, className) {\n\t\tcontainer.forEach(function (row) {\n\t\t\tif (row[colIdx] && row[colIdx].unique) {\n\t\t\t\t_addClass(row[colIdx].cell, className);\n\t\t\t}\n\t\t});\n\t}\n\t\n\t/**\n\t * Take the column definitions and static columns arrays and calculate how\n\t * they relate to column indexes. The callback function will then apply the\n\t * definition found for a column to a suitable configuration object.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {array} aoColDefs The aoColumnDefs array that is to be applied\n\t *  @param {array} aoCols The aoColumns array that defines columns individually\n\t *  @param {array} headerLayout Layout for header as it was loaded\n\t *  @param {function} fn Callback function - takes two parameters, the calculated\n\t *    column index and the definition for that column.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, headerLayout, fn )\n\t{\n\t\tvar i, iLen, j, jLen, k, kLen, def;\n\t\tvar columns = oSettings.aoColumns;\n\t\n\t\tif ( aoCols ) {\n\t\t\tfor ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) {\n\t\t\t\tif (aoCols[i] && aoCols[i].name) {\n\t\t\t\t\tcolumns[i].sName = aoCols[i].name;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Column definitions with aTargets\n\t\tif ( aoColDefs )\n\t\t{\n\t\t\t/* Loop over the definitions array - loop in reverse so first instance has priority */\n\t\t\tfor ( i=aoColDefs.length-1 ; i>=0 ; i-- )\n\t\t\t{\n\t\t\t\tdef = aoColDefs[i];\n\t\n\t\t\t\t/* Each definition can target multiple columns, as it is an array */\n\t\t\t\tvar aTargets = def.target !== undefined\n\t\t\t\t\t? def.target\n\t\t\t\t\t: def.targets !== undefined\n\t\t\t\t\t\t? def.targets\n\t\t\t\t\t\t: def.aTargets;\n\t\n\t\t\t\tif ( ! Array.isArray( aTargets ) )\n\t\t\t\t{\n\t\t\t\t\taTargets = [ aTargets ];\n\t\t\t\t}\n\t\n\t\t\t\tfor ( j=0, jLen=aTargets.length ; j<jLen ; j++ )\n\t\t\t\t{\n\t\t\t\t\tvar target = aTargets[j];\n\t\n\t\t\t\t\tif ( typeof target === 'number' && target >= 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Add columns that we don't yet know about */\n\t\t\t\t\t\twhile( columns.length <= target )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_fnAddColumn( oSettings );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t/* Integer, basic index */\n\t\t\t\t\t\tfn( target, def );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( typeof target === 'number' && target < 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t/* Negative integer, right to left column counting */\n\t\t\t\t\t\tfn( columns.length+target, def );\n\t\t\t\t\t}\n\t\t\t\t\telse if ( typeof target === 'string' )\n\t\t\t\t\t{\n\t\t\t\t\t\tfor ( k=0, kLen=columns.length ; k<kLen ; k++ ) {\n\t\t\t\t\t\t\tif (target === '_all') {\n\t\t\t\t\t\t\t\t// Apply to all columns\n\t\t\t\t\t\t\t\tfn( k, def );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (target.indexOf(':name') !== -1) {\n\t\t\t\t\t\t\t\t// Column selector\n\t\t\t\t\t\t\t\tif (columns[k].sName === target.replace(':name', '')) {\n\t\t\t\t\t\t\t\t\tfn( k, def );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t// Cell selector\n\t\t\t\t\t\t\t\theaderLayout.forEach(function (row) {\n\t\t\t\t\t\t\t\t\tif (row[k]) {\n\t\t\t\t\t\t\t\t\t\tvar cell = $(row[k].cell);\n\t\n\t\t\t\t\t\t\t\t\t\t// Legacy support. Note that it means that we don't support\n\t\t\t\t\t\t\t\t\t\t// an element name selector only, since they are treated as\n\t\t\t\t\t\t\t\t\t\t// class names for 1.x compat.\n\t\t\t\t\t\t\t\t\t\tif (target.match(/^[a-z][\\w-]*$/i)) {\n\t\t\t\t\t\t\t\t\t\t\ttarget = '.' + target;\n\t\t\t\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t\t\t\tif (cell.is( target )) {\n\t\t\t\t\t\t\t\t\t\t\tfn( k, def );\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Statically defined columns array\n\t\tif ( aoCols ) {\n\t\t\tfor ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) {\n\t\t\t\tfn( i, aoCols[i] );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Get the width for a given set of columns\n\t *\n\t * @param {*} settings DataTables settings object\n\t * @param {*} targets Columns - comma separated string or array of numbers\n\t * @param {*} original Use the original width (true) or calculated (false)\n\t * @param {*} incVisible Include visible columns (true) or not (false)\n\t * @returns Combined CSS value\n\t */\n\tfunction _fnColumnsSumWidth( settings, targets, original, incVisible ) {\n\t\tif ( ! Array.isArray( targets ) ) {\n\t\t\ttargets = _fnColumnsFromHeader( targets );\n\t\t}\n\t\n\t\tvar sum = 0;\n\t\tvar unit;\n\t\tvar columns = settings.aoColumns;\n\t\t\n\t\tfor ( var i=0, iLen=targets.length ; i<iLen ; i++ ) {\n\t\t\tvar column = columns[ targets[i] ];\n\t\t\tvar definedWidth = original ?\n\t\t\t\tcolumn.sWidthOrig :\n\t\t\t\tcolumn.sWidth;\n\t\n\t\t\tif ( ! incVisible && column.bVisible === false ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\tif ( definedWidth === null || definedWidth === undefined ) {\n\t\t\t\treturn null; // can't determine a defined width - browser defined\n\t\t\t}\n\t\t\telse if ( typeof definedWidth === 'number' ) {\n\t\t\t\tunit = 'px';\n\t\t\t\tsum += definedWidth;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tvar matched = definedWidth.match(/([\\d\\.]+)([^\\d]*)/);\n\t\n\t\t\t\tif ( matched ) {\n\t\t\t\t\tsum += matched[1] * 1;\n\t\t\t\t\tunit = matched.length === 3 ?\n\t\t\t\t\t\tmatched[2] :\n\t\t\t\t\t\t'px';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn sum + unit;\n\t}\n\t\n\tfunction _fnColumnsFromHeader( cell )\n\t{\n\t\tvar attr = $(cell).closest('[data-dt-column]').attr('data-dt-column');\n\t\n\t\tif ( ! attr ) {\n\t\t\treturn [];\n\t\t}\n\t\n\t\treturn attr.split(',').map( function (val) {\n\t\t\treturn val * 1;\n\t\t} );\n\t}\n\t/**\n\t * Add a data array to the table, creating DOM node etc. This is the parallel to\n\t * _fnGatherData, but for adding rows from a JavaScript source, rather than a\n\t * DOM source.\n\t *  @param {object} settings dataTables settings object\n\t *  @param {array} data data array to be added\n\t *  @param {node} [tr] TR element to add to the table - optional. If not given,\n\t *    DataTables will create a row automatically\n\t *  @param {array} [tds] Array of TD|TH elements for the row - must be given\n\t *    if nTr is.\n\t *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddData ( settings, dataIn, tr, tds )\n\t{\n\t\t/* Create the object for storing information about this new row */\n\t\tvar rowIdx = settings.aoData.length;\n\t\tvar rowModel = $.extend( true, {}, DataTable.models.oRow, {\n\t\t\tsrc: tr ? 'dom' : 'data',\n\t\t\tidx: rowIdx\n\t\t} );\n\t\n\t\trowModel._aData = dataIn;\n\t\tsettings.aoData.push( rowModel );\n\t\n\t\tvar columns = settings.aoColumns;\n\t\n\t\tfor ( var i=0, iLen=columns.length ; i<iLen ; i++ )\n\t\t{\n\t\t\t// Invalidate the column types as the new data needs to be revalidated\n\t\t\tcolumns[i].sType = null;\n\t\t}\n\t\n\t\t/* Add to the display array */\n\t\tsettings.aiDisplayMaster.push( rowIdx );\n\t\n\t\tvar id = settings.rowIdFn( dataIn );\n\t\tif ( id !== undefined ) {\n\t\t\tsettings.aIds[ id ] = rowModel;\n\t\t}\n\t\n\t\t/* Create the DOM information, or register it if already present */\n\t\tif ( tr || ! settings.oFeatures.bDeferRender )\n\t\t{\n\t\t\t_fnCreateTr( settings, rowIdx, tr, tds );\n\t\t}\n\t\n\t\treturn rowIdx;\n\t}\n\t\n\t\n\t/**\n\t * Add one or more TR elements to the table. Generally we'd expect to\n\t * use this for reading data from a DOM sourced table, but it could be\n\t * used for an TR element. Note that if a TR is given, it is used (i.e.\n\t * it is not cloned).\n\t *  @param {object} settings dataTables settings object\n\t *  @param {array|node|jQuery} trs The TR element(s) to add to the table\n\t *  @returns {array} Array of indexes for the added rows\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddTr( settings, trs )\n\t{\n\t\tvar row;\n\t\n\t\t// Allow an individual node to be passed in\n\t\tif ( ! (trs instanceof $) ) {\n\t\t\ttrs = $(trs);\n\t\t}\n\t\n\t\treturn trs.map( function (i, el) {\n\t\t\trow = _fnGetRowElements( settings, el );\n\t\t\treturn _fnAddData( settings, row.data, el, row.cells );\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Get the data for a given cell from the internal cache, taking into account data mapping\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} rowIdx aoData row id\n\t *  @param {int} colIdx Column index\n\t *  @param {string} type data get type ('display', 'type' 'filter|search' 'sort|order')\n\t *  @returns {*} Cell data\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetCellData( settings, rowIdx, colIdx, type )\n\t{\n\t\tif (type === 'search') {\n\t\t\ttype = 'filter';\n\t\t}\n\t\telse if (type === 'order') {\n\t\t\ttype = 'sort';\n\t\t}\n\t\n\t\tvar row = settings.aoData[rowIdx];\n\t\n\t\tif (! row) {\n\t\t\treturn undefined;\n\t\t}\n\t\n\t\tvar draw           = settings.iDraw;\n\t\tvar col            = settings.aoColumns[colIdx];\n\t\tvar rowData        = row._aData;\n\t\tvar defaultContent = col.sDefaultContent;\n\t\tvar cellData       = col.fnGetData( rowData, type, {\n\t\t\tsettings: settings,\n\t\t\trow:      rowIdx,\n\t\t\tcol:      colIdx\n\t\t} );\n\t\n\t\t// Allow for a node being returned for non-display types\n\t\tif (type !== 'display' && cellData && typeof cellData === 'object' && cellData.nodeName) {\n\t\t\tcellData = cellData.innerHTML;\n\t\t}\n\t\n\t\tif ( cellData === undefined ) {\n\t\t\tif ( settings.iDrawError != draw && defaultContent === null ) {\n\t\t\t\t_fnLog( settings, 0, \"Requested unknown parameter \"+\n\t\t\t\t\t(typeof col.mData=='function' ? '{function}' : \"'\"+col.mData+\"'\")+\n\t\t\t\t\t\" for row \"+rowIdx+\", column \"+colIdx, 4 );\n\t\t\t\tsettings.iDrawError = draw;\n\t\t\t}\n\t\t\treturn defaultContent;\n\t\t}\n\t\n\t\t// When the data source is null and a specific data type is requested (i.e.\n\t\t// not the original data), we can use default column data\n\t\tif ( (cellData === rowData || cellData === null) && defaultContent !== null && type !== undefined ) {\n\t\t\tcellData = defaultContent;\n\t\t}\n\t\telse if ( typeof cellData === 'function' ) {\n\t\t\t// If the data source is a function, then we run it and use the return,\n\t\t\t// executing in the scope of the data object (for instances)\n\t\t\treturn cellData.call( rowData );\n\t\t}\n\t\n\t\tif ( cellData === null && type === 'display' ) {\n\t\t\treturn '';\n\t\t}\n\t\n\t\tif ( type === 'filter' ) {\n\t\t\tvar formatters = DataTable.ext.type.search;\n\t\n\t\t\tif ( formatters[ col.sType ] ) {\n\t\t\t\tcellData = formatters[ col.sType ]( cellData );\n\t\t\t}\n\t\t}\n\t\n\t\treturn cellData;\n\t}\n\t\n\t\n\t/**\n\t * Set the value for a specific cell, into the internal data cache\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} rowIdx aoData row id\n\t *  @param {int} colIdx Column index\n\t *  @param {*} val Value to set\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSetCellData( settings, rowIdx, colIdx, val )\n\t{\n\t\tvar col     = settings.aoColumns[colIdx];\n\t\tvar rowData = settings.aoData[rowIdx]._aData;\n\t\n\t\tcol.fnSetData( rowData, val, {\n\t\t\tsettings: settings,\n\t\t\trow:      rowIdx,\n\t\t\tcol:      colIdx\n\t\t}  );\n\t}\n\t\n\t/**\n\t * Write a value to a cell\n\t * @param {*} td Cell\n\t * @param {*} val Value\n\t */\n\tfunction _fnWriteCell(td, val)\n\t{\n\t\tif (val && typeof val === 'object' && val.nodeName) {\n\t\t\t$(td)\n\t\t\t\t.empty()\n\t\t\t\t.append(val);\n\t\t}\n\t\telse {\n\t\t\ttd.innerHTML = val;\n\t\t}\n\t}\n\t\n\t\n\t// Private variable that is used to match action syntax in the data property object\n\tvar __reArray = /\\[.*?\\]$/;\n\tvar __reFn = /\\(\\)$/;\n\t\n\t/**\n\t * Split string on periods, taking into account escaped periods\n\t * @param  {string} str String to split\n\t * @return {array} Split string\n\t */\n\tfunction _fnSplitObjNotation( str )\n\t{\n\t\tvar parts = str.match(/(\\\\.|[^.])+/g) || [''];\n\t\n\t\treturn parts.map( function ( s ) {\n\t\t\treturn s.replace(/\\\\\\./g, '.');\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Return a function that can be used to get data from a source object, taking\n\t * into account the ability to use nested objects as a source\n\t *  @param {string|int|function} mSource The data source for the object\n\t *  @returns {function} Data get function\n\t *  @memberof DataTable#oApi\n\t */\n\tvar _fnGetObjectDataFn = DataTable.util.get;\n\t\n\t\n\t/**\n\t * Return a function that can be used to set data from a source object, taking\n\t * into account the ability to use nested objects as a source\n\t *  @param {string|int|function} mSource The data source for the object\n\t *  @returns {function} Data set function\n\t *  @memberof DataTable#oApi\n\t */\n\tvar _fnSetObjectDataFn = DataTable.util.set;\n\t\n\t\n\t/**\n\t * Return an array with the full table data\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns array {array} aData Master data array\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetDataMaster ( settings )\n\t{\n\t\treturn _pluck( settings.aoData, '_aData' );\n\t}\n\t\n\t\n\t/**\n\t * Nuke the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnClearTable( settings )\n\t{\n\t\tsettings.aoData.length = 0;\n\t\tsettings.aiDisplayMaster.length = 0;\n\t\tsettings.aiDisplay.length = 0;\n\t\tsettings.aIds = {};\n\t}\n\t\n\t\n\t/**\n\t * Mark cached data as invalid such that a re-read of the data will occur when\n\t * the cached data is next requested. Also update from the data source object.\n\t *\n\t * @param {object} settings DataTables settings object\n\t * @param {int}    rowIdx   Row index to invalidate\n\t * @param {string} [src]    Source to invalidate from: undefined, 'auto', 'dom'\n\t *     or 'data'\n\t * @param {int}    [colIdx] Column index to invalidate. If undefined the whole\n\t *     row will be invalidated\n\t * @memberof DataTable#oApi\n\t *\n\t * @todo For the modularisation of v1.11 this will need to become a callback, so\n\t *   the sort and filter methods can subscribe to it. That will required\n\t *   initialisation options for sorting, which is why it is not already baked in\n\t */\n\tfunction _fnInvalidate( settings, rowIdx, src, colIdx )\n\t{\n\t\tvar row = settings.aoData[ rowIdx ];\n\t\tvar i, iLen;\n\t\n\t\t// Remove the cached data for the row\n\t\trow._aSortData = null;\n\t\trow._aFilterData = null;\n\t\trow.displayData = null;\n\t\n\t\t// Are we reading last data from DOM or the data object?\n\t\tif ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) {\n\t\t\t// Read the data from the DOM\n\t\t\trow._aData = _fnGetRowElements(\n\t\t\t\t\tsettings, row, colIdx, colIdx === undefined ? undefined : row._aData\n\t\t\t\t)\n\t\t\t\t.data;\n\t\t}\n\t\telse {\n\t\t\t// Reading from data object, update the DOM\n\t\t\tvar cells = row.anCells;\n\t\t\tvar display = _fnGetRowDisplay(settings, rowIdx);\n\t\n\t\t\tif ( cells ) {\n\t\t\t\tif ( colIdx !== undefined ) {\n\t\t\t\t\t_fnWriteCell(cells[colIdx], display[colIdx]);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tfor ( i=0, iLen=cells.length ; i<iLen ; i++ ) {\n\t\t\t\t\t\t_fnWriteCell(cells[i], display[i]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Column specific invalidation\n\t\tvar cols = settings.aoColumns;\n\t\tif ( colIdx !== undefined ) {\n\t\t\t// Type - the data might have changed\n\t\t\tcols[ colIdx ].sType = null;\n\t\n\t\t\t// Max length string. Its a fairly cheep recalculation, so not worth\n\t\t\t// something more complicated\n\t\t\tcols[ colIdx ].wideStrings = null;\n\t\t}\n\t\telse {\n\t\t\tfor ( i=0, iLen=cols.length ; i<iLen ; i++ ) {\n\t\t\t\tcols[i].sType = null;\n\t\t\t\tcols[i].wideStrings = null;\n\t\t\t}\n\t\n\t\t\t// Update DataTables special `DT_*` attributes for the row\n\t\t\t_fnRowAttributes( settings, row );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Build a data source object from an HTML row, reading the contents of the\n\t * cells that are in the row.\n\t *\n\t * @param {object} settings DataTables settings object\n\t * @param {node|object} TR element from which to read data or existing row\n\t *   object from which to re-read the data from the cells\n\t * @param {int} [colIdx] Optional column index\n\t * @param {array|object} [d] Data source object. If `colIdx` is given then this\n\t *   parameter should also be given and will be used to write the data into.\n\t *   Only the column in question will be written\n\t * @returns {object} Object with two parameters: `data` the data read, in\n\t *   document order, and `cells` and array of nodes (they can be useful to the\n\t *   caller, so rather than needing a second traversal to get them, just return\n\t *   them from here).\n\t * @memberof DataTable#oApi\n\t */\n\tfunction _fnGetRowElements( settings, row, colIdx, d )\n\t{\n\t\tvar\n\t\t\ttds = [],\n\t\t\ttd = row.firstChild,\n\t\t\tname, col, i=0, contents,\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tobjectRead = settings._rowReadObject;\n\t\n\t\t// Allow the data object to be passed in, or construct\n\t\td = d !== undefined ?\n\t\t\td :\n\t\t\tobjectRead ?\n\t\t\t\t{} :\n\t\t\t\t[];\n\t\n\t\tvar attr = function ( str, td  ) {\n\t\t\tif ( typeof str === 'string' ) {\n\t\t\t\tvar idx = str.indexOf('@');\n\t\n\t\t\t\tif ( idx !== -1 ) {\n\t\t\t\t\tvar attr = str.substring( idx+1 );\n\t\t\t\t\tvar setter = _fnSetObjectDataFn( str );\n\t\t\t\t\tsetter( d, td.getAttribute( attr ) );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\n\t\t// Read data from a cell and store into the data object\n\t\tvar cellProcess = function ( cell ) {\n\t\t\tif ( colIdx === undefined || colIdx === i ) {\n\t\t\t\tcol = columns[i];\n\t\t\t\tcontents = (cell.innerHTML).trim();\n\t\n\t\t\t\tif ( col && col._bAttrSrc ) {\n\t\t\t\t\tvar setter = _fnSetObjectDataFn( col.mData._ );\n\t\t\t\t\tsetter( d, contents );\n\t\n\t\t\t\t\tattr( col.mData.sort, cell );\n\t\t\t\t\tattr( col.mData.type, cell );\n\t\t\t\t\tattr( col.mData.filter, cell );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Depending on the `data` option for the columns the data can\n\t\t\t\t\t// be read to either an object or an array.\n\t\t\t\t\tif ( objectRead ) {\n\t\t\t\t\t\tif ( ! col._setter ) {\n\t\t\t\t\t\t\t// Cache the setter function\n\t\t\t\t\t\t\tcol._setter = _fnSetObjectDataFn( col.mData );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcol._setter( d, contents );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\td[i] = contents;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\ti++;\n\t\t};\n\t\n\t\tif ( td ) {\n\t\t\t// `tr` element was passed in\n\t\t\twhile ( td ) {\n\t\t\t\tname = td.nodeName.toUpperCase();\n\t\n\t\t\t\tif ( name == \"TD\" || name == \"TH\" ) {\n\t\t\t\t\tcellProcess( td );\n\t\t\t\t\ttds.push( td );\n\t\t\t\t}\n\t\n\t\t\t\ttd = td.nextSibling;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// Existing row object passed in\n\t\t\ttds = row.anCells;\n\t\n\t\t\tfor ( var j=0, jen=tds.length ; j<jen ; j++ ) {\n\t\t\t\tcellProcess( tds[j] );\n\t\t\t}\n\t\t}\n\t\n\t\t// Read the ID from the DOM if present\n\t\tvar rowNode = row.firstChild ? row : row.nTr;\n\t\n\t\tif ( rowNode ) {\n\t\t\tvar id = rowNode.getAttribute( 'id' );\n\t\n\t\t\tif ( id ) {\n\t\t\t\t_fnSetObjectDataFn( settings.rowId )( d, id );\n\t\t\t}\n\t\t}\n\t\n\t\treturn {\n\t\t\tdata: d,\n\t\t\tcells: tds\n\t\t};\n\t}\n\t\n\t/**\n\t * Render and cache a row's display data for the columns, if required\n\t * @returns \n\t */\n\tfunction _fnGetRowDisplay (settings, rowIdx) {\n\t\tvar rowModal = settings.aoData[rowIdx];\n\t\tvar columns = settings.aoColumns;\n\t\n\t\tif (! rowModal.displayData) {\n\t\t\t// Need to render and cache\n\t\t\trowModal.displayData = [];\n\t\t\n\t\t\tfor ( var colIdx=0, len=columns.length ; colIdx<len ; colIdx++ ) {\n\t\t\t\trowModal.displayData.push(\n\t\t\t\t\t_fnGetCellData( settings, rowIdx, colIdx, 'display' )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\n\t\treturn rowModal.displayData;\n\t}\n\t\n\t/**\n\t * Create a new TR element (and it's TD children) for a row\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {int} iRow Row to consider\n\t *  @param {node} [nTrIn] TR element to add to the table - optional. If not given,\n\t *    DataTables will create a row automatically\n\t *  @param {array} [anTds] Array of TD|TH elements for the row - must be given\n\t *    if nTr is.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCreateTr ( oSettings, iRow, nTrIn, anTds )\n\t{\n\t\tvar\n\t\t\trow = oSettings.aoData[iRow],\n\t\t\trowData = row._aData,\n\t\t\tcells = [],\n\t\t\tnTr, nTd, oCol,\n\t\t\ti, iLen, create,\n\t\t\ttrClass = oSettings.oClasses.tbody.row;\n\t\n\t\tif ( row.nTr === null )\n\t\t{\n\t\t\tnTr = nTrIn || document.createElement('tr');\n\t\n\t\t\trow.nTr = nTr;\n\t\t\trow.anCells = cells;\n\t\n\t\t\t_addClass(nTr, trClass);\n\t\n\t\t\t/* Use a private property on the node to allow reserve mapping from the node\n\t\t\t * to the aoData array for fast look up\n\t\t\t */\n\t\t\tnTr._DT_RowIndex = iRow;\n\t\n\t\t\t/* Special parameters can be given by the data source to be used on the row */\n\t\t\t_fnRowAttributes( oSettings, row );\n\t\n\t\t\t/* Process each column */\n\t\t\tfor ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )\n\t\t\t{\n\t\t\t\toCol = oSettings.aoColumns[i];\n\t\t\t\tcreate = nTrIn && anTds[i] ? false : true;\n\t\n\t\t\t\tnTd = create ? document.createElement( oCol.sCellType ) : anTds[i];\n\t\n\t\t\t\tif (! nTd) {\n\t\t\t\t\t_fnLog( oSettings, 0, 'Incorrect column count', 18 );\n\t\t\t\t}\n\t\n\t\t\t\tnTd._DT_CellIndex = {\n\t\t\t\t\trow: iRow,\n\t\t\t\t\tcolumn: i\n\t\t\t\t};\n\t\t\t\t\n\t\t\t\tcells.push( nTd );\n\t\t\t\t\n\t\t\t\tvar display = _fnGetRowDisplay(oSettings, iRow);\n\t\n\t\t\t\t// Need to create the HTML if new, or if a rendering function is defined\n\t\t\t\tif (\n\t\t\t\t\tcreate ||\n\t\t\t\t\t(\n\t\t\t\t\t\t(oCol.mRender || oCol.mData !== i) &&\n\t\t\t\t\t\t(!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display')\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\t_fnWriteCell(nTd, display[i]);\n\t\t\t\t}\n\t\n\t\t\t\t// column class\n\t\t\t\t_addClass(nTd, oCol.sClass);\n\t\n\t\t\t\t// Visibility - add or remove as required\n\t\t\t\tif ( oCol.bVisible && create )\n\t\t\t\t{\n\t\t\t\t\tnTr.appendChild( nTd );\n\t\t\t\t}\n\t\t\t\telse if ( ! oCol.bVisible && ! create )\n\t\t\t\t{\n\t\t\t\t\tnTd.parentNode.removeChild( nTd );\n\t\t\t\t}\n\t\n\t\t\t\tif ( oCol.fnCreatedCell )\n\t\t\t\t{\n\t\t\t\t\toCol.fnCreatedCell.call( oSettings.oInstance,\n\t\t\t\t\t\tnTd, _fnGetCellData( oSettings, iRow, i ), rowData, iRow, i\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t_fnCallbackFire( oSettings, 'aoRowCreatedCallback', 'row-created', [nTr, rowData, iRow, cells] );\n\t\t}\n\t\telse {\n\t\t\t_addClass(row.nTr, trClass);\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Add attributes to a row based on the special `DT_*` parameters in a data\n\t * source object.\n\t *  @param {object} settings DataTables settings object\n\t *  @param {object} DataTables row object for the row to be modified\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnRowAttributes( settings, row )\n\t{\n\t\tvar tr = row.nTr;\n\t\tvar data = row._aData;\n\t\n\t\tif ( tr ) {\n\t\t\tvar id = settings.rowIdFn( data );\n\t\n\t\t\tif ( id ) {\n\t\t\t\ttr.id = id;\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowClass ) {\n\t\t\t\t// Remove any classes added by DT_RowClass before\n\t\t\t\tvar a = data.DT_RowClass.split(' ');\n\t\t\t\trow.__rowc = row.__rowc ?\n\t\t\t\t\t_unique( row.__rowc.concat( a ) ) :\n\t\t\t\t\ta;\n\t\n\t\t\t\t$(tr)\n\t\t\t\t\t.removeClass( row.__rowc.join(' ') )\n\t\t\t\t\t.addClass( data.DT_RowClass );\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowAttr ) {\n\t\t\t\t$(tr).attr( data.DT_RowAttr );\n\t\t\t}\n\t\n\t\t\tif ( data.DT_RowData ) {\n\t\t\t\t$(tr).data( data.DT_RowData );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Create the HTML header for the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBuildHead( settings, side )\n\t{\n\t\tvar classes = settings.oClasses;\n\t\tvar columns = settings.aoColumns;\n\t\tvar i, iLen, row;\n\t\tvar target = side === 'header'\n\t\t\t? settings.nTHead\n\t\t\t: settings.nTFoot;\n\t\tvar titleProp = side === 'header' ? 'sTitle' : side;\n\t\n\t\t// Footer might be defined\n\t\tif (! target) {\n\t\t\treturn;\n\t\t}\n\t\n\t\t// If no cells yet and we have content for them, then create\n\t\tif (side === 'header' || _pluck(settings.aoColumns, titleProp).join('')) {\n\t\t\trow = $('tr', target);\n\t\n\t\t\t// Add a row if needed\n\t\t\tif (! row.length) {\n\t\t\t\trow = $('<tr/>').appendTo(target)\n\t\t\t}\n\t\n\t\t\t// Add the number of cells needed to make up to the number of columns\n\t\t\tif (row.length === 1) {\n\t\t\t\tvar cellCount = 0;\n\t\t\t\t\n\t\t\t\t$('td, th', row).each(function () {\n\t\t\t\t\tcellCount += this.colSpan;\n\t\t\t\t});\n\t\n\t\t\t\tfor ( i=cellCount, iLen=columns.length ; i<iLen ; i++ ) {\n\t\t\t\t\t$('<th/>')\n\t\t\t\t\t\t.html( columns[i][titleProp] || '' )\n\t\t\t\t\t\t.appendTo( row );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\tvar detected = _fnDetectHeader( settings, target, true );\n\t\n\t\tif (side === 'header') {\n\t\t\tsettings.aoHeader = detected;\n\t\t\t$('tr', target).addClass(classes.thead.row);\n\t\t}\n\t\telse {\n\t\t\tsettings.aoFooter = detected;\n\t\t\t$('tr', target).addClass(classes.tfoot.row);\n\t\t}\n\t\n\t\t// Every cell needs to be passed through the renderer\n\t\t$(target).children('tr').children('th, td')\n\t\t\t.each( function () {\n\t\t\t\t_fnRenderer( settings, side )(\n\t\t\t\t\tsettings, $(this), classes\n\t\t\t\t);\n\t\t\t} );\n\t}\n\t\n\t/**\n\t * Build a layout structure for a header or footer\n\t *\n\t * @param {*} settings DataTables settings\n\t * @param {*} source Source layout array\n\t * @param {*} incColumns What columns should be included\n\t * @returns Layout array in column index order\n\t */\n\tfunction _fnHeaderLayout( settings, source, incColumns )\n\t{\n\t\tvar row, column, cell;\n\t\tvar local = [];\n\t\tvar structure = [];\n\t\tvar columns = settings.aoColumns;\n\t\tvar columnCount = columns.length;\n\t\tvar rowspan, colspan;\n\t\n\t\tif ( ! source ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Default is to work on only visible columns\n\t\tif ( ! incColumns ) {\n\t\t\tincColumns = _range(columnCount)\n\t\t\t\t.filter(function (idx) {\n\t\t\t\t\treturn columns[idx].bVisible;\n\t\t\t\t});\n\t\t}\n\t\n\t\t// Make a copy of the master layout array, but with only the columns we want\n\t\tfor ( row=0 ; row<source.length ; row++ ) {\n\t\t\t// Remove any columns we haven't selected\n\t\t\tlocal[row] = source[row].slice().filter(function (cell, i) {\n\t\t\t\treturn incColumns.includes(i);\n\t\t\t});\n\t\n\t\t\t// Prep the structure array - it needs an element for each row\n\t\t\tstructure.push( [] );\n\t\t}\n\t\n\t\tfor ( row=0 ; row<local.length ; row++ ) {\n\t\t\tfor ( column=0 ; column<local[row].length ; column++ ) {\n\t\t\t\trowspan = 1;\n\t\t\t\tcolspan = 1;\n\t\n\t\t\t\t// Check to see if there is already a cell (row/colspan) covering our target\n\t\t\t\t// insert point. If there is, then there is nothing to do.\n\t\t\t\tif ( structure[row][column] === undefined ) {\n\t\t\t\t\tcell = local[row][column].cell;\n\t\n\t\t\t\t\t// Expand for rowspan\n\t\t\t\t\twhile (\n\t\t\t\t\t\tlocal[row+rowspan] !== undefined &&\n\t\t\t\t\t\tlocal[row][column].cell == local[row+rowspan][column].cell\n\t\t\t\t\t) {\n\t\t\t\t\t\tstructure[row+rowspan][column] = null;\n\t\t\t\t\t\trowspan++;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// And for colspan\n\t\t\t\t\twhile (\n\t\t\t\t\t\tlocal[row][column+colspan] !== undefined &&\n\t\t\t\t\t\tlocal[row][column].cell == local[row][column+colspan].cell\n\t\t\t\t\t) {\n\t\t\t\t\t\t// Which also needs to go over rows\n\t\t\t\t\t\tfor ( var k=0 ; k<rowspan ; k++ ) {\n\t\t\t\t\t\t\tstructure[row+k][column+colspan] = null;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tcolspan++;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar titleSpan = $('.dt-column-title', cell);\n\t\n\t\t\t\t\tstructure[row][column] = {\n\t\t\t\t\t\tcell: cell,\n\t\t\t\t\t\tcolspan: colspan,\n\t\t\t\t\t\trowspan: rowspan,\n\t\t\t\t\t\ttitle: titleSpan.length\n\t\t\t\t\t\t\t? titleSpan.html()\n\t\t\t\t\t\t\t: $(cell).html()\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn structure;\n\t}\n\t\n\t\n\t/**\n\t * Draw the header (or footer) element based on the column visibility states.\n\t *\n\t *  @param object oSettings dataTables settings object\n\t *  @param array aoSource Layout array from _fnDetectHeader\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDrawHead( settings, source )\n\t{\n\t\tvar layout = _fnHeaderLayout(settings, source);\n\t\tvar tr, n;\n\t\n\t\tfor ( var row=0 ; row<source.length ; row++ ) {\n\t\t\ttr = source[row].row;\n\t\n\t\t\t// All cells are going to be replaced, so empty out the row\n\t\t\t// Can't use $().empty() as that kills event handlers\n\t\t\tif (tr) {\n\t\t\t\twhile( (n = tr.firstChild) ) {\n\t\t\t\t\ttr.removeChild( n );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tfor ( var column=0 ; column<layout[row].length ; column++ ) {\n\t\t\t\tvar point = layout[row][column];\n\t\n\t\t\t\tif (point) {\n\t\t\t\t\t$(point.cell)\n\t\t\t\t\t\t.appendTo(tr)\n\t\t\t\t\t\t.attr('rowspan', point.rowspan)\n\t\t\t\t\t\t.attr('colspan', point.colspan);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Insert the required TR nodes into the table for display\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param ajaxComplete true after ajax call to complete rendering\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDraw( oSettings, ajaxComplete )\n\t{\n\t\t// Allow for state saving and a custom start position\n\t\t_fnStart( oSettings );\n\t\n\t\t/* Provide a pre-callback function which can be used to cancel the draw is false is returned */\n\t\tvar aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );\n\t\tif ( aPreDraw.indexOf(false) !== -1 )\n\t\t{\n\t\t\t_fnProcessingDisplay( oSettings, false );\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar anRows = [];\n\t\tvar iRowCount = 0;\n\t\tvar bServerSide = _fnDataSource( oSettings ) == 'ssp';\n\t\tvar aiDisplay = oSettings.aiDisplay;\n\t\tvar iDisplayStart = oSettings._iDisplayStart;\n\t\tvar iDisplayEnd = oSettings.fnDisplayEnd();\n\t\tvar columns = oSettings.aoColumns;\n\t\tvar body = $(oSettings.nTBody);\n\t\n\t\toSettings.bDrawing = true;\n\t\n\t\t/* Server-side processing draw intercept */\n\t\tif ( oSettings.deferLoading )\n\t\t{\n\t\t\toSettings.deferLoading = false;\n\t\t\toSettings.iDraw++;\n\t\t\t_fnProcessingDisplay( oSettings, false );\n\t\t}\n\t\telse if ( !bServerSide )\n\t\t{\n\t\t\toSettings.iDraw++;\n\t\t}\n\t\telse if ( !oSettings.bDestroying && !ajaxComplete)\n\t\t{\n\t\t\t// Show loading message for server-side processing\n\t\t\tif (oSettings.iDraw === 0) {\n\t\t\t\tbody.empty().append(_emptyRow(oSettings));\n\t\t\t}\n\t\n\t\t\t_fnAjaxUpdate( oSettings );\n\t\t\treturn;\n\t\t}\n\t\n\t\tif ( aiDisplay.length !== 0 )\n\t\t{\n\t\t\tvar iStart = bServerSide ? 0 : iDisplayStart;\n\t\t\tvar iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd;\n\t\n\t\t\tfor ( var j=iStart ; j<iEnd ; j++ )\n\t\t\t{\n\t\t\t\tvar iDataIndex = aiDisplay[j];\n\t\t\t\tvar aoData = oSettings.aoData[ iDataIndex ];\n\t\n\t\t\t\t// Row has been deleted - can't be displayed\n\t\t\t\tif (aoData === null)\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\n\t\t\t\t// Row node hasn't been created yet\n\t\t\t\tif ( aoData.nTr === null )\n\t\t\t\t{\n\t\t\t\t\t_fnCreateTr( oSettings, iDataIndex );\n\t\t\t\t}\n\t\n\t\t\t\tvar nRow = aoData.nTr;\n\t\n\t\t\t\t// Add various classes as needed\n\t\t\t\tfor (var i=0 ; i<columns.length ; i++) {\n\t\t\t\t\tvar col = columns[i];\n\t\t\t\t\tvar td = aoData.anCells[i];\n\t\n\t\t\t\t\t_addClass(td, _ext.type.className[col.sType]); // auto class\n\t\t\t\t\t_addClass(td, oSettings.oClasses.tbody.cell); // all cells\n\t\t\t\t}\n\t\n\t\t\t\t// Row callback functions - might want to manipulate the row\n\t\t\t\t// iRowCount and j are not currently documented. Are they at all\n\t\t\t\t// useful?\n\t\t\t\t_fnCallbackFire( oSettings, 'aoRowCallback', null,\n\t\t\t\t\t[nRow, aoData._aData, iRowCount, j, iDataIndex] );\n\t\n\t\t\t\tanRows.push( nRow );\n\t\t\t\tiRowCount++;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tanRows[ 0 ] = _emptyRow(oSettings);\n\t\t}\n\t\n\t\t/* Header and footer callbacks */\n\t\t_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0],\n\t\t\t_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );\n\t\n\t\t_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0],\n\t\t\t_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );\n\t\n\t\t// replaceChildren is faster, but only became widespread in 2020,\n\t\t// so a fall back in jQuery is provided for older browsers.\n\t\tif (body[0].replaceChildren) {\n\t\t\tbody[0].replaceChildren.apply(body[0], anRows);\n\t\t}\n\t\telse {\n\t\t\tbody.children().detach();\n\t\t\tbody.append( $(anRows) );\n\t\t}\n\t\n\t\t// Empty table needs a specific class\n\t\t$(oSettings.nTableWrapper).toggleClass('dt-empty-footer', $('tr', oSettings.nTFoot).length === 0);\n\t\n\t\t/* Call all required callback functions for the end of a draw */\n\t\t_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings], true );\n\t\n\t\t/* Draw is complete, sorting and filtering must be as well */\n\t\toSettings.bSorted = false;\n\t\toSettings.bFiltered = false;\n\t\toSettings.bDrawing = false;\n\t}\n\t\n\t\n\t/**\n\t * Redraw the table - taking account of the various features which are enabled\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {boolean} [holdPosition] Keep the current paging position. By default\n\t *    the paging is reset to the first page\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnReDraw( settings, holdPosition, recompute )\n\t{\n\t\tvar\n\t\t\tfeatures = settings.oFeatures,\n\t\t\tsort     = features.bSort,\n\t\t\tfilter   = features.bFilter;\n\t\n\t\tif (recompute === undefined || recompute === true) {\n\t\t\t// Resolve any column types that are unknown due to addition or invalidation\n\t\t\t_fnColumnTypes( settings );\n\t\n\t\t\tif ( sort ) {\n\t\t\t\t_fnSort( settings );\n\t\t\t}\n\t\n\t\t\tif ( filter ) {\n\t\t\t\t_fnFilterComplete( settings, settings.oPreviousSearch );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// No filtering, so we want to just use the display master\n\t\t\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\t\t}\n\t\t}\n\t\n\t\tif ( holdPosition !== true ) {\n\t\t\tsettings._iDisplayStart = 0;\n\t\t}\n\t\n\t\t// Let any modules know about the draw hold position state (used by\n\t\t// scrolling internally)\n\t\tsettings._drawHold = holdPosition;\n\t\n\t\t_fnDraw( settings );\n\t\n\t\tsettings.api.one('draw', function () {\n\t\t\tsettings._drawHold = false;\n\t\t});\n\t}\n\t\n\t\n\t/*\n\t * Table is empty - create a row with an empty message in it\n\t */\n\tfunction _emptyRow ( settings ) {\n\t\tvar oLang = settings.oLanguage;\n\t\tvar zero = oLang.sZeroRecords;\n\t\tvar dataSrc = _fnDataSource( settings );\n\t\n\t\t// Make use of the fact that settings.json is only set once the initial data has\n\t\t// been loaded. Show loading when that isn't the case\n\t\tif ((dataSrc === 'ssp' || dataSrc === 'ajax') && ! settings.json) {\n\t\t\tzero = oLang.sLoadingRecords;\n\t\t}\n\t\telse if ( oLang.sEmptyTable && settings.fnRecordsTotal() === 0 )\n\t\t{\n\t\t\tzero = oLang.sEmptyTable;\n\t\t}\n\t\n\t\treturn $( '<tr/>' )\n\t\t\t.append( $('<td />', {\n\t\t\t\t'colSpan': _fnVisibleColumns( settings ),\n\t\t\t\t'class':   settings.oClasses.empty.row\n\t\t\t} ).html( zero ) )[0];\n\t}\n\t\n\t\n\t/**\n\t * Expand the layout items into an object for the rendering function\n\t */\n\tfunction _layoutItems (row, align, items) {\n\t\tif ( Array.isArray(items)) {\n\t\t\tfor (var i=0 ; i<items.length ; i++) {\n\t\t\t\t_layoutItems(row, align, items[i]);\n\t\t\t}\n\t\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar rowCell = row[align];\n\t\n\t\t// If it is an object, then there can be multiple features contained in it\n\t\tif ( $.isPlainObject( items ) ) {\n\t\t\t// A feature plugin cannot be named \"features\" due to this check\n\t\t\tif (items.features) {\n\t\t\t\tif (items.rowId) {\n\t\t\t\t\trow.id = items.rowId;\n\t\t\t\t}\n\t\t\t\tif (items.rowClass) {\n\t\t\t\t\trow.className = items.rowClass;\n\t\t\t\t}\n\t\n\t\t\t\trowCell.id = items.id;\n\t\t\t\trowCell.className = items.className;\n\t\n\t\t\t\t_layoutItems(row, align, items.features);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tObject.keys(items).map(function (key) {\n\t\t\t\t\trowCell.contents.push( {\n\t\t\t\t\t\tfeature: key,\n\t\t\t\t\t\topts: items[key]\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\trowCell.contents.push(items);\n\t\t}\n\t}\n\t\n\t/**\n\t * Find, or create a layout row\n\t */\n\tfunction _layoutGetRow(rows, rowNum, align) {\n\t\tvar row;\n\t\n\t\t// Find existing rows\n\t\tfor (var i=0; i<rows.length; i++) {\n\t\t\trow = rows[i];\n\t\n\t\t\tif (row.rowNum === rowNum) {\n\t\t\t\t// full is on its own, but start and end share a row\n\t\t\t\tif (\n\t\t\t\t\t(align === 'full' && row.full) ||\n\t\t\t\t\t((align === 'start' || align === 'end') && (row.start || row.end))\n\t\t\t\t) {\n\t\t\t\t\tif (! row[align]) {\n\t\t\t\t\t\trow[align] = {\n\t\t\t\t\t\t\tcontents: []\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\n\t\t\t\t\treturn row;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// If we get this far, then there was no match, create a new row\n\t\trow = {\n\t\t\trowNum: rowNum\t\n\t\t};\n\t\n\t\trow[align] = {\n\t\t\tcontents: []\n\t\t};\n\t\n\t\trows.push(row);\n\t\n\t\treturn row;\n\t}\n\t\n\t/**\n\t * Convert a `layout` object given by a user to the object structure needed\n\t * for the renderer. This is done twice, once for above and once for below\n\t * the table. Ordering must also be considered.\n\t *\n\t * @param {*} settings DataTables settings object\n\t * @param {*} layout Layout object to convert\n\t * @param {string} side `top` or `bottom`\n\t * @returns Converted array structure - one item for each row.\n\t */\n\tfunction _layoutArray ( settings, layout, side ) {\n\t\tvar rows = [];\n\t\t\n\t\t// Split out into an array\n\t\t$.each( layout, function ( pos, items ) {\n\t\t\tif (items === null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\tvar parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/);\n\t\t\tvar rowNum = parts[2]\n\t\t\t\t? parts[2] * 1\n\t\t\t\t: 0;\n\t\t\tvar align = parts[3]\n\t\t\t\t? parts[3].toLowerCase()\n\t\t\t\t: 'full';\n\t\n\t\t\t// Filter out the side we aren't interested in\n\t\t\tif (parts[1] !== side) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// Get or create the row we should attach to\n\t\t\tvar row = _layoutGetRow(rows, rowNum, align);\n\t\n\t\t\t_layoutItems(row, align, items);\n\t\t});\n\t\n\t\t// Order by item identifier\n\t\trows.sort( function ( a, b ) {\n\t\t\tvar order1 = a.rowNum;\n\t\t\tvar order2 = b.rowNum;\n\t\n\t\t\t// If both in the same row, then the row with `full` comes first\n\t\t\tif (order1 === order2) {\n\t\t\t\tvar ret = a.full && ! b.full ? -1 : 1;\n\t\n\t\t\t\treturn side === 'bottom'\n\t\t\t\t\t? ret * -1\n\t\t\t\t\t: ret;\n\t\t\t}\n\t\n\t\t\treturn order2 - order1;\n\t\t} );\n\t\n\t\t// Invert for below the table\n\t\tif ( side === 'bottom' ) {\n\t\t\trows.reverse();\n\t\t}\n\t\n\t\tfor (var row = 0; row<rows.length; row++) {\n\t\t\tdelete rows[row].rowNum;\n\t\n\t\t\t_layoutResolve(settings, rows[row]);\n\t\t}\n\t\n\t\treturn rows;\n\t}\n\t\n\t\n\t/**\n\t * Convert the contents of a row's layout object to nodes that can be inserted\n\t * into the document by a renderer. Execute functions, look up plug-ins, etc.\n\t *\n\t * @param {*} settings DataTables settings object\n\t * @param {*} row Layout object for this row\n\t */\n\tfunction _layoutResolve( settings, row ) {\n\t\tvar getFeature = function (feature, opts) {\n\t\t\tif ( ! _ext.features[ feature ] ) {\n\t\t\t\t_fnLog( settings, 0, 'Unknown feature: '+ feature );\n\t\t\t}\n\t\n\t\t\treturn _ext.features[ feature ].apply( this, [settings, opts] );\n\t\t};\n\t\n\t\tvar resolve = function ( item ) {\n\t\t\tif (! row[ item ]) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\tvar line = row[ item ].contents;\n\t\n\t\t\tfor ( var i=0, iLen=line.length ; i<iLen ; i++ ) {\n\t\t\t\tif ( ! line[i] ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\telse if ( typeof line[i] === 'string' ) {\n\t\t\t\t\tline[i] = getFeature( line[i], null );\n\t\t\t\t}\n\t\t\t\telse if ( $.isPlainObject(line[i]) ) {\n\t\t\t\t\t// If it's an object, it just has feature and opts properties from\n\t\t\t\t\t// the transform in _layoutArray\n\t\t\t\t\tline[i] = getFeature(line[i].feature, line[i].opts);\n\t\t\t\t}\n\t\t\t\telse if ( typeof line[i].node === 'function' ) {\n\t\t\t\t\tline[i] = line[i].node( settings );\n\t\t\t\t}\n\t\t\t\telse if ( typeof line[i] === 'function' ) {\n\t\t\t\t\tvar inst = line[i]( settings );\n\t\n\t\t\t\t\tline[i] = typeof inst.node === 'function' ?\n\t\t\t\t\t\tinst.node() :\n\t\t\t\t\t\tinst;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\n\t\tresolve('start');\n\t\tresolve('end');\n\t\tresolve('full');\n\t}\n\t\n\t\n\t/**\n\t * Add the options to the page HTML for the table\n\t *  @param {object} settings DataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAddOptionsHtml ( settings )\n\t{\n\t\tvar classes = settings.oClasses;\n\t\tvar table = $(settings.nTable);\n\t\n\t\t// Wrapper div around everything DataTables controls\n\t\tvar insert = $('<div/>')\n\t\t\t.attr({\n\t\t\t\tid:      settings.sTableId+'_wrapper',\n\t\t\t\t'class': classes.container\n\t\t\t})\n\t\t\t.insertBefore(table);\n\t\n\t\tsettings.nTableWrapper = insert[0];\n\t\n\t\tif (settings.sDom) {\n\t\t\t// Legacy\n\t\t\t_fnLayoutDom(settings, settings.sDom, insert);\n\t\t}\n\t\telse {\n\t\t\tvar top = _layoutArray( settings, settings.layout, 'top' );\n\t\t\tvar bottom = _layoutArray( settings, settings.layout, 'bottom' );\n\t\t\tvar renderer = _fnRenderer( settings, 'layout' );\n\t\t\n\t\t\t// Everything above - the renderer will actually insert the contents into the document\n\t\t\ttop.forEach(function (item) {\n\t\t\t\trenderer( settings, insert, item );\n\t\t\t});\n\t\n\t\t\t// The table - always the center of attention\n\t\t\trenderer( settings, insert, {\n\t\t\t\tfull: {\n\t\t\t\t\ttable: true,\n\t\t\t\t\tcontents: [ _fnFeatureHtmlTable(settings) ]\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\t// Everything below\n\t\t\tbottom.forEach(function (item) {\n\t\t\t\trenderer( settings, insert, item );\n\t\t\t});\n\t\t}\n\t\n\t\t// Processing floats on top, so it isn't an inserted feature\n\t\t_processingHtml( settings );\n\t}\n\t\n\t/**\n\t * Draw the table with the legacy DOM property\n\t * @param {*} settings DT settings object\n\t * @param {*} dom DOM string\n\t * @param {*} insert Insert point\n\t */\n\tfunction _fnLayoutDom( settings, dom, insert )\n\t{\n\t\tvar parts = dom.match(/(\".*?\")|('.*?')|./g);\n\t\tvar featureNode, option, newNode, next, attr;\n\t\n\t\tfor ( var i=0 ; i<parts.length ; i++ ) {\n\t\t\tfeatureNode = null;\n\t\t\toption = parts[i];\n\t\n\t\t\tif ( option == '<' ) {\n\t\t\t\t// New container div\n\t\t\t\tnewNode = $('<div/>');\n\t\n\t\t\t\t// Check to see if we should append an id and/or a class name to the container\n\t\t\t\tnext = parts[i+1];\n\t\n\t\t\t\tif ( next[0] == \"'\" || next[0] == '\"' ) {\n\t\t\t\t\tattr = next.replace(/['\"]/g, '');\n\t\n\t\t\t\t\tvar id = '', className;\n\t\n\t\t\t\t\t/* The attribute can be in the format of \"#id.class\", \"#id\" or \"class\" This logic\n\t\t\t\t\t * breaks the string into parts and applies them as needed\n\t\t\t\t\t */\n\t\t\t\t\tif ( attr.indexOf('.') != -1 ) {\n\t\t\t\t\t\tvar split = attr.split('.');\n\t\n\t\t\t\t\t\tid = split[0];\n\t\t\t\t\t\tclassName = split[1];\n\t\t\t\t\t}\n\t\t\t\t\telse if ( attr[0] == \"#\" ) {\n\t\t\t\t\t\tid = attr;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tclassName = attr;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tnewNode\n\t\t\t\t\t\t.attr('id', id.substring(1))\n\t\t\t\t\t\t.addClass(className);\n\t\n\t\t\t\t\ti++; // Move along the position array\n\t\t\t\t}\n\t\n\t\t\t\tinsert.append( newNode );\n\t\t\t\tinsert = newNode;\n\t\t\t}\n\t\t\telse if ( option == '>' ) {\n\t\t\t\t// End container div\n\t\t\t\tinsert = insert.parent();\n\t\t\t}\n\t\t\telse if ( option == 't' ) {\n\t\t\t\t// Table\n\t\t\t\tfeatureNode = _fnFeatureHtmlTable( settings );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tDataTable.ext.feature.forEach(function(feature) {\n\t\t\t\t\tif ( option == feature.cFeature ) {\n\t\t\t\t\t\tfeatureNode = feature.fnInit( settings );\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\n\t\t\t// Add to the display\n\t\t\tif ( featureNode ) {\n\t\t\t\tinsert.append( featureNode );\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Use the DOM source to create up an array of header cells. The idea here is to\n\t * create a layout grid (array) of rows x columns, which contains a reference\n\t * to the cell at that point in the grid (regardless of col/rowspan), such that\n\t * any column / row could be removed and the new grid constructed\n\t *  @param {node} thead The header/footer element for the table\n\t *  @returns {array} Calculated layout array\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDetectHeader ( settings, thead, write )\n\t{\n\t\tvar columns = settings.aoColumns;\n\t\tvar rows = $(thead).children('tr');\n\t\tvar row, cell;\n\t\tvar i, k, l, iLen, shifted, column, colspan, rowspan;\n\t\tvar titleRow = settings.titleRow;\n\t\tvar isHeader = thead && thead.nodeName.toLowerCase() === 'thead';\n\t\tvar layout = [];\n\t\tvar unique;\n\t\tvar shift = function ( a, i, j ) {\n\t\t\tvar k = a[i];\n\t\t\twhile ( k[j] ) {\n\t\t\t\tj++;\n\t\t\t}\n\t\t\treturn j;\n\t\t};\n\t\n\t\t// We know how many rows there are in the layout - so prep it\n\t\tfor ( i=0, iLen=rows.length ; i<iLen ; i++ ) {\n\t\t\tlayout.push( [] );\n\t\t}\n\t\n\t\tfor ( i=0, iLen=rows.length ; i<iLen ; i++ ) {\n\t\t\trow = rows[i];\n\t\t\tcolumn = 0;\n\t\n\t\t\t// For every cell in the row..\n\t\t\tcell = row.firstChild;\n\t\t\twhile ( cell ) {\n\t\t\t\tif (\n\t\t\t\t\tcell.nodeName.toUpperCase() == 'TD' ||\n\t\t\t\t\tcell.nodeName.toUpperCase() == 'TH'\n\t\t\t\t) {\n\t\t\t\t\tvar cols = [];\n\t\t\t\t\tvar jqCell = $(cell);\n\t\n\t\t\t\t\t// Get the col and rowspan attributes from the DOM and sanitise them\n\t\t\t\t\tcolspan = cell.getAttribute('colspan') * 1;\n\t\t\t\t\trowspan = cell.getAttribute('rowspan') * 1;\n\t\t\t\t\tcolspan = (!colspan || colspan===0 || colspan===1) ? 1 : colspan;\n\t\t\t\t\trowspan = (!rowspan || rowspan===0 || rowspan===1) ? 1 : rowspan;\n\t\n\t\t\t\t\t// There might be colspan cells already in this row, so shift our target\n\t\t\t\t\t// accordingly\n\t\t\t\t\tshifted = shift( layout, i, column );\n\t\n\t\t\t\t\t// Cache calculation for unique columns\n\t\t\t\t\tunique = colspan === 1 ?\n\t\t\t\t\t\ttrue :\n\t\t\t\t\t\tfalse;\n\t\t\t\t\t\n\t\t\t\t\t// Perform header setup\n\t\t\t\t\tif ( write ) {\n\t\t\t\t\t\tif (unique) {\n\t\t\t\t\t\t\t// Allow column options to be set from HTML attributes\n\t\t\t\t\t\t\t_fnColumnOptions( settings, shifted, _fnEscapeObject(jqCell.data()) );\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Get the width for the column. This can be defined from the\n\t\t\t\t\t\t\t// width attribute, style attribute or `columns.width` option\n\t\t\t\t\t\t\tvar columnDef = columns[shifted];\n\t\t\t\t\t\t\tvar width = cell.getAttribute('width') || null;\n\t\t\t\t\t\t\tvar t = cell.style.width.match(/width:\\s*(\\d+[pxem%]+)/);\n\t\t\t\t\t\t\tif ( t ) {\n\t\t\t\t\t\t\t\twidth = t[1];\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\tcolumnDef.sWidthOrig = columnDef.sWidth || width;\n\t\n\t\t\t\t\t\t\tif (isHeader) {\n\t\t\t\t\t\t\t\t// Column title handling - can be user set, or read from the DOM\n\t\t\t\t\t\t\t\t// This happens before the render, so the original is still in place\n\t\t\t\t\t\t\t\tif ( columnDef.sTitle !== null && ! columnDef.autoTitle ) {\n\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\t(titleRow === true && i === 0) || // top row\n\t\t\t\t\t\t\t\t\t\t(titleRow === false && i === rows.length -1) || // bottom row\n\t\t\t\t\t\t\t\t\t\t(titleRow === i) || // specific row\n\t\t\t\t\t\t\t\t\t\t(titleRow === null)\n\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\tcell.innerHTML = columnDef.sTitle;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t\tif (! columnDef.sTitle && unique) {\n\t\t\t\t\t\t\t\t\tcolumnDef.sTitle = _stripHtml(cell.innerHTML);\n\t\t\t\t\t\t\t\t\tcolumnDef.autoTitle = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t// Footer specific operations\n\t\t\t\t\t\t\t\tif (columnDef.footer) {\n\t\t\t\t\t\t\t\t\tcell.innerHTML = columnDef.footer;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t// Fall back to the aria-label attribute on the table header if no ariaTitle is\n\t\t\t\t\t\t\t// provided.\n\t\t\t\t\t\t\tif (! columnDef.ariaTitle) {\n\t\t\t\t\t\t\t\tcolumnDef.ariaTitle = jqCell.attr(\"aria-label\") || columnDef.sTitle;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t// Column specific class names\n\t\t\t\t\t\t\tif ( columnDef.className ) {\n\t\t\t\t\t\t\t\tjqCell.addClass( columnDef.className );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Wrap the column title so we can write to it in future\n\t\t\t\t\t\tif ( $('.dt-column-title', cell).length === 0) {\n\t\t\t\t\t\t\t$(document.createElement(settings.columnTitleTag))\n\t\t\t\t\t\t\t\t.addClass('dt-column-title')\n\t\t\t\t\t\t\t\t.append(cell.childNodes)\n\t\t\t\t\t\t\t\t.appendTo(cell);\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tsettings.orderIndicators &&\n\t\t\t\t\t\t\tisHeader &&\n\t\t\t\t\t\t\tjqCell.filter(':not([data-dt-order=disable])').length !== 0 &&\n\t\t\t\t\t\t\tjqCell.parent(':not([data-dt-order=disable])').length !== 0 &&\n\t\t\t\t\t\t\t$('.dt-column-order', cell).length === 0\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t$(document.createElement(settings.columnTitleTag))\n\t\t\t\t\t\t\t\t.addClass('dt-column-order')\n\t\t\t\t\t\t\t\t.appendTo(cell);\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// We need to wrap the elements in the header in another element to use flexbox\n\t\t\t\t\t\t// layout for those elements\n\t\t\t\t\t\tvar headerFooter = isHeader ? 'header' : 'footer';\n\t\n\t\t\t\t\t\tif ( $('div.dt-column-' + headerFooter, cell).length === 0) {\n\t\t\t\t\t\t\t$('<div>')\n\t\t\t\t\t\t\t\t.addClass('dt-column-' + headerFooter)\n\t\t\t\t\t\t\t\t.append(cell.childNodes)\n\t\t\t\t\t\t\t\t.appendTo(cell);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// If there is col / rowspan, copy the information into the layout grid\n\t\t\t\t\tfor ( l=0 ; l<colspan ; l++ ) {\n\t\t\t\t\t\tfor ( k=0 ; k<rowspan ; k++ ) {\n\t\t\t\t\t\t\tlayout[i+k][shifted+l] = {\n\t\t\t\t\t\t\t\tcell: cell,\n\t\t\t\t\t\t\t\tunique: unique\n\t\t\t\t\t\t\t};\n\t\n\t\t\t\t\t\t\tlayout[i+k].row = row;\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tcols.push( shifted+l );\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Assign an attribute so spanning cells can still be identified\n\t\t\t\t\t// as belonging to a column\n\t\t\t\t\tcell.setAttribute('data-dt-column', _unique(cols).join(','));\n\t\t\t\t}\n\t\n\t\t\t\tcell = cell.nextSibling;\n\t\t\t}\n\t\t}\n\t\n\t\treturn layout;\n\t}\n\t\n\t/**\n\t * Set the start position for draw\n\t *  @param {object} oSettings dataTables settings object\n\t */\n\tfunction _fnStart( oSettings )\n\t{\n\t\tvar bServerSide = _fnDataSource( oSettings ) == 'ssp';\n\t\tvar iInitDisplayStart = oSettings.iInitDisplayStart;\n\t\n\t\t// Check and see if we have an initial draw position from state saving\n\t\tif ( iInitDisplayStart !== undefined && iInitDisplayStart !== -1 )\n\t\t{\n\t\t\toSettings._iDisplayStart = bServerSide ?\n\t\t\t\tiInitDisplayStart :\n\t\t\t\tiInitDisplayStart >= oSettings.fnRecordsDisplay() ?\n\t\t\t\t\t0 :\n\t\t\t\t\tiInitDisplayStart;\n\t\n\t\t\toSettings.iInitDisplayStart = -1;\n\t\t}\n\t}\n\t\n\t/**\n\t * Create an Ajax call based on the table's settings, taking into account that\n\t * parameters can have multiple forms, and backwards compatibility.\n\t *\n\t * @param {object} oSettings dataTables settings object\n\t * @param {array} data Data to send to the server, required by\n\t *     DataTables - may be augmented by developer callbacks\n\t * @param {function} fn Callback function to run when data is obtained\n\t */\n\tfunction _fnBuildAjax(oSettings, data, fn) {\n\t\tvar ajaxData;\n\t\tvar ajax = oSettings.ajax;\n\t\tvar instance = oSettings.oInstance;\n\t\tvar callback = function (json) {\n\t\t\tvar status = oSettings.jqXHR ? oSettings.jqXHR.status : null;\n\t\n\t\t\tif (json === null || (typeof status === 'number' && status == 204)) {\n\t\t\t\tjson = {};\n\t\t\t\t_fnAjaxDataSrc(oSettings, json, []);\n\t\t\t}\n\t\n\t\t\tvar error = json.error || json.sError;\n\t\t\tif (error) {\n\t\t\t\t_fnLog(oSettings, 0, error);\n\t\t\t}\n\t\n\t\t\t// Microsoft often wrap JSON as a string in another JSON object\n\t\t\t// Let's handle that automatically\n\t\t\tif (json.d && typeof json.d === 'string') {\n\t\t\t\ttry {\n\t\t\t\t\tjson = JSON.parse(json.d);\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// noop\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\toSettings.json = json;\n\t\n\t\t\t_fnCallbackFire(oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR], true);\n\t\t\tfn(json);\n\t\t};\n\t\n\t\tif ($.isPlainObject(ajax) && ajax.data) {\n\t\t\tajaxData = ajax.data;\n\t\n\t\t\tvar newData =\n\t\t\t\ttypeof ajaxData === 'function'\n\t\t\t\t\t? ajaxData(data, oSettings) // fn can manipulate data or return\n\t\t\t\t\t: ajaxData; // an object or array to merge\n\t\n\t\t\t// If the function returned something, use that alone\n\t\t\tdata = typeof ajaxData === 'function' && newData ? newData : $.extend(true, data, newData);\n\t\n\t\t\t// Remove the data property as we've resolved it already and don't want\n\t\t\t// jQuery to do it again (it is restored at the end of the function)\n\t\t\tdelete ajax.data;\n\t\t}\n\t\n\t\tvar baseAjax = {\n\t\t\turl: typeof ajax === 'string' ? ajax : '',\n\t\t\tdata: data,\n\t\t\tsuccess: callback,\n\t\t\tdataType: 'json',\n\t\t\tcache: false,\n\t\t\ttype: oSettings.sServerMethod,\n\t\t\terror: function (xhr, error) {\n\t\t\t\tvar ret = _fnCallbackFire(\n\t\t\t\t\toSettings,\n\t\t\t\t\tnull,\n\t\t\t\t\t'xhr',\n\t\t\t\t\t[oSettings, null, oSettings.jqXHR],\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\n\t\t\t\tif (ret.indexOf(true) === -1) {\n\t\t\t\t\tif (error == 'parsererror') {\n\t\t\t\t\t\t_fnLog(oSettings, 0, 'Invalid JSON response', 1);\n\t\t\t\t\t}\n\t\t\t\t\telse if (xhr.readyState === 4) {\n\t\t\t\t\t\t_fnLog(oSettings, 0, 'Ajax error', 7);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t_fnProcessingDisplay(oSettings, false);\n\t\t\t}\n\t\t};\n\t\n\t\t// If `ajax` option is an object, extend and override our default base\n\t\tif ($.isPlainObject(ajax)) {\n\t\t\t$.extend(baseAjax, ajax);\n\t\t}\n\t\n\t\t// Store the data submitted for the API\n\t\toSettings.oAjaxData = data;\n\t\n\t\t// Allow plug-ins and external processes to modify the data\n\t\t_fnCallbackFire(oSettings, null, 'preXhr', [oSettings, data, baseAjax], true);\n\t\n\t\t// Custom Ajax option to submit the parameters as a JSON string\n\t\tif (baseAjax.submitAs === 'json' && typeof data === 'object') {\n\t\t\tbaseAjax.data = JSON.stringify(data);\n\t\n\t\t\tif (!baseAjax.contentType) {\n\t\t\t\tbaseAjax.contentType = 'application/json; charset=utf-8';\n\t\t\t}\n\t\t}\n\t\n\t\tif (typeof ajax === 'function') {\n\t\t\t// Is a function - let the caller define what needs to be done\n\t\t\toSettings.jqXHR = ajax.call(instance, data, callback, oSettings);\n\t\t}\n\t\telse if (ajax.url === '') {\n\t\t\t// No url, so don't load any data. Just apply an empty data array\n\t\t\t// to the object for the callback.\n\t\t\tvar empty = {};\n\t\n\t\t\t_fnAjaxDataSrc(oSettings, empty, []);\n\t\t\tcallback(empty);\n\t\t}\n\t\telse {\n\t\t\t// Object to extend the base settings\n\t\t\toSettings.jqXHR = $.ajax(baseAjax);\n\t\t}\n\t\n\t\t// Restore for next time around\n\t\tif (ajaxData) {\n\t\t\tajax.data = ajaxData;\n\t\t}\n\t}\n\t\n\t/**\n\t * Update the table using an Ajax call\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {boolean} Block the table drawing or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxUpdate(settings) {\n\t\tsettings.iDraw++;\n\t\t_fnProcessingDisplay(settings, true);\n\t\n\t\t_fnBuildAjax(settings, _fnAjaxParameters(settings), function (json) {\n\t\t\t_fnAjaxUpdateDraw(settings, json);\n\t\t});\n\t}\n\t\n\t/**\n\t * Build up the parameters in an object needed for a server-side processing\n\t * request.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @returns {bool} block the table drawing or not\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxParameters(settings) {\n\t\tvar columns = settings.aoColumns,\n\t\t\tfeatures = settings.oFeatures,\n\t\t\tpreSearch = settings.oPreviousSearch,\n\t\t\tpreColSearch = settings.aoPreSearchCols,\n\t\t\tcolData = function (idx, prop) {\n\t\t\t\treturn typeof columns[idx][prop] === 'function' ? 'function' : columns[idx][prop];\n\t\t\t};\n\t\n\t\treturn {\n\t\t\tdraw: settings.iDraw,\n\t\t\tcolumns: columns.map(function (column, i) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: colData(i, 'mData'),\n\t\t\t\t\tname: column.sName,\n\t\t\t\t\tsearchable: column.bSearchable,\n\t\t\t\t\torderable: column.bSortable,\n\t\t\t\t\tsearch: {\n\t\t\t\t\t\tvalue: preColSearch[i].search,\n\t\t\t\t\t\tregex: preColSearch[i].regex,\n\t\t\t\t\t\tfixed: Object.keys(column.searchFixed)\n\t\t\t\t\t\t\t.map(function (name) {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tname: name,\n\t\t\t\t\t\t\t\t\tterm: typeof column.searchFixed[name] !== 'function'\n\t\t\t\t\t\t\t\t\t\t? column.searchFixed[name].toString()\n\t\t\t\t\t\t\t\t\t\t: 'function'\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}),\n\t\t\torder: _fnSortFlatten(settings).map(function (val) {\n\t\t\t\treturn {\n\t\t\t\t\tcolumn: val.col,\n\t\t\t\t\tdir: val.dir,\n\t\t\t\t\tname: colData(val.col, 'sName')\n\t\t\t\t};\n\t\t\t}),\n\t\t\tstart: settings._iDisplayStart,\n\t\t\tlength: features.bPaginate ? settings._iDisplayLength : -1,\n\t\t\tsearch: {\n\t\t\t\tvalue: preSearch.search,\n\t\t\t\tregex: preSearch.regex,\n\t\t\t\tfixed: Object.keys(settings.searchFixed)\n\t\t\t\t\t.map(function (name) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tname: name,\n\t\t\t\t\t\t\tterm: typeof settings.searchFixed[name] !== 'function'\n\t\t\t\t\t\t\t\t? settings.searchFixed[name].toString()\n\t\t\t\t\t\t\t\t: 'function'\n\t\t\t\t\t\t};\n\t\t\t\t\t})\n\t\t\t}\n\t\t};\n\t}\n\t\n\t/**\n\t * Data the data from the server (nuking the old) and redraw the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} json json data return from the server.\n\t *  @param {string} json.sEcho Tracking flag for DataTables to match requests\n\t *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering\n\t *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering\n\t *  @param {array} json.aaData The data to display on this page\n\t *  @param {string} [json.sColumns] Column ordering (sName, comma separated)\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnAjaxUpdateDraw(settings, json) {\n\t\tvar data = _fnAjaxDataSrc(settings, json);\n\t\tvar draw = _fnAjaxDataSrcParam(settings, 'draw', json);\n\t\tvar recordsTotal = _fnAjaxDataSrcParam(settings, 'recordsTotal', json);\n\t\tvar recordsFiltered = _fnAjaxDataSrcParam(settings, 'recordsFiltered', json);\n\t\n\t\tif (draw !== undefined) {\n\t\t\t// Protect against out of sequence returns\n\t\t\tif (draw * 1 < settings.iDraw) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettings.iDraw = draw * 1;\n\t\t}\n\t\n\t\t// No data in returned object, so rather than an array, we show an empty table\n\t\tif (!data) {\n\t\t\tdata = [];\n\t\t}\n\t\n\t\t_fnClearTable(settings);\n\t\tsettings._iRecordsTotal = parseInt(recordsTotal, 10);\n\t\tsettings._iRecordsDisplay = parseInt(recordsFiltered, 10);\n\t\n\t\tfor (var i = 0, iLen = data.length; i < iLen; i++) {\n\t\t\t_fnAddData(settings, data[i]);\n\t\t}\n\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\n\t\t_fnColumnTypes(settings);\n\t\t_fnDraw(settings, true);\n\t\t_fnInitComplete(settings);\n\t\t_fnProcessingDisplay(settings, false);\n\t}\n\t\n\t/**\n\t * Get the data from the JSON data source to use for drawing a table. Using\n\t * `_fnGetObjectDataFn` allows the data to be sourced from a property of the\n\t * source object, or from a processing function.\n\t *  @param {object} settings dataTables settings object\n\t *  @param  {object} json Data source object / array from the server\n\t *  @return {array} Array of data to use\n\t */\n\tfunction _fnAjaxDataSrc(settings, json, write) {\n\t\tvar dataProp = 'data';\n\t\n\t\tif ($.isPlainObject(settings.ajax) && settings.ajax.dataSrc !== undefined) {\n\t\t\t// Could in inside a `dataSrc` object, or not!\n\t\t\tvar dataSrc = settings.ajax.dataSrc;\n\t\n\t\t\t// string, function and object are valid types\n\t\t\tif (typeof dataSrc === 'string' || typeof dataSrc === 'function') {\n\t\t\t\tdataProp = dataSrc;\n\t\t\t}\n\t\t\telse if (dataSrc.data !== undefined) {\n\t\t\t\tdataProp = dataSrc.data;\n\t\t\t}\n\t\t}\n\t\n\t\tif (!write) {\n\t\t\tif (dataProp === 'data') {\n\t\t\t\t// If the default, then we still want to support the old style, and safely ignore\n\t\t\t\t// it if possible\n\t\t\t\treturn json.aaData || json[dataProp];\n\t\t\t}\n\t\n\t\t\treturn dataProp !== '' ? _fnGetObjectDataFn(dataProp)(json) : json;\n\t\t}\n\t\n\t\t// set\n\t\t_fnSetObjectDataFn(dataProp)(json, write);\n\t}\n\t\n\t/**\n\t * Very similar to _fnAjaxDataSrc, but for the other SSP properties\n\t * @param {*} settings DataTables settings object\n\t * @param {*} param Target parameter\n\t * @param {*} json JSON data\n\t * @returns Resolved value\n\t */\n\tfunction _fnAjaxDataSrcParam(settings, param, json) {\n\t\tvar dataSrc = $.isPlainObject(settings.ajax) ? settings.ajax.dataSrc : null;\n\t\n\t\tif (dataSrc && dataSrc[param]) {\n\t\t\t// Get from custom location\n\t\t\treturn _fnGetObjectDataFn(dataSrc[param])(json);\n\t\t}\n\t\n\t\t// else - Default behaviour\n\t\tvar old = '';\n\t\n\t\t// Legacy support\n\t\tif (param === 'draw') {\n\t\t\told = 'sEcho';\n\t\t}\n\t\telse if (param === 'recordsTotal') {\n\t\t\told = 'iTotalRecords';\n\t\t}\n\t\telse if (param === 'recordsFiltered') {\n\t\t\told = 'iTotalDisplayRecords';\n\t\t}\n\t\n\t\treturn json[old] !== undefined ? json[old] : json[param];\n\t}\n\t\n\t\n\t/**\n\t * Filter the table using both the global filter and column based filtering\n\t *  @param {object} settings dataTables settings object\n\t *  @param {object} input search information\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterComplete ( settings, input )\n\t{\n\t\tvar columnsSearch = settings.aoPreSearchCols;\n\t\n\t\t// In server-side processing all filtering is done by the server, so no point hanging around here\n\t\tif ( _fnDataSource( settings ) != 'ssp' )\n\t\t{\n\t\t\t// Check if any of the rows were invalidated\n\t\t\t_fnFilterData( settings );\n\t\n\t\t\t// Start from the full data set\n\t\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\n\t\t\t// Global filter first\n\t\t\t_fnFilter( settings.aiDisplay, settings, input.search, input );\n\t\n\t\t\t$.each(settings.searchFixed, function (name, term) {\n\t\t\t\t_fnFilter(settings.aiDisplay, settings, term, {});\n\t\t\t});\n\t\n\t\t\t// Then individual column filters\n\t\t\tfor ( var i=0 ; i<columnsSearch.length ; i++ )\n\t\t\t{\n\t\t\t\tvar col = columnsSearch[i];\n\t\n\t\t\t\t_fnFilter(\n\t\t\t\t\tsettings.aiDisplay,\n\t\t\t\t\tsettings,\n\t\t\t\t\tcol.search,\n\t\t\t\t\tcol,\n\t\t\t\t\ti\n\t\t\t\t);\n\t\n\t\t\t\t$.each(settings.aoColumns[i].searchFixed, function (name, term) {\n\t\t\t\t\t_fnFilter(settings.aiDisplay, settings, term, {}, i);\n\t\t\t\t});\n\t\t\t}\n\t\n\t\t\t// And finally global filtering\n\t\t\t_fnFilterCustom( settings );\n\t\t}\n\t\n\t\t// Tell the draw function we have been filtering\n\t\tsettings.bFiltered = true;\n\t\n\t\t_fnCallbackFire( settings, null, 'search', [settings] );\n\t}\n\t\n\t\n\t/**\n\t * Apply custom filtering functions\n\t * \n\t * This is legacy now that we have named functions, but it is widely used\n\t * from 1.x, so it is not yet deprecated.\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterCustom( settings )\n\t{\n\t\tvar filters = DataTable.ext.search;\n\t\tvar displayRows = settings.aiDisplay;\n\t\tvar row, rowIdx;\n\t\n\t\tfor ( var i=0, iLen=filters.length ; i<iLen ; i++ ) {\n\t\t\tvar rows = [];\n\t\n\t\t\t// Loop over each row and see if it should be included\n\t\t\tfor ( var j=0, jen=displayRows.length ; j<jen ; j++ ) {\n\t\t\t\trowIdx = displayRows[ j ];\n\t\t\t\trow = settings.aoData[ rowIdx ];\n\t\n\t\t\t\tif ( filters[i]( settings, row._aFilterData, rowIdx, row._aData, j ) ) {\n\t\t\t\t\trows.push( rowIdx );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// So the array reference doesn't break set the results into the\n\t\t\t// existing array\n\t\t\tdisplayRows.length = 0;\n\t\t\t_fnArrayApply(displayRows, rows);\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Filter the data table based on user input and draw the table\n\t */\n\tfunction _fnFilter( searchRows, settings, input, options, column )\n\t{\n\t\tif ( input === '' ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar i = 0;\n\t\tvar matched = [];\n\t\n\t\t// Search term can be a function, regex or string - if a string we apply our\n\t\t// smart filtering regex (assuming the options require that)\n\t\tvar searchFunc = typeof input === 'function' ? input : null;\n\t\tvar rpSearch = input instanceof RegExp\n\t\t\t? input\n\t\t\t: searchFunc\n\t\t\t\t? null\n\t\t\t\t: _fnFilterCreateSearch( input, options );\n\t\n\t\t// Then for each row, does the test pass. If not, lop the row from the array\n\t\tfor (i=0 ; i<searchRows.length ; i++) {\n\t\t\tvar row = settings.aoData[ searchRows[i] ];\n\t\t\tvar data = column === undefined\n\t\t\t\t? row._sFilterRow\n\t\t\t\t: row._aFilterData[ column ];\n\t\n\t\t\tif ( (searchFunc && searchFunc(data, row._aData, searchRows[i], column)) || (rpSearch && rpSearch.test(data)) ) {\n\t\t\t\tmatched.push(searchRows[i]);\n\t\t\t}\n\t\t}\n\t\n\t\t// Mutate the searchRows array\n\t\tsearchRows.length = matched.length;\n\t\n\t\tfor (i=0 ; i<matched.length ; i++) {\n\t\t\tsearchRows[i] = matched[i];\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Build a regular expression object suitable for searching a table\n\t *  @param {string} sSearch string to search for\n\t *  @param {bool} bRegex treat as a regular expression or not\n\t *  @param {bool} bSmart perform smart filtering or not\n\t *  @param {bool} bCaseInsensitive Do case-insensitive matching or not\n\t *  @returns {RegExp} constructed object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFilterCreateSearch( search, inOpts )\n\t{\n\t\tvar not = [];\n\t\tvar options = $.extend({}, {\n\t\t\tboundary: false,\n\t\t\tcaseInsensitive: true,\n\t\t\texact: false,\n\t\t\tregex: false,\n\t\t\tsmart: true\n\t\t}, inOpts);\n\t\n\t\tif (typeof search !== 'string') {\n\t\t\tsearch = search.toString();\n\t\t}\n\t\n\t\t// Remove diacritics if normalize is set up to do so\n\t\tsearch = _normalize(search);\n\t\n\t\tif (options.exact) {\n\t\t\treturn new RegExp(\n\t\t\t\t'^'+_fnEscapeRegex(search)+'$',\n\t\t\t\toptions.caseInsensitive ? 'i' : ''\n\t\t\t);\n\t\t}\n\t\n\t\tsearch = options.regex ?\n\t\t\tsearch :\n\t\t\t_fnEscapeRegex( search );\n\t\t\n\t\tif ( options.smart ) {\n\t\t\t/* For smart filtering we want to allow the search to work regardless of\n\t\t\t * word order. We also want double quoted text to be preserved, so word\n\t\t\t * order is important - a la google. And a negative look around for\n\t\t\t * finding rows which don't contain a given string.\n\t\t\t * \n\t\t\t * So this is the sort of thing we want to generate:\n\t\t\t * \n\t\t\t * ^(?=.*?\\bone\\b)(?=.*?\\btwo three\\b)(?=.*?\\bfour\\b).*$\n\t\t\t */\n\t\t\tvar parts = search.match( /!?[\"\\u201C][^\"\\u201D]+[\"\\u201D]|[^ ]+/g ) || [''];\n\t\t\tvar a = parts.map( function ( word ) {\n\t\t\t\tvar negative = false;\n\t\t\t\tvar m;\n\t\n\t\t\t\t// Determine if it is a \"does not include\"\n\t\t\t\tif ( word.charAt(0) === '!' ) {\n\t\t\t\t\tnegative = true;\n\t\t\t\t\tword = word.substring(1);\n\t\t\t\t}\n\t\n\t\t\t\t// Strip the quotes from around matched phrases\n\t\t\t\tif ( word.charAt(0) === '\"' ) {\n\t\t\t\t\tm = word.match( /^\"(.*)\"$/ );\n\t\t\t\t\tword = m ? m[1] : word;\n\t\t\t\t}\n\t\t\t\telse if ( word.charAt(0) === '\\u201C' ) {\n\t\t\t\t\t// Smart quote match (iPhone users)\n\t\t\t\t\tm = word.match( /^\\u201C(.*)\\u201D$/ );\n\t\t\t\t\tword = m ? m[1] : word;\n\t\t\t\t}\n\t\n\t\t\t\t// For our \"not\" case, we need to modify the string that is\n\t\t\t\t// allowed to match at the end of the expression.\n\t\t\t\tif (negative) {\n\t\t\t\t\tif (word.length > 1) {\n\t\t\t\t\t\tnot.push('(?!'+word+')');\n\t\t\t\t\t}\n\t\n\t\t\t\t\tword = '';\n\t\t\t\t}\n\t\n\t\t\t\treturn word.replace(/\"/g, '');\n\t\t\t} );\n\t\n\t\t\tvar match = not.length\n\t\t\t\t? not.join('')\n\t\t\t\t: '';\n\t\n\t\t\tvar boundary = options.boundary\n\t\t\t\t? '\\\\b'\n\t\t\t\t: '';\n\t\n\t\t\tsearch = '^(?=.*?'+boundary+a.join( ')(?=.*?'+boundary )+')('+match+'.)*$';\n\t\t}\n\t\n\t\treturn new RegExp( search, options.caseInsensitive ? 'i' : '' );\n\t}\n\t\n\t\n\t/**\n\t * Escape a string such that it can be used in a regular expression\n\t *  @param {string} sVal string to escape\n\t *  @returns {string} escaped string\n\t *  @memberof DataTable#oApi\n\t */\n\tvar _fnEscapeRegex = DataTable.util.escapeRegex;\n\t\n\tvar __filter_div = $('<div>')[0];\n\tvar __filter_div_textContent = __filter_div.textContent !== undefined;\n\t\n\t// Update the filtering data for each row if needed (by invalidation or first run)\n\tfunction _fnFilterData ( settings )\n\t{\n\t\tvar columns = settings.aoColumns;\n\t\tvar data = settings.aoData;\n\t\tvar column;\n\t\tvar j, jen, filterData, cellData, row;\n\t\tvar wasInvalidated = false;\n\t\n\t\tfor ( var rowIdx=0 ; rowIdx<data.length ; rowIdx++ ) {\n\t\t\tif (! data[rowIdx]) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\trow = data[rowIdx];\n\t\n\t\t\tif ( ! row._aFilterData ) {\n\t\t\t\tfilterData = [];\n\t\n\t\t\t\tfor ( j=0, jen=columns.length ; j<jen ; j++ ) {\n\t\t\t\t\tcolumn = columns[j];\n\t\n\t\t\t\t\tif ( column.bSearchable ) {\n\t\t\t\t\t\tcellData = _fnGetCellData( settings, rowIdx, j, 'filter' );\n\t\n\t\t\t\t\t\t// Search in DataTables is string based\n\t\t\t\t\t\tif ( cellData === null ) {\n\t\t\t\t\t\t\tcellData = '';\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif ( typeof cellData !== 'string' && cellData.toString ) {\n\t\t\t\t\t\t\tcellData = cellData.toString();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tcellData = '';\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// If it looks like there is an HTML entity in the string,\n\t\t\t\t\t// attempt to decode it so sorting works as expected. Note that\n\t\t\t\t\t// we could use a single line of jQuery to do this, but the DOM\n\t\t\t\t\t// method used here is much faster https://jsperf.com/html-decode\n\t\t\t\t\tif ( cellData.indexOf && cellData.indexOf('&') !== -1 ) {\n\t\t\t\t\t\t__filter_div.innerHTML = cellData;\n\t\t\t\t\t\tcellData = __filter_div_textContent ?\n\t\t\t\t\t\t\t__filter_div.textContent :\n\t\t\t\t\t\t\t__filter_div.innerText;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif ( cellData.replace ) {\n\t\t\t\t\t\tcellData = cellData.replace(/[\\r\\n\\u2028]/g, '');\n\t\t\t\t\t}\n\t\n\t\t\t\t\tfilterData.push( cellData );\n\t\t\t\t}\n\t\n\t\t\t\trow._aFilterData = filterData;\n\t\t\t\trow._sFilterRow = filterData.join('  ');\n\t\t\t\twasInvalidated = true;\n\t\t\t}\n\t\t}\n\t\n\t\treturn wasInvalidated;\n\t}\n\t\n\t\n\t/**\n\t * Draw the table for the first time, adding all required features\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnInitialise ( settings )\n\t{\n\t\tvar i;\n\t\tvar init = settings.oInit;\n\t\tvar deferLoading = settings.deferLoading;\n\t\tvar dataSrc = _fnDataSource( settings );\n\t\n\t\t// Ensure that the table data is fully initialised\n\t\tif ( ! settings.bInitialised ) {\n\t\t\tsetTimeout( function(){ _fnInitialise( settings ); }, 200 );\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Build the header / footer for the table\n\t\t_fnBuildHead( settings, 'header' );\n\t\t_fnBuildHead( settings, 'footer' );\n\t\n\t\t// Load the table's state (if needed) and then render around it and draw\n\t\t_fnLoadState( settings, init, function () {\n\t\t\t// Then draw the header / footer\n\t\t\t_fnDrawHead( settings, settings.aoHeader );\n\t\t\t_fnDrawHead( settings, settings.aoFooter );\n\t\n\t\t\t// Cache the paging start point, as the first redraw will reset it\n\t\t\tvar iAjaxStart = settings.iInitDisplayStart\n\t\n\t\t\t// Local data load\n\t\t\t// Check if there is data passing into the constructor\n\t\t\tif ( init.aaData ) {\n\t\t\t\tfor ( i=0 ; i<init.aaData.length ; i++ ) {\n\t\t\t\t\t_fnAddData( settings, init.aaData[ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if ( deferLoading || dataSrc == 'dom' ) {\n\t\t\t\t// Grab the data from the page\n\t\t\t\t_fnAddTr( settings, $(settings.nTBody).children('tr') );\n\t\t\t}\n\t\n\t\t\t// Filter not yet applied - copy the display master\n\t\t\tsettings.aiDisplay = settings.aiDisplayMaster.slice();\n\t\n\t\t\t// Enable features\n\t\t\t_fnAddOptionsHtml( settings );\n\t\t\t_fnSortInit( settings );\n\t\n\t\t\t_colGroup( settings );\n\t\n\t\t\t/* Okay to show that something is going on now */\n\t\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t\t_fnCallbackFire( settings, null, 'preInit', [settings], true );\n\t\n\t\t\t// If there is default sorting required - let's do it. The sort function\n\t\t\t// will do the drawing for us. Otherwise we draw the table regardless of the\n\t\t\t// Ajax source - this allows the table to look initialised for Ajax sourcing\n\t\t\t// data (show 'loading' message possibly)\n\t\t\t_fnReDraw( settings );\n\t\n\t\t\t// Server-side processing init complete is done by _fnAjaxUpdateDraw\n\t\t\tif ( dataSrc != 'ssp' || deferLoading ) {\n\t\t\t\t// if there is an ajax source load the data\n\t\t\t\tif ( dataSrc == 'ajax' ) {\n\t\t\t\t\t_fnBuildAjax( settings, {}, function(json) {\n\t\t\t\t\t\tvar aData = _fnAjaxDataSrc( settings, json );\n\t\n\t\t\t\t\t\t// Got the data - add it to the table\n\t\t\t\t\t\tfor ( i=0 ; i<aData.length ; i++ ) {\n\t\t\t\t\t\t\t_fnAddData( settings, aData[i] );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// Reset the init display for cookie saving. We've already done\n\t\t\t\t\t\t// a filter, and therefore cleared it before. So we need to make\n\t\t\t\t\t\t// it appear 'fresh'\n\t\t\t\t\t\tsettings.iInitDisplayStart = iAjaxStart;\n\t\n\t\t\t\t\t\t_fnReDraw( settings );\n\t\t\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t\t\t\t_fnInitComplete( settings );\n\t\t\t\t\t}, settings );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t_fnInitComplete( settings );\n\t\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Draw the table for the first time, adding all required features\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnInitComplete ( settings )\n\t{\n\t\tif (settings._bInitComplete) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar args = [settings, settings.json];\n\t\n\t\tsettings._bInitComplete = true;\n\t\n\t\t// Table is fully set up and we have data, so calculate the\n\t\t// column widths\n\t\t_fnAdjustColumnSizing( settings );\n\t\n\t\t_fnCallbackFire( settings, null, 'plugin-init', args, true );\n\t\t_fnCallbackFire( settings, 'aoInitComplete', 'init', args, true );\n\t}\n\t\n\tfunction _fnLengthChange ( settings, val )\n\t{\n\t\tvar len = parseInt( val, 10 );\n\t\tsettings._iDisplayLength = len;\n\t\n\t\t_fnLengthOverflow( settings );\n\t\n\t\t// Fire length change event\n\t\t_fnCallbackFire( settings, null, 'length', [settings, len] );\n\t}\n\t\n\t/**\n\t * Alter the display settings to change the page\n\t *  @param {object} settings DataTables settings object\n\t *  @param {string|int} action Paging action to take: \"first\", \"previous\",\n\t *    \"next\" or \"last\" or page number to jump to (integer)\n\t *  @param [bool] redraw Automatically draw the update or not\n\t *  @returns {bool} true page has changed, false - no change\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnPageChange ( settings, action, redraw )\n\t{\n\t\tvar\n\t\t\tstart     = settings._iDisplayStart,\n\t\t\tlen       = settings._iDisplayLength,\n\t\t\trecords   = settings.fnRecordsDisplay();\n\t\n\t\tif ( records === 0 || len === -1 )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\telse if ( typeof action === \"number\" )\n\t\t{\n\t\t\tstart = action * len;\n\t\n\t\t\tif ( start > records )\n\t\t\t{\n\t\t\t\tstart = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"first\" )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\telse if ( action == \"previous\" )\n\t\t{\n\t\t\tstart = len >= 0 ?\n\t\t\t\tstart - len :\n\t\t\t\t0;\n\t\n\t\t\tif ( start < 0 )\n\t\t\t{\n\t\t\t\tstart = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"next\" )\n\t\t{\n\t\t\tif ( start + len < records )\n\t\t\t{\n\t\t\t\tstart += len;\n\t\t\t}\n\t\t}\n\t\telse if ( action == \"last\" )\n\t\t{\n\t\t\tstart = Math.floor( (records-1) / len) * len;\n\t\t}\n\t\telse if ( action === 'ellipsis' )\n\t\t{\n\t\t\treturn;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_fnLog( settings, 0, \"Unknown paging action: \"+action, 5 );\n\t\t}\n\t\n\t\tvar changed = settings._iDisplayStart !== start;\n\t\tsettings._iDisplayStart = start;\n\t\n\t\t_fnCallbackFire( settings, null, changed ? 'page' : 'page-nc', [settings] );\n\t\n\t\tif ( changed && redraw ) {\n\t\t\t_fnDraw( settings );\n\t\t}\n\t\n\t\treturn changed;\n\t}\n\t\n\t\n\t/**\n\t * Generate the node required for the processing node\n\t *  @param {object} settings DataTables settings object\n\t */\n\tfunction _processingHtml ( settings )\n\t{\n\t\tvar table = settings.nTable;\n\t\tvar scrolling = settings.oScroll.sX !== '' || settings.oScroll.sY !== '';\n\t\n\t\tif ( settings.oFeatures.bProcessing ) {\n\t\t\tvar n = $('<div/>', {\n\t\t\t\t\t'id': settings.sTableId + '_processing',\n\t\t\t\t\t'class': settings.oClasses.processing.container,\n\t\t\t\t\t'role': 'status'\n\t\t\t\t} )\n\t\t\t\t.html( settings.oLanguage.sProcessing )\n\t\t\t\t.append('<div><div></div><div></div><div></div><div></div></div>');\n\t\n\t\t\t// Different positioning depending on if scrolling is enabled or not\n\t\t\tif (scrolling) {\n\t\t\t\tn.prependTo( $('div.dt-scroll', settings.nTableWrapper) );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tn.insertBefore( table );\n\t\t\t}\n\t\n\t\t\t$(table).on( 'processing.dt.DT', function (e, s, show) {\n\t\t\t\tn.css( 'display', show ? 'block' : 'none' );\n\t\t\t} );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Display or hide the processing indicator\n\t *  @param {object} settings DataTables settings object\n\t *  @param {bool} show Show the processing indicator (true) or not (false)\n\t */\n\tfunction _fnProcessingDisplay ( settings, show )\n\t{\n\t\t// Ignore cases when we are still redrawing\n\t\tif (settings.bDrawing && show === false) {\n\t\t\treturn;\n\t\t}\n\t\n\t\t_fnCallbackFire( settings, null, 'processing', [settings, show] );\n\t}\n\t\n\t/**\n\t * Show the processing element if an action takes longer than a given time\n\t *\n\t * @param {*} settings DataTables settings object\n\t * @param {*} enable Do (true) or not (false) async processing (local feature enablement)\n\t * @param {*} run Function to run\n\t */\n\tfunction _fnProcessingRun( settings, enable, run ) {\n\t\tif (! enable) {\n\t\t\t// Immediate execution, synchronous\n\t\t\trun();\n\t\t}\n\t\telse {\n\t\t\t_fnProcessingDisplay(settings, true);\n\t\t\t\n\t\t\t// Allow the processing display to show if needed\n\t\t\tsetTimeout(function () {\n\t\t\t\trun();\n\t\n\t\t\t\t_fnProcessingDisplay(settings, false);\n\t\t\t}, 0);\n\t\t}\n\t}\n\t/**\n\t * Add any control elements for the table - specifically scrolling\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {node} Node to add to the DOM\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnFeatureHtmlTable ( settings )\n\t{\n\t\tvar table = $(settings.nTable);\n\t\n\t\t// Scrolling from here on in\n\t\tvar scroll = settings.oScroll;\n\t\n\t\tif ( scroll.sX === '' && scroll.sY === '' ) {\n\t\t\treturn settings.nTable;\n\t\t}\n\t\n\t\tvar scrollX = scroll.sX;\n\t\tvar scrollY = scroll.sY;\n\t\tvar classes = settings.oClasses.scrolling;\n\t\tvar caption = settings.captionNode;\n\t\tvar captionSide = caption ? caption._captionSide : null;\n\t\tvar headerClone = $( table[0].cloneNode(false) );\n\t\tvar footerClone = $( table[0].cloneNode(false) );\n\t\tvar footer = table.children('tfoot');\n\t\tvar _div = '<div/>';\n\t\tvar size = function ( s ) {\n\t\t\treturn !s ? null : _fnStringToCss( s );\n\t\t};\n\t\n\t\tif ( ! footer.length ) {\n\t\t\tfooter = null;\n\t\t}\n\t\n\t\t/*\n\t\t * The HTML structure that we want to generate in this function is:\n\t\t *  div - scroller\n\t\t *    div - scroll head\n\t\t *      div - scroll head inner\n\t\t *        table - scroll head table\n\t\t *          thead - thead\n\t\t *    div - scroll body\n\t\t *      table - table (master table)\n\t\t *        thead - thead clone for sizing\n\t\t *        tbody - tbody\n\t\t *    div - scroll foot\n\t\t *      div - scroll foot inner\n\t\t *        table - scroll foot table\n\t\t *          tfoot - tfoot\n\t\t */\n\t\tvar scroller = $( _div, { 'class': classes.container } )\n\t\t\t.append(\n\t\t\t\t$(_div, { 'class': classes.header.self } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\twidth: scrollX ? size(scrollX) : '100%'\n\t\t\t\t\t} )\n\t\t\t\t\t.append(\n\t\t\t\t\t\t$(_div, { 'class': classes.header.inner } )\n\t\t\t\t\t\t\t.css( {\n\t\t\t\t\t\t\t\t'box-sizing': 'content-box',\n\t\t\t\t\t\t\t\twidth: scroll.sXInner || '100%'\n\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\theaderClone\n\t\t\t\t\t\t\t\t\t.removeAttr('id')\n\t\t\t\t\t\t\t\t\t.css( 'margin-left', 0 )\n\t\t\t\t\t\t\t\t\t.append( captionSide === 'top' ? caption : null )\n\t\t\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\t\t\ttable.children('thead')\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t)\n\t\t\t.append(\n\t\t\t\t$(_div, { 'class': classes.body } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\tposition: 'relative',\n\t\t\t\t\t\toverflow: 'auto',\n\t\t\t\t\t\twidth: size( scrollX )\n\t\t\t\t\t} )\n\t\t\t\t\t.append( table )\n\t\t\t);\n\t\n\t\tif ( footer ) {\n\t\t\tscroller.append(\n\t\t\t\t$(_div, { 'class': classes.footer.self } )\n\t\t\t\t\t.css( {\n\t\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\twidth: scrollX ? size(scrollX) : '100%'\n\t\t\t\t\t} )\n\t\t\t\t\t.append(\n\t\t\t\t\t\t$(_div, { 'class': classes.footer.inner } )\n\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\tfooterClone\n\t\t\t\t\t\t\t\t\t.removeAttr('id')\n\t\t\t\t\t\t\t\t\t.css( 'margin-left', 0 )\n\t\t\t\t\t\t\t\t\t.append( captionSide === 'bottom' ? caption : null )\n\t\t\t\t\t\t\t\t\t.append(\n\t\t\t\t\t\t\t\t\t\ttable.children('tfoot')\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\n\t\tvar children = scroller.children();\n\t\tvar scrollHead = children[0];\n\t\tvar scrollBody = children[1];\n\t\tvar scrollFoot = footer ? children[2] : null;\n\t\n\t\t// When the body is scrolled, then we also want to scroll the headers\n\t\t$(scrollBody).on( 'scroll.DT', function () {\n\t\t\tvar scrollLeft = this.scrollLeft;\n\t\n\t\t\tscrollHead.scrollLeft = scrollLeft;\n\t\n\t\t\tif ( footer ) {\n\t\t\t\tscrollFoot.scrollLeft = scrollLeft;\n\t\t\t}\n\t\t} );\n\t\n\t\t// When focus is put on the header cells, we might need to scroll the body\n\t\t$('th, td', scrollHead).on('focus', function () {\n\t\t\tvar scrollLeft = scrollHead.scrollLeft;\n\t\n\t\t\tscrollBody.scrollLeft = scrollLeft;\n\t\n\t\t\tif ( footer ) {\n\t\t\t\tscrollBody.scrollLeft = scrollLeft;\n\t\t\t}\n\t\t});\n\t\n\t\t$(scrollBody).css('max-height', scrollY);\n\t\tif (! scroll.bCollapse) {\n\t\t\t$(scrollBody).css('height', scrollY);\n\t\t}\n\t\n\t\tsettings.nScrollHead = scrollHead;\n\t\tsettings.nScrollBody = scrollBody;\n\t\tsettings.nScrollFoot = scrollFoot;\n\t\n\t\t// On redraw - align columns\n\t\tsettings.aoDrawCallback.push(_fnScrollDraw);\n\t\n\t\treturn scroller[0];\n\t}\n\t\n\t\n\t\n\t/**\n\t * Update the header, footer and body tables for resizing - i.e. column\n\t * alignment.\n\t *\n\t * Welcome to the most horrible function DataTables. The process that this\n\t * function follows is basically:\n\t *   1. Re-create the table inside the scrolling div\n\t *   2. Correct colgroup > col values if needed\n\t *   3. Copy colgroup > col over to header and footer\n\t *   4. Clean up\n\t *\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnScrollDraw ( settings )\n\t{\n\t\t// Given that this is such a monster function, a lot of variables are use\n\t\t// to try and keep the minimised size as small as possible\n\t\tvar\n\t\t\tscroll         = settings.oScroll,\n\t\t\tbarWidth       = scroll.iBarWidth,\n\t\t\tdivHeader      = $(settings.nScrollHead),\n\t\t\tdivHeaderInner = divHeader.children('div'),\n\t\t\tdivHeaderTable = divHeaderInner.children('table'),\n\t\t\tdivBodyEl      = settings.nScrollBody,\n\t\t\tdivBody        = $(divBodyEl),\n\t\t\tdivFooter      = $(settings.nScrollFoot),\n\t\t\tdivFooterInner = divFooter.children('div'),\n\t\t\tdivFooterTable = divFooterInner.children('table'),\n\t\t\theader         = $(settings.nTHead),\n\t\t\ttable          = $(settings.nTable),\n\t\t\tfooter         = settings.nTFoot && $('th, td', settings.nTFoot).length ? $(settings.nTFoot) : null,\n\t\t\tbrowser        = settings.oBrowser,\n\t\t\theaderCopy, footerCopy;\n\t\n\t\t// If the scrollbar visibility has changed from the last draw, we need to\n\t\t// adjust the column sizes as the table width will have changed to account\n\t\t// for the scrollbar\n\t\tvar scrollBarVis = divBodyEl.scrollHeight > divBodyEl.clientHeight;\n\t\t\n\t\tif ( settings.scrollBarVis !== scrollBarVis && settings.scrollBarVis !== undefined ) {\n\t\t\tsettings.scrollBarVis = scrollBarVis;\n\t\t\t_fnAdjustColumnSizing( settings );\n\t\t\treturn; // adjust column sizing will call this function again\n\t\t}\n\t\telse {\n\t\t\tsettings.scrollBarVis = scrollBarVis;\n\t\t}\n\t\n\t\t// 1. Re-create the table inside the scrolling div\n\t\t// Remove the old minimised thead and tfoot elements in the inner table\n\t\ttable.children('thead, tfoot').remove();\n\t\n\t\t// Clone the current header and footer elements and then place it into the inner table\n\t\theaderCopy = header.clone().prependTo( table );\n\t\theaderCopy.find('th, td').removeAttr('tabindex');\n\t\theaderCopy.find('[id]').removeAttr('id');\n\t\n\t\tif ( footer ) {\n\t\t\tfooterCopy = footer.clone().prependTo( table );\n\t\t\tfooterCopy.find('[id]').removeAttr('id');\n\t\t}\n\t\n\t\t// 2. Correct colgroup > col values if needed\n\t\t// It is possible that the cell sizes are smaller than the content, so we need to\n\t\t// correct colgroup>col for such cases. This can happen if the auto width detection\n\t\t// uses a cell which has a longer string, but isn't the widest! For example \n\t\t// \"Chief Executive Officer (CEO)\" is the longest string in the demo, but\n\t\t// \"Systems Administrator\" is actually the widest string since it doesn't collapse.\n\t\t// Note the use of translating into a column index to get the `col` element. This\n\t\t// is because of Responsive which might remove `col` elements, knocking the alignment\n\t\t// of the indexes out.\n\t\tif (settings.aiDisplay.length) {\n\t\t\t// Get the column sizes from the first row in the table. This should really be a\n\t\t\t// [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year\n\t\t\t// browser support\n\t\t\tvar firstTr = null;\n\t\t\tvar start = _fnDataSource( settings ) !== 'ssp'\n\t\t\t\t? settings._iDisplayStart\n\t\t\t\t: 0;\n\t\n\t\t\tfor (i=start ; i<start + settings.aiDisplay.length ; i++) {\n\t\t\t\tvar idx = settings.aiDisplay[i];\n\t\t\t\tvar tr = settings.aoData[idx].nTr;\n\t\n\t\t\t\tif (tr) {\n\t\t\t\t\tfirstTr = tr;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tif (firstTr) {\n\t\t\t\tvar colSizes = $(firstTr).children('th, td').map(function (vis) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tidx: _fnVisibleToColumnIndex(settings, vis),\n\t\t\t\t\t\twidth: $(this).outerWidth()\n\t\t\t\t\t};\n\t\t\t\t});\n\t\n\t\t\t\t// Check against what the colgroup > col is set to and correct if needed\n\t\t\t\tfor (var i=0 ; i<colSizes.length ; i++) {\n\t\t\t\t\tvar colEl = settings.aoColumns[ colSizes[i].idx ].colEl[0];\n\t\t\t\t\tvar colWidth = colEl.style.width.replace('px', '');\n\t\n\t\t\t\t\tif (colWidth !== colSizes[i].width) {\n\t\t\t\t\t\tcolEl.style.width = colSizes[i].width + 'px';\n\t\n\t\t\t\t\t\tif (scroll.sX) {\n\t\t\t\t\t\t\tcolEl.style.minWidth = colSizes[i].width + 'px';\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// 3. Copy the colgroup over to the header and footer\n\t\tdivHeaderTable\n\t\t\t.find('colgroup')\n\t\t\t.remove();\n\t\n\t\tdivHeaderTable.append(settings.colgroup.clone());\n\t\n\t\tif ( footer ) {\n\t\t\tdivFooterTable\n\t\t\t\t.find('colgroup')\n\t\t\t\t.remove();\n\t\n\t\t\tdivFooterTable.append(settings.colgroup.clone());\n\t\t}\n\t\n\t\t// \"Hide\" the header and footer that we used for the sizing. We need to keep\n\t\t// the content of the cell so that the width applied to the header and body\n\t\t// both match, but we want to hide it completely.\n\t\t$('th, td', headerCopy).each(function () {\n\t\t\t$(this.childNodes).wrapAll('<div class=\"dt-scroll-sizing\">');\n\t\t});\n\t\n\t\tif ( footer ) {\n\t\t\t$('th, td', footerCopy).each(function () {\n\t\t\t\t$(this.childNodes).wrapAll('<div class=\"dt-scroll-sizing\">');\n\t\t\t});\n\t\t}\n\t\n\t\t// 4. Clean up\n\t\t// Figure out if there are scrollbar present - if so then we need the header and footer to\n\t\t// provide a bit more space to allow \"overflow\" scrolling (i.e. past the scrollbar)\n\t\tvar isScrolling = Math.floor(table.height()) > divBodyEl.clientHeight || divBody.css('overflow-y') == \"scroll\";\n\t\tvar paddingSide = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' );\n\t\n\t\t// Set the width's of the header and footer tables\n\t\tvar outerWidth = table.outerWidth();\n\t\n\t\tdivHeaderTable.css('width', _fnStringToCss( outerWidth ));\n\t\tdivHeaderInner\n\t\t\t.css('width', _fnStringToCss( outerWidth ))\n\t\t\t.css(paddingSide, isScrolling ? barWidth+\"px\" : \"0px\");\n\t\n\t\tif ( footer ) {\n\t\t\tdivFooterTable.css('width', _fnStringToCss( outerWidth ));\n\t\t\tdivFooterInner\n\t\t\t\t.css('width', _fnStringToCss( outerWidth ))\n\t\t\t\t.css(paddingSide, isScrolling ? barWidth+\"px\" : \"0px\");\n\t\t}\n\t\n\t\t// Correct DOM ordering for colgroup - comes before the thead\n\t\ttable.children('colgroup').prependTo(table);\n\t\n\t\t// Adjust the position of the header in case we loose the y-scrollbar\n\t\tdivBody.trigger('scroll');\n\t\n\t\t// If sorting or filtering has occurred, jump the scrolling back to the top\n\t\t// only if we aren't holding the position\n\t\tif ( (settings.bSorted || settings.bFiltered) && ! settings._drawHold ) {\n\t\t\tdivBodyEl.scrollTop = 0;\n\t\t}\n\t}\n\t\n\t/**\n\t * Calculate the width of columns for the table\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCalculateColumnWidths ( settings )\n\t{\n\t\t// Not interested in doing column width calculation if auto-width is disabled\n\t\tif (! settings.oFeatures.bAutoWidth) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\ttable = settings.nTable,\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tscroll = settings.oScroll,\n\t\t\tscrollY = scroll.sY,\n\t\t\tscrollX = scroll.sX,\n\t\t\tscrollXInner = scroll.sXInner,\n\t\t\tvisibleColumns = _fnGetColumns( settings, 'bVisible' ),\n\t\t\ttableWidthAttr = table.getAttribute('width'), // from DOM element\n\t\t\ttableContainer = table.parentNode,\n\t\t\ti, j, column, columnIdx;\n\t\t\t\n\t\tvar styleWidth = table.style.width;\n\t\tvar containerWidth = _fnWrapperWidth(settings);\n\t\n\t\t// Don't re-run for the same width as the last time\n\t\tif (containerWidth === settings.containerWidth) {\n\t\t\treturn false;\n\t\t}\n\t\n\t\tsettings.containerWidth = containerWidth;\n\t\n\t\t// If there is no width applied as a CSS style or as an attribute, we assume that\n\t\t// the width is intended to be 100%, which is usually is in CSS, but it is very\n\t\t// difficult to correctly parse the rules to get the final result.\n\t\tif ( ! styleWidth && ! tableWidthAttr) {\n\t\t\ttable.style.width = '100%';\n\t\t\tstyleWidth = '100%';\n\t\t}\n\t\n\t\tif ( styleWidth && styleWidth.indexOf('%') !== -1 ) {\n\t\t\ttableWidthAttr = styleWidth;\n\t\t}\n\t\n\t\t// Let plug-ins know that we are doing a recalc, in case they have changed any of the\n\t\t// visible columns their own way (e.g. Responsive uses display:none).\n\t\t_fnCallbackFire(\n\t\t\tsettings,\n\t\t\tnull,\n\t\t\t'column-calc',\n\t\t\t{visible: visibleColumns},\n\t\t\tfalse\n\t\t);\n\t\n\t\t// Construct a worst case table with the widest, assign any user defined\n\t\t// widths, then insert it into  the DOM and allow the browser to do all\n\t\t// the hard work of calculating table widths\n\t\tvar tmpTable = $(table.cloneNode())\n\t\t\t.css( 'visibility', 'hidden' )\n\t\t\t.css( 'margin', 0 )\n\t\t\t.removeAttr( 'id' );\n\t\n\t\t// Clean up the table body\n\t\ttmpTable.append('<tbody/>')\n\t\n\t\t// Clone the table header and footer - we can't use the header / footer\n\t\t// from the cloned table, since if scrolling is active, the table's\n\t\t// real header and footer are contained in different table tags\n\t\ttmpTable\n\t\t\t.append( $(settings.nTHead).clone() )\n\t\t\t.append( $(settings.nTFoot).clone() );\n\t\n\t\t// Remove any assigned widths from the footer (from scrolling)\n\t\ttmpTable.find('tfoot th, tfoot td').css('width', '');\n\t\n\t\t// Apply custom sizing to the cloned header\n\t\ttmpTable.find('thead th, thead td').each( function () {\n\t\t\t// Get the `width` from the header layout\n\t\t\tvar width = _fnColumnsSumWidth( settings, this, true, false );\n\t\n\t\t\tif ( width ) {\n\t\t\t\tthis.style.width = width;\n\t\n\t\t\t\t// For scrollX we need to force the column width otherwise the\n\t\t\t\t// browser will collapse it. If this width is smaller than the\n\t\t\t\t// width the column requires, then it will have no effect\n\t\t\t\tif ( scrollX ) {\n\t\t\t\t\tthis.style.minWidth = width;\n\t\n\t\t\t\t\t$( this ).append( $('<div/>').css( {\n\t\t\t\t\t\twidth: width,\n\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\tpadding: 0,\n\t\t\t\t\t\tborder: 0,\n\t\t\t\t\t\theight: 1\n\t\t\t\t\t} ) );\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.style.width = '';\n\t\t\t}\n\t\t} );\n\t\n\t\t// Get the widest strings for each of the visible columns and add them to\n\t\t// our table to create a \"worst case\"\n\t\tvar longestData = [];\n\t\n\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\tlongestData.push(_fnGetWideStrings(settings, visibleColumns[i]));\n\t\t}\n\t\n\t\tif (longestData.length) {\n\t\t\tfor ( i=0 ; i<longestData[0].length ; i++ ) {\n\t\t\t\tvar tr = $('<tr/>').appendTo( tmpTable.find('tbody') );\n\t\n\t\t\t\tfor ( j=0 ; j<visibleColumns.length ; j++ ) {\n\t\t\t\t\tcolumnIdx = visibleColumns[j];\n\t\t\t\t\tcolumn = columns[ columnIdx ];\n\t\n\t\t\t\t\tvar longest = longestData[j][i] || '';\n\t\t\t\t\tvar autoClass = _ext.type.className[column.sType];\n\t\t\t\t\tvar padding = column.sContentPadding || (scrollX ? '-' : '');\n\t\t\t\t\tvar text = longest + padding;\n\t\t\t\t\tvar insert = longest.indexOf('<') === -1 && longest.indexOf('&') === -1\n\t\t\t\t\t\t? document.createTextNode(text)\n\t\t\t\t\t\t: text\n\t\n\t\t\t\t\t$('<td/>')\n\t\t\t\t\t\t.addClass(autoClass)\n\t\t\t\t\t\t.addClass(column.sClass)\n\t\t\t\t\t\t.append(insert)\n\t\t\t\t\t\t.appendTo(tr);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Tidy the temporary table - remove name attributes so there aren't\n\t\t// duplicated in the dom (radio elements for example)\n\t\t$('[name]', tmpTable).removeAttr('name');\n\t\n\t\t// Table has been built, attach to the document so we can work with it.\n\t\t// A holding element is used, positioned at the top of the container\n\t\t// with minimal height, so it has no effect on if the container scrolls\n\t\t// or not. Otherwise it might trigger scrolling when it actually isn't\n\t\t// needed\n\t\tvar holder = $('<div/>').css( scrollX || scrollY ?\n\t\t\t\t{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\ttop: 0,\n\t\t\t\t\tleft: 0,\n\t\t\t\t\theight: 1,\n\t\t\t\t\tright: 0,\n\t\t\t\t\toverflow: 'hidden'\n\t\t\t\t} :\n\t\t\t\t{}\n\t\t\t)\n\t\t\t.append( tmpTable )\n\t\t\t.appendTo( tableContainer );\n\t\n\t\t// When scrolling (X or Y) we want to set the width of the table as \n\t\t// appropriate. However, when not scrolling leave the table width as it\n\t\t// is. This results in slightly different, but I think correct behaviour\n\t\tif ( scrollX && scrollXInner ) {\n\t\t\ttmpTable.width( scrollXInner );\n\t\t}\n\t\telse if ( scrollX ) {\n\t\t\ttmpTable.css( 'width', 'auto' );\n\t\t\ttmpTable.removeAttr('width');\n\t\n\t\t\t// If there is no width attribute or style, then allow the table to\n\t\t\t// collapse\n\t\t\tif ( tmpTable.outerWidth() < tableContainer.clientWidth && tableWidthAttr ) {\n\t\t\t\ttmpTable.outerWidth( tableContainer.clientWidth );\n\t\t\t}\n\t\t}\n\t\telse if ( scrollY ) {\n\t\t\ttmpTable.outerWidth( tableContainer.clientWidth );\n\t\t}\n\t\telse if ( tableWidthAttr ) {\n\t\t\ttmpTable.outerWidth( tableWidthAttr );\n\t\t}\n\t\n\t\t// Get the width of each column in the constructed table\n\t\tvar total = 0;\n\t\tvar bodyCells = tmpTable.find('tbody tr').eq(0).children();\n\t\n\t\tfor ( i=0 ; i<visibleColumns.length ; i++ ) {\n\t\t\t// Use getBounding for sub-pixel accuracy, which we then want to round up!\n\t\t\tvar bounding = bodyCells[i].getBoundingClientRect().width;\n\t\n\t\t\t// Total is tracked to remove any sub-pixel errors as the outerWidth\n\t\t\t// of the table might not equal the total given here\n\t\t\ttotal += bounding;\n\t\n\t\t\t// Width for each column to use\n\t\t\tcolumns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding );\n\t\t}\n\t\n\t\ttable.style.width = _fnStringToCss( total );\n\t\n\t\t// Finished with the table - ditch it\n\t\tholder.remove();\n\t\n\t\t// If there is a width attr, we want to attach an event listener which\n\t\t// allows the table sizing to automatically adjust when the window is\n\t\t// resized. Use the width attr rather than CSS, since we can't know if the\n\t\t// CSS is a relative value or absolute - DOM read is always px.\n\t\tif ( tableWidthAttr ) {\n\t\t\ttable.style.width = _fnStringToCss( tableWidthAttr );\n\t\t}\n\t\n\t\tif ( (tableWidthAttr || scrollX) && ! settings._reszEvt ) {\n\t\t\tvar resize = DataTable.util.throttle( function () {\n\t\t\t\tvar newWidth = _fnWrapperWidth(settings);\n\t\n\t\t\t\t// Don't do it if destroying or the container width is 0\n\t\t\t\tif (! settings.bDestroying && newWidth !== 0) {\n\t\t\t\t\t_fnAdjustColumnSizing( settings );\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\t// For browsers that support it (~2020 onwards for wide support) we can watch for the\n\t\t\t// container changing width.\n\t\t\tif (window.ResizeObserver) {\n\t\t\t\t// This is a tricky beast - if the element is visible when `.observe()` is called,\n\t\t\t\t// then the callback is immediately run. Which we don't want. If the element isn't\n\t\t\t\t// visible, then it isn't run, but we want it to run when it is then made visible.\n\t\t\t\t// This flag allows the above to be satisfied.\n\t\t\t\tvar first = $(settings.nTableWrapper).is(':visible');\n\t\n\t\t\t\t// Use an empty div to attach the observer so it isn't impacted by height changes\n\t\t\t\tvar resizer = $('<div>')\n\t\t\t\t\t.css({\n\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\theight: 0\n\t\t\t\t\t})\n\t\t\t\t\t.addClass('dt-autosize')\n\t\t\t\t\t.appendTo(settings.nTableWrapper);\n\t\n\t\t\t\tsettings.resizeObserver = new ResizeObserver(function (e) {\n\t\t\t\t\tif (first) {\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresize();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\n\t\t\t\tsettings.resizeObserver.observe(resizer[0]);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// For old browsers, the best we can do is listen for a window resize\n\t\t\t\t$(window).on('resize.DT-'+settings.sInstance, resize);\n\t\t\t}\n\t\n\t\t\tsettings._reszEvt = true;\n\t\t}\n\t}\n\t\n\t/**\n\t * Get the width of the DataTables wrapper element\n\t *\n\t * @param {*} settings DataTables settings object\n\t * @returns Width\n\t */\n\tfunction _fnWrapperWidth(settings) {\n\t\treturn $(settings.nTableWrapper).is(':visible')\n\t\t\t? $(settings.nTableWrapper).width()\n\t\t\t: 0;\n\t}\n\t\n\t/**\n\t * Get the widest strings for each column.\n\t *\n\t * It is very difficult to determine what the widest string actually is due to variable character\n\t * width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance\n\t * and this is a critical point, so we use two techniques to determine a collection of the longest\n\t * strings from the column, which will likely contain the widest strings:\n\t *\n\t * 1) Get the top three longest strings from the column\n\t * 2) Get the top three widest words (i.e. an unbreakable phrase)\n\t *\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} colIdx column of interest\n\t *  @returns {string[]} Array of the longest strings\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnGetWideStrings( settings, colIdx )\n\t{\n\t\tvar column = settings.aoColumns[colIdx];\n\t\n\t\t// Do we need to recalculate (i.e. was invalidated), or just use the cached data?\n\t\tif (! column.wideStrings) {\n\t\t\tvar allStrings = [];\n\t\t\tvar collection = [];\n\t\n\t\t\t// Create an array with the string information for the column\n\t\t\tfor ( var i=0, iLen=settings.aiDisplayMaster.length ; i<iLen ; i++ ) {\n\t\t\t\tvar rowIdx = settings.aiDisplayMaster[i];\n\t\t\t\tvar data = _fnGetRowDisplay(settings, rowIdx)[colIdx];\n\t\n\t\t\t\tvar cellString = data && typeof data === 'object' && data.nodeType\n\t\t\t\t\t? data.innerHTML\n\t\t\t\t\t: data+'';\n\t\n\t\t\t\t// Remove id / name attributes from elements so they\n\t\t\t\t// don't interfere with existing elements\n\t\t\t\tcellString = cellString\n\t\t\t\t\t.replace(/id=\".*?\"/g, '')\n\t\t\t\t\t.replace(/name=\".*?\"/g, '');\n\t\n\t\t\t\t// Don't want Javascript at all in these calculation cells.\n\t\t\t\tcellString = cellString.replace(/<script.*?<\\/script>/gi, ' ');\n\t\n\t\t\t\tvar noHtml = _stripHtml(cellString, ' ')\n\t\t\t\t\t.replace( /&nbsp;/g, ' ' );\n\t\t\n\t\t\t\t// The length is calculated on the text only, but we keep the HTML\n\t\t\t\t// in the string so it can be used in the calculation table\n\t\t\t\tcollection.push({\n\t\t\t\t\tstr: cellString,\n\t\t\t\t\tlen: noHtml.length\n\t\t\t\t});\n\t\n\t\t\t\tallStrings.push(noHtml);\n\t\t\t}\n\t\n\t\t\t// Order and then cut down to the size we need\n\t\t\tcollection\n\t\t\t\t.sort(function (a, b) {\n\t\t\t\t\treturn b.len - a.len;\n\t\t\t\t})\n\t\t\t\t.splice(3);\n\t\n\t\t\tcolumn.wideStrings = collection.map(function (item) {\n\t\t\t\treturn item.str;\n\t\t\t});\n\t\n\t\t\t// Longest unbroken string\n\t\t\tlet parts = allStrings.join(' ').split(' ');\n\t\n\t\t\tparts.sort(function (a, b) {\n\t\t\t\treturn b.length - a.length;\n\t\t\t});\n\t\n\t\t\tif (parts.length) {\n\t\t\t\tcolumn.wideStrings.push(parts[0]);\n\t\t\t}\n\t\n\t\t\tif (parts.length > 1) {\n\t\t\t\tcolumn.wideStrings.push(parts[1]);\n\t\t\t}\n\t\n\t\t\tif (parts.length > 2) {\n\t\t\t\tcolumn.wideStrings.push(parts[3]);\n\t\t\t}\n\t\t}\n\t\n\t\treturn column.wideStrings;\n\t}\n\t\n\t\n\t/**\n\t * Append a CSS unit (only if required) to a string\n\t *  @param {string} value to css-ify\n\t *  @returns {string} value with css unit\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnStringToCss( s )\n\t{\n\t\tif ( s === null ) {\n\t\t\treturn '0px';\n\t\t}\n\t\n\t\tif ( typeof s == 'number' ) {\n\t\t\treturn s < 0 ?\n\t\t\t\t'0px' :\n\t\t\t\ts+'px';\n\t\t}\n\t\n\t\t// Check it has a unit character already\n\t\treturn s.match(/\\d$/) ?\n\t\t\ts+'px' :\n\t\t\ts;\n\t}\n\t\n\t/**\n\t * Re-insert the `col` elements for current visibility\n\t *\n\t * @param {*} settings DT settings\n\t */\n\tfunction _colGroup( settings ) {\n\t\tvar cols = settings.aoColumns;\n\t\n\t\tsettings.colgroup.empty();\n\t\n\t\tfor (i=0 ; i<cols.length ; i++) {\n\t\t\tif (cols[i].bVisible) {\n\t\t\t\tsettings.colgroup.append(cols[i].colEl);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\tfunction _fnSortInit( settings ) {\n\t\tvar target = settings.nTHead;\n\t\tvar headerRows = target.querySelectorAll('tr');\n\t\tvar titleRow = settings.titleRow;\n\t\tvar notSelector = ':not([data-dt-order=\"disable\"]):not([data-dt-order=\"icon-only\"])';\n\t\t\n\t\t// Legacy support for `orderCellsTop`\n\t\tif (titleRow === true) {\n\t\t\ttarget = headerRows[0];\n\t\t}\n\t\telse if (titleRow === false) {\n\t\t\ttarget = headerRows[ headerRows.length - 1 ];\n\t\t}\n\t\telse if (titleRow !== null) {\n\t\t\ttarget = headerRows[titleRow];\n\t\t}\n\t\t// else - all rows\n\t\n\t\tif (settings.orderHandler) {\n\t\t\t_fnSortAttachListener(\n\t\t\t\tsettings,\n\t\t\t\ttarget,\n\t\t\t\ttarget === settings.nTHead\n\t\t\t\t\t? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector\n\t\t\t\t\t: 'th'+notSelector+', td'+notSelector\n\t\t\t);\n\t\t}\n\t\n\t\t// Need to resolve the user input array into our internal structure\n\t\tvar order = [];\n\t\t_fnSortResolve( settings, order, settings.aaSorting );\n\t\n\t\tsettings.aaSorting = order;\n\t}\n\t\n\t\n\tfunction _fnSortAttachListener(settings, node, selector, column, callback) {\n\t\t_fnBindAction( node, selector, function (e) {\n\t\t\tvar run = false;\n\t\t\tvar columns = column === undefined\n\t\t\t\t? _fnColumnsFromHeader( e.target )\n\t\t\t\t: typeof column === 'function'\n\t\t\t\t\t? column()\n\t\t\t\t\t: Array.isArray(column)\n\t\t\t\t\t\t? column\n\t\t\t\t\t\t: [column];\n\t\n\t\t\tif ( columns.length ) {\n\t\t\t\tfor ( var i=0, iLen=columns.length ; i<iLen ; i++ ) {\n\t\t\t\t\tvar ret = _fnSortAdd( settings, columns[i], i, e.shiftKey );\n\t\n\t\t\t\t\tif (ret !== false) {\n\t\t\t\t\t\trun = true;\n\t\t\t\t\t}\t\t\t\t\t\n\t\n\t\t\t\t\t// If the first entry is no sort, then subsequent\n\t\t\t\t\t// sort columns are ignored\n\t\t\t\t\tif (settings.aaSorting.length === 1 && settings.aaSorting[0][1] === '') {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\tif (run) {\n\t\t\t\t\t_fnProcessingRun(settings, true, function () {\n\t\t\t\t\t\t_fnSort( settings );\n\t\t\t\t\t\t_fnSortDisplay( settings, settings.aiDisplay );\n\t\n\t\t\t\t\t\t_fnReDraw( settings, false, false );\n\t\n\t\t\t\t\t\tif (callback) {\n\t\t\t\t\t\t\tcallback();\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t}\n\t\n\t/**\n\t * Sort the display array to match the master's order\n\t * @param {*} settings\n\t */\n\tfunction _fnSortDisplay(settings, display) {\n\t\tif (display.length < 2) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar master = settings.aiDisplayMaster;\n\t\tvar masterMap = {};\n\t\tvar map = {};\n\t\tvar i;\n\t\n\t\t// Rather than needing an `indexOf` on master array, we can create a map\n\t\tfor (i=0 ; i<master.length ; i++) {\n\t\t\tmasterMap[master[i]] = i;\n\t\t}\n\t\n\t\t// And then cache what would be the indexOf from the display\n\t\tfor (i=0 ; i<display.length ; i++) {\n\t\t\tmap[display[i]] = masterMap[display[i]];\n\t\t}\n\t\n\t\tdisplay.sort(function(a, b){\n\t\t\t// Short version of this function is simply `master.indexOf(a) - master.indexOf(b);`\n\t\t\treturn map[a] - map[b];\n\t\t});\n\t}\n\t\n\t\n\tfunction _fnSortResolve (settings, nestedSort, sort) {\n\t\tvar push = function ( a ) {\n\t\t\tif ($.isPlainObject(a)) {\n\t\t\t\tif (a.idx !== undefined) {\n\t\t\t\t\t// Index based ordering\n\t\t\t\t\tnestedSort.push([a.idx, a.dir]);\n\t\t\t\t}\n\t\t\t\telse if (a.name) {\n\t\t\t\t\t// Name based ordering\n\t\t\t\t\tvar cols = _pluck( settings.aoColumns, 'sName');\n\t\t\t\t\tvar idx = cols.indexOf(a.name);\n\t\n\t\t\t\t\tif (idx !== -1) {\n\t\t\t\t\t\tnestedSort.push([idx, a.dir]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Plain column index and direction pair\n\t\t\t\tnestedSort.push(a);\n\t\t\t}\n\t\t};\n\t\n\t\tif ( $.isPlainObject(sort) ) {\n\t\t\t// Object\n\t\t\tpush(sort);\n\t\t}\n\t\telse if ( sort.length && typeof sort[0] === 'number' ) {\n\t\t\t// 1D array\n\t\t\tpush(sort);\n\t\t}\n\t\telse if ( sort.length ) {\n\t\t\t// 2D array\n\t\t\tfor (var z=0; z<sort.length; z++) {\n\t\t\t\tpush(sort[z]); // Object or array\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\tfunction _fnSortFlatten ( settings )\n\t{\n\t\tvar\n\t\t\ti, k, kLen,\n\t\t\taSort = [],\n\t\t\textSort = DataTable.ext.type.order,\n\t\t\taoColumns = settings.aoColumns,\n\t\t\taDataSort, iCol, sType, srcCol,\n\t\t\tfixed = settings.aaSortingFixed,\n\t\t\tfixedObj = $.isPlainObject( fixed ),\n\t\t\tnestedSort = [];\n\t\t\n\t\tif ( ! settings.oFeatures.bSort ) {\n\t\t\treturn aSort;\n\t\t}\n\t\n\t\t// Build the sort array, with pre-fix and post-fix options if they have been\n\t\t// specified\n\t\tif ( Array.isArray( fixed ) ) {\n\t\t\t_fnSortResolve( settings, nestedSort, fixed );\n\t\t}\n\t\n\t\tif ( fixedObj && fixed.pre ) {\n\t\t\t_fnSortResolve( settings, nestedSort, fixed.pre );\n\t\t}\n\t\n\t\t_fnSortResolve( settings, nestedSort, settings.aaSorting );\n\t\n\t\tif (fixedObj && fixed.post ) {\n\t\t\t_fnSortResolve( settings, nestedSort, fixed.post );\n\t\t}\n\t\n\t\tfor ( i=0 ; i<nestedSort.length ; i++ )\n\t\t{\n\t\t\tsrcCol = nestedSort[i][0];\n\t\n\t\t\tif ( aoColumns[ srcCol ] ) {\n\t\t\t\taDataSort = aoColumns[ srcCol ].aDataSort;\n\t\n\t\t\t\tfor ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )\n\t\t\t\t{\n\t\t\t\t\tiCol = aDataSort[k];\n\t\t\t\t\tsType = aoColumns[ iCol ].sType || 'string';\n\t\n\t\t\t\t\tif ( nestedSort[i]._idx === undefined ) {\n\t\t\t\t\t\tnestedSort[i]._idx = aoColumns[iCol].asSorting.indexOf(nestedSort[i][1]);\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif ( nestedSort[i][1] ) {\n\t\t\t\t\t\taSort.push( {\n\t\t\t\t\t\t\tsrc:       srcCol,\n\t\t\t\t\t\t\tcol:       iCol,\n\t\t\t\t\t\t\tdir:       nestedSort[i][1],\n\t\t\t\t\t\t\tindex:     nestedSort[i]._idx,\n\t\t\t\t\t\t\ttype:      sType,\n\t\t\t\t\t\t\tformatter: extSort[ sType+\"-pre\" ],\n\t\t\t\t\t\t\tsorter:    extSort[ sType+\"-\"+nestedSort[i][1] ]\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn aSort;\n\t}\n\t\n\t/**\n\t * Change the order of the table\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSort ( oSettings, col, dir )\n\t{\n\t\tvar\n\t\t\ti, iLen,\n\t\t\taiOrig = [],\n\t\t\textSort = DataTable.ext.type.order,\n\t\t\taoData = oSettings.aoData,\n\t\t\tsortCol,\n\t\t\tdisplayMaster = oSettings.aiDisplayMaster,\n\t\t\taSort;\n\t\n\t\t// Make sure the columns all have types defined\n\t\t_fnColumnTypes(oSettings);\n\t\n\t\t// Allow a specific column to be sorted, which will _not_ alter the display\n\t\t// master\n\t\tif (col !== undefined) {\n\t\t\tvar srcCol = oSettings.aoColumns[col];\n\t\n\t\t\taSort = [{\n\t\t\t\tsrc:       col,\n\t\t\t\tcol:       col,\n\t\t\t\tdir:       dir,\n\t\t\t\tindex:     0,\n\t\t\t\ttype:      srcCol.sType,\n\t\t\t\tformatter: extSort[ srcCol.sType+\"-pre\" ],\n\t\t\t\tsorter:    extSort[ srcCol.sType+\"-\"+dir ]\n\t\t\t}];\n\t\t\tdisplayMaster = displayMaster.slice();\n\t\t}\n\t\telse {\n\t\t\taSort = _fnSortFlatten( oSettings );\n\t\t}\n\t\n\t\tfor ( i=0, iLen=aSort.length ; i<iLen ; i++ ) {\n\t\t\tsortCol = aSort[i];\n\t\n\t\t\t// Load the data needed for the sort, for each cell\n\t\t\t_fnSortData( oSettings, sortCol.col );\n\t\t}\n\t\n\t\t/* No sorting required if server-side or no sorting array */\n\t\tif ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 )\n\t\t{\n\t\t\t// Reset the initial positions on each pass so we get a stable sort\n\t\t\tfor ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) {\n\t\t\t\taiOrig[ i ] = i;\n\t\t\t}\n\t\n\t\t\t// If the first sort is desc, then reverse the array to preserve original\n\t\t\t// order, just in reverse\n\t\t\tif (aSort.length && aSort[0].dir === 'desc' && oSettings.orderDescReverse) {\n\t\t\t\taiOrig.reverse();\n\t\t\t}\n\t\n\t\t\t/* Do the sort - here we want multi-column sorting based on a given data source (column)\n\t\t\t * and sorting function (from oSort) in a certain direction. It's reasonably complex to\n\t\t\t * follow on its own, but this is what we want (example two column sorting):\n\t\t\t *  fnLocalSorting = function(a,b){\n\t\t\t *    var test;\n\t\t\t *    test = oSort['string-asc']('data11', 'data12');\n\t\t\t *      if (test !== 0)\n\t\t\t *        return test;\n\t\t\t *    test = oSort['numeric-desc']('data21', 'data22');\n\t\t\t *    if (test !== 0)\n\t\t\t *      return test;\n\t\t\t *    return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );\n\t\t\t *  }\n\t\t\t * Basically we have a test for each sorting column, if the data in that column is equal,\n\t\t\t * test the next column. If all columns match, then we use a numeric sort on the row\n\t\t\t * positions in the original data array to provide a stable sort.\n\t\t\t */\n\t\t\tdisplayMaster.sort( function ( a, b ) {\n\t\t\t\tvar\n\t\t\t\t\tx, y, k, test, sort,\n\t\t\t\t\tlen=aSort.length,\n\t\t\t\t\tdataA = aoData[a]._aSortData,\n\t\t\t\t\tdataB = aoData[b]._aSortData;\n\t\n\t\t\t\tfor ( k=0 ; k<len ; k++ ) {\n\t\t\t\t\tsort = aSort[k];\n\t\n\t\t\t\t\t// Data, which may have already been through a `-pre` function\n\t\t\t\t\tx = dataA[ sort.col ];\n\t\t\t\t\ty = dataB[ sort.col ];\n\t\n\t\t\t\t\tif (sort.sorter) {\n\t\t\t\t\t\t// If there is a custom sorter (`-asc` or `-desc`) for this\n\t\t\t\t\t\t// data type, use it\n\t\t\t\t\t\ttest = sort.sorter(x, y);\n\t\n\t\t\t\t\t\tif ( test !== 0 ) {\n\t\t\t\t\t\t\treturn test;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// Otherwise, use generic sorting\n\t\t\t\t\t\ttest = x<y ? -1 : x>y ? 1 : 0;\n\t\n\t\t\t\t\t\tif ( test !== 0 ) {\n\t\t\t\t\t\t\treturn sort.dir === 'asc' ? test : -test;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\tx = aiOrig[a];\n\t\t\t\ty = aiOrig[b];\n\t\n\t\t\t\treturn x<y ? -1 : x>y ? 1 : 0;\n\t\t\t} );\n\t\t}\n\t\telse if ( aSort.length === 0 ) {\n\t\t\t// Apply index order\n\t\t\tdisplayMaster.sort(function (x, y) {\n\t\t\t\treturn x<y ? -1 : x>y ? 1 : 0;\n\t\t\t});\n\t\t}\n\t\n\t\tif (col === undefined) {\n\t\t\t// Tell the draw function that we have sorted the data\n\t\t\toSettings.bSorted = true;\n\t\t\toSettings.sortDetails = aSort;\n\t\n\t\t\t_fnCallbackFire( oSettings, null, 'order', [oSettings, aSort] );\n\t\t}\n\t\n\t\treturn displayMaster;\n\t}\n\t\n\t\n\t/**\n\t * Function to run on user sort request\n\t *  @param {object} settings dataTables settings object\n\t *  @param {node} attachTo node to attach the handler to\n\t *  @param {int} colIdx column sorting index\n\t *  @param {int} addIndex Counter\n\t *  @param {boolean} [shift=false] Shift click add\n\t *  @param {function} [callback] callback function\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSortAdd ( settings, colIdx, addIndex, shift )\n\t{\n\t\tvar col = settings.aoColumns[ colIdx ];\n\t\tvar sorting = settings.aaSorting;\n\t\tvar asSorting = col.asSorting;\n\t\tvar nextSortIdx;\n\t\tvar next = function ( a, overflow ) {\n\t\t\tvar idx = a._idx;\n\t\t\tif ( idx === undefined ) {\n\t\t\t\tidx = asSorting.indexOf(a[1]);\n\t\t\t}\n\t\n\t\t\treturn idx+1 < asSorting.length ?\n\t\t\t\tidx+1 :\n\t\t\t\toverflow ?\n\t\t\t\t\tnull :\n\t\t\t\t\t0;\n\t\t};\n\t\n\t\tif ( ! col.bSortable ) {\n\t\t\treturn false;\n\t\t}\n\t\n\t\t// Convert to 2D array if needed\n\t\tif ( typeof sorting[0] === 'number' ) {\n\t\t\tsorting = settings.aaSorting = [ sorting ];\n\t\t}\n\t\n\t\t// If appending the sort then we are multi-column sorting\n\t\tif ( (shift || addIndex) && settings.oFeatures.bSortMulti ) {\n\t\t\t// Are we already doing some kind of sort on this column?\n\t\t\tvar sortIdx = _pluck(sorting, '0').indexOf(colIdx);\n\t\n\t\t\tif ( sortIdx !== -1 ) {\n\t\t\t\t// Yes, modify the sort\n\t\t\t\tnextSortIdx = next( sorting[sortIdx], true );\n\t\n\t\t\t\tif ( nextSortIdx === null && sorting.length === 1 ) {\n\t\t\t\t\tnextSortIdx = 0; // can't remove sorting completely\n\t\t\t\t}\n\t\n\t\t\t\tif ( nextSortIdx === null || asSorting[ nextSortIdx ] === '' ) {\n\t\t\t\t\tsorting.splice( sortIdx, 1 );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tsorting[sortIdx][1] = asSorting[ nextSortIdx ];\n\t\t\t\t\tsorting[sortIdx]._idx = nextSortIdx;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (shift) {\n\t\t\t\t// No sort on this column yet, being added by shift click\n\t\t\t\t// add it as itself\n\t\t\t\tsorting.push( [ colIdx, asSorting[0], 0 ] );\n\t\t\t\tsorting[sorting.length-1]._idx = 0;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// No sort on this column yet, being added from a colspan\n\t\t\t\t// so add with same direction as first column\n\t\t\t\tsorting.push( [ colIdx, sorting[0][1], 0 ] );\n\t\t\t\tsorting[sorting.length-1]._idx = 0;\n\t\t\t}\n\t\t}\n\t\telse if ( sorting.length && sorting[0][0] == colIdx ) {\n\t\t\t// Single column - already sorting on this column, modify the sort\n\t\t\tnextSortIdx = next( sorting[0] );\n\t\n\t\t\tsorting.length = 1;\n\t\t\tsorting[0][1] = asSorting[ nextSortIdx ];\n\t\t\tsorting[0]._idx = nextSortIdx;\n\t\t}\n\t\telse {\n\t\t\t// Single column - sort only on this column\n\t\t\tsorting.length = 0;\n\t\t\tsorting.push( [ colIdx, asSorting[0] ] );\n\t\t\tsorting[0]._idx = 0;\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Set the sorting classes on table's body, Note: it is safe to call this function\n\t * when bSort and bSortClasses are false\n\t *  @param {object} oSettings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnSortingClasses( settings )\n\t{\n\t\tvar oldSort = settings.aLastSort;\n\t\tvar sortClass = settings.oClasses.order.position;\n\t\tvar sort = _fnSortFlatten( settings );\n\t\tvar features = settings.oFeatures;\n\t\tvar i, iLen, colIdx;\n\t\n\t\tif ( features.bSort && features.bSortClasses ) {\n\t\t\t// Remove old sorting classes\n\t\t\tfor ( i=0, iLen=oldSort.length ; i<iLen ; i++ ) {\n\t\t\t\tcolIdx = oldSort[i].src;\n\t\n\t\t\t\t// Remove column sorting\n\t\t\t\t$( _pluck( settings.aoData, 'anCells', colIdx ) )\n\t\t\t\t\t.removeClass( sortClass + (i<2 ? i+1 : 3) );\n\t\t\t}\n\t\n\t\t\t// Add new column sorting\n\t\t\tfor ( i=0, iLen=sort.length ; i<iLen ; i++ ) {\n\t\t\t\tcolIdx = sort[i].src;\n\t\n\t\t\t\t$( _pluck( settings.aoData, 'anCells', colIdx ) )\n\t\t\t\t\t.addClass( sortClass + (i<2 ? i+1 : 3) );\n\t\t\t}\n\t\t}\n\t\n\t\tsettings.aLastSort = sort;\n\t}\n\t\n\t\n\t// Get the data to sort a column, be it from cache, fresh (populating the\n\t// cache), or from a sort formatter\n\tfunction _fnSortData( settings, colIdx )\n\t{\n\t\t// Custom sorting function - provided by the sort data type\n\t\tvar column = settings.aoColumns[ colIdx ];\n\t\tvar customSort = DataTable.ext.order[ column.sSortDataType ];\n\t\tvar customData;\n\t\n\t\tif ( customSort ) {\n\t\t\tcustomData = customSort.call( settings.oInstance, settings, colIdx,\n\t\t\t\t_fnColumnIndexToVisible( settings, colIdx )\n\t\t\t);\n\t\t}\n\t\n\t\t// Use / populate cache\n\t\tvar row, cellData;\n\t\tvar formatter = DataTable.ext.type.order[ column.sType+\"-pre\" ];\n\t\tvar data = settings.aoData;\n\t\n\t\tfor ( var rowIdx=0 ; rowIdx<data.length ; rowIdx++ ) {\n\t\t\t// Sparse array\n\t\t\tif (! data[rowIdx]) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\trow = data[rowIdx];\n\t\n\t\t\tif ( ! row._aSortData ) {\n\t\t\t\trow._aSortData = [];\n\t\t\t}\n\t\n\t\t\tif ( ! row._aSortData[colIdx] || customSort ) {\n\t\t\t\tcellData = customSort ?\n\t\t\t\t\tcustomData[rowIdx] : // If there was a custom sort function, use data from there\n\t\t\t\t\t_fnGetCellData( settings, rowIdx, colIdx, 'sort' );\n\t\n\t\t\t\trow._aSortData[ colIdx ] = formatter ?\n\t\t\t\t\tformatter( cellData, settings ) :\n\t\t\t\t\tcellData;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * State information for a table\n\t *\n\t * @param {*} settings\n\t * @returns State object\n\t */\n\tfunction _fnSaveState ( settings )\n\t{\n\t\tif (settings._bLoadingState) {\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Sort state saving uses [[idx, order]] structure.\n\t\tvar sorting = [];\n\t\t_fnSortResolve(settings, sorting, settings.aaSorting );\n\t\n\t\t/* Store the interesting variables */\n\t\tvar columns = settings.aoColumns;\n\t\tvar state = {\n\t\t\ttime:    +new Date(),\n\t\t\tstart:   settings._iDisplayStart,\n\t\t\tlength:  settings._iDisplayLength,\n\t\t\torder:   sorting.map(function (sort) {\n\t\t\t\t// If a column name is available, use it\n\t\t\t\treturn columns[sort[0]] && columns[sort[0]].sName\n\t\t\t\t\t? [ columns[sort[0]].sName, sort[1] ]\n\t\t\t\t\t: sort.slice();\n\t\t\t} ),\n\t\t\tsearch:  $.extend({}, settings.oPreviousSearch),\n\t\t\tcolumns: settings.aoColumns.map( function ( col, i ) {\n\t\t\t\treturn {\n\t\t\t\t\tname: col.sName,\n\t\t\t\t\tvisible: col.bVisible,\n\t\t\t\t\tsearch: $.extend({}, settings.aoPreSearchCols[i])\n\t\t\t\t};\n\t\t\t} )\n\t\t};\n\t\n\t\tsettings.oSavedState = state;\n\t\t_fnCallbackFire( settings, \"aoStateSaveParams\", 'stateSaveParams', [settings, state] );\n\t\t\n\t\tif ( settings.oFeatures.bStateSave && !settings.bDestroying )\n\t\t{\n\t\t\tsettings.fnStateSaveCallback.call( settings.oInstance, settings, state );\n\t\t}\t\n\t}\n\t\n\t\n\t/**\n\t * Attempt to load a saved table state\n\t *  @param {object} oSettings dataTables settings object\n\t *  @param {object} oInit DataTables init object so we can override settings\n\t *  @param {function} callback Callback to execute when the state has been loaded\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnLoadState ( settings, init, callback )\n\t{\n\t\tif ( ! settings.oFeatures.bStateSave ) {\n\t\t\tcallback();\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar loaded = function(state) {\n\t\t\t_fnImplementState(settings, state, callback);\n\t\t}\n\t\n\t\tvar state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );\n\t\n\t\tif ( state !== undefined ) {\n\t\t\t_fnImplementState( settings, state, callback );\n\t\t}\n\t\t// otherwise, wait for the loaded callback to be executed\n\t\n\t\treturn true;\n\t}\n\t\n\tfunction _fnImplementState ( settings, s, callback) {\n\t\tvar i, iLen;\n\t\tvar columns = settings.aoColumns;\n\t\tvar currentNames = _pluck(settings.aoColumns, 'sName');\n\t\n\t\tsettings._bLoadingState = true;\n\t\n\t\t// When StateRestore was introduced the state could now be implemented at any time\n\t\t// Not just initialisation. To do this an api instance is required in some places\n\t\tvar api = settings._bInitComplete ? new DataTable.Api(settings) : null;\n\t\n\t\tif ( ! s || ! s.time ) {\n\t\t\tsettings._bLoadingState = false;\n\t\t\tcallback();\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Reject old data\n\t\tvar duration = settings.iStateDuration;\n\t\tif ( duration > 0 && s.time < +new Date() - (duration*1000) ) {\n\t\t\tsettings._bLoadingState = false;\n\t\t\tcallback();\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Allow custom and plug-in manipulation functions to alter the saved data set and\n\t\t// cancelling of loading by returning false\n\t\tvar abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );\n\t\tif ( abStateLoad.indexOf(false) !== -1 ) {\n\t\t\tsettings._bLoadingState = false;\n\t\t\tcallback();\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Store the saved state so it might be accessed at any time\n\t\tsettings.oLoadedState = $.extend( true, {}, s );\n\t\n\t\t// This is needed for ColReorder, which has to happen first to allow all\n\t\t// the stored indexes to be usable. It is not publicly documented.\n\t\t_fnCallbackFire( settings, null, 'stateLoadInit', [settings, s], true );\n\t\n\t\t// Page Length\n\t\tif ( s.length !== undefined ) {\n\t\t\t// If already initialised just set the value directly so that the select element is also updated\n\t\t\tif (api) {\n\t\t\t\tapi.page.len(s.length)\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsettings._iDisplayLength   = s.length;\n\t\t\t}\n\t\t}\n\t\n\t\t// Restore key features\n\t\tif ( s.start !== undefined ) {\n\t\t\tif(api === null) {\n\t\t\t\tsettings._iDisplayStart    = s.start;\n\t\t\t\tsettings.iInitDisplayStart = s.start;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t_fnPageChange(settings, s.start/settings._iDisplayLength);\n\t\t\t}\n\t\t}\n\t\n\t\t// Order\n\t\tif ( s.order !== undefined ) {\n\t\t\tsettings.aaSorting = [];\n\t\t\t$.each( s.order, function ( i, col ) {\n\t\t\t\tvar set = [ col[0], col[1] ];\n\t\n\t\t\t\t// A column name was stored and should be used for restore\n\t\t\t\tif (typeof col[0] === 'string') {\n\t\t\t\t\t// Find the name from the current list of column names\n\t\t\t\t\tvar idx = currentNames.indexOf(col[0]);\n\t\n\t\t\t\t\tif (idx < 0) {\n\t\t\t\t\t\t// If the column was not found ignore it and continue\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tset[0] = idx;\n\t\t\t\t}\n\t\t\t\telse if (set[0] >= columns.length) {\n\t\t\t\t\t// If the column index is out of bounds ignore it and continue\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tsettings.aaSorting.push(set);\n\t\t\t} );\n\t\t}\n\t\n\t\t// Search\n\t\tif ( s.search !== undefined ) {\n\t\t\t$.extend( settings.oPreviousSearch, s.search );\n\t\t}\n\t\n\t\t// Columns\n\t\tif ( s.columns ) {\n\t\t\tvar set = s.columns;\n\t\t\tvar incoming = _pluck(s.columns, 'name');\n\t\n\t\t\t// Check if it is a 2.2 style state object with a `name` property for the columns, and if\n\t\t\t// the name was defined. If so, then create a new array that will map the state object\n\t\t\t// given, to the current columns (don't bother if they are already matching tho).\n\t\t\tif (incoming.join('').length && incoming.join('') !== currentNames.join('')) {\n\t\t\t\tset = [];\n\t\n\t\t\t\t// For each column, try to find the name in the incoming array\n\t\t\t\tfor (i=0 ; i<currentNames.length ; i++) {\n\t\t\t\t\tif (currentNames[i] != '') {\n\t\t\t\t\t\tvar idx = incoming.indexOf(currentNames[i]);\n\t\n\t\t\t\t\t\tif (idx >= 0) {\n\t\t\t\t\t\t\tset.push(s.columns[idx]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t// No matching column name in the state's columns, so this might be a new\n\t\t\t\t\t\t\t// column and thus can't have a state already.\n\t\t\t\t\t\t\tset.push({});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// If no name, but other columns did have a name, then there is no knowing\n\t\t\t\t\t\t// where this one came from originally so it can't be restored.\n\t\t\t\t\t\tset.push({});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// If the number of columns to restore is different from current, then all bets are off.\n\t\t\tif (set.length === columns.length) {\n\t\t\t\tfor ( i=0, iLen=set.length ; i<iLen ; i++ ) {\n\t\t\t\t\tvar col = set[i];\n\t\n\t\t\t\t\t// Visibility\n\t\t\t\t\tif ( col.visible !== undefined ) {\n\t\t\t\t\t\t// If the api is defined, the table has been initialised so we need to use it rather than internal settings\n\t\t\t\t\t\tif (api) {\n\t\t\t\t\t\t\t// Don't redraw the columns on every iteration of this loop, we will do this at the end instead\n\t\t\t\t\t\t\tapi.column(i).visible(col.visible, false);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tcolumns[i].bVisible = col.visible;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Search\n\t\t\t\t\tif ( col.search !== undefined ) {\n\t\t\t\t\t\t$.extend( settings.aoPreSearchCols[i], col.search );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\t// If the api is defined then we need to adjust the columns once the visibility has been changed\n\t\t\t\tif (api) {\n\t\t\t\t\tapi.one('draw', function () {\n\t\t\t\t\t\tapi.columns.adjust();\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\tsettings._bLoadingState = false;\n\t\t_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );\n\t\tcallback();\n\t}\n\t\n\t/**\n\t * Log an error message\n\t *  @param {object} settings dataTables settings object\n\t *  @param {int} level log error messages, or display them to the user\n\t *  @param {string} msg error message\n\t *  @param {int} tn Technical note id to get more information about the error.\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnLog( settings, level, msg, tn )\n\t{\n\t\tmsg = 'DataTables warning: '+\n\t\t\t(settings ? 'table id='+settings.sTableId+' - ' : '')+msg;\n\t\n\t\tif ( tn ) {\n\t\t\tmsg += '. For more information about this error, please see '+\n\t\t\t'https://datatables.net/tn/'+tn;\n\t\t}\n\t\n\t\tif ( ! level  ) {\n\t\t\t// Backwards compatibility pre 1.10\n\t\t\tvar ext = DataTable.ext;\n\t\t\tvar type = ext.sErrMode || ext.errMode;\n\t\n\t\t\tif ( settings ) {\n\t\t\t\t_fnCallbackFire( settings, null, 'dt-error', [ settings, tn, msg ], true );\n\t\t\t}\n\t\n\t\t\tif ( type == 'alert' ) {\n\t\t\t\talert( msg );\n\t\t\t}\n\t\t\telse if ( type == 'throw' ) {\n\t\t\t\tthrow new Error(msg);\n\t\t\t}\n\t\t\telse if ( typeof type == 'function' ) {\n\t\t\t\ttype( settings, tn, msg );\n\t\t\t}\n\t\t}\n\t\telse if ( window.console && console.log ) {\n\t\t\tconsole.log( msg );\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * See if a property is defined on one object, if so assign it to the other object\n\t *  @param {object} ret target object\n\t *  @param {object} src source object\n\t *  @param {string} name property\n\t *  @param {string} [mappedName] name to map too - optional, name used if not given\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnMap( ret, src, name, mappedName )\n\t{\n\t\tif ( Array.isArray( name ) ) {\n\t\t\t$.each( name, function (i, val) {\n\t\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\t\t_fnMap( ret, src, val[0], val[1] );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t_fnMap( ret, src, val );\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\treturn;\n\t\t}\n\t\n\t\tif ( mappedName === undefined ) {\n\t\t\tmappedName = name;\n\t\t}\n\t\n\t\tif ( src[name] !== undefined ) {\n\t\t\tret[mappedName] = src[name];\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Extend objects - very similar to jQuery.extend, but deep copy objects, and\n\t * shallow copy arrays. The reason we need to do this, is that we don't want to\n\t * deep copy array init values (such as aaSorting) since the dev wouldn't be\n\t * able to override them, but we do want to deep copy arrays.\n\t *  @param {object} out Object to extend\n\t *  @param {object} extender Object from which the properties will be applied to\n\t *      out\n\t *  @param {boolean} breakRefs If true, then arrays will be sliced to take an\n\t *      independent copy with the exception of the `data` or `aaData` parameters\n\t *      if they are present. This is so you can pass in a collection to\n\t *      DataTables and have that used as your data source without breaking the\n\t *      references\n\t *  @returns {object} out Reference, just for convenience - out === the return.\n\t *  @memberof DataTable#oApi\n\t *  @todo This doesn't take account of arrays inside the deep copied objects.\n\t */\n\tfunction _fnExtend( out, extender, breakRefs )\n\t{\n\t\tvar val;\n\t\n\t\tfor ( var prop in extender ) {\n\t\t\tif ( Object.prototype.hasOwnProperty.call(extender, prop) ) {\n\t\t\t\tval = extender[prop];\n\t\n\t\t\t\tif ( $.isPlainObject( val ) ) {\n\t\t\t\t\tif ( ! $.isPlainObject( out[prop] ) ) {\n\t\t\t\t\t\tout[prop] = {};\n\t\t\t\t\t}\n\t\t\t\t\t$.extend( true, out[prop], val );\n\t\t\t\t}\n\t\t\t\telse if ( breakRefs && prop !== 'data' && prop !== 'aaData' && Array.isArray(val) ) {\n\t\t\t\t\tout[prop] = val.slice();\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tout[prop] = val;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t}\n\t\n\t\n\t/**\n\t * Bind an event handler to allow a click or return key to activate the callback.\n\t * This is good for accessibility since a return on the keyboard will have the\n\t * same effect as a click, if the element has focus.\n\t *  @param {element} n Element to bind the action to\n\t *  @param {object|string} selector Selector (for delegated events) or data object\n\t *   to pass to the triggered function\n\t *  @param {function} fn Callback function for when the event is triggered\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnBindAction( n, selector, fn )\n\t{\n\t\t$(n)\n\t\t\t.on( 'click.DT', selector, function (e) {\n\t\t\t\tfn(e);\n\t\t\t} )\n\t\t\t.on( 'keypress.DT', selector, function (e){\n\t\t\t\tif ( e.which === 13 ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tfn(e);\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.on( 'selectstart.DT', selector, function () {\n\t\t\t\t// Don't want a double click resulting in text selection\n\t\t\t\treturn false;\n\t\t\t} );\n\t}\n\t\n\t\n\t/**\n\t * Register a callback function. Easily allows a callback function to be added to\n\t * an array store of callback functions that can then all be called together.\n\t *  @param {object} settings dataTables settings object\n\t *  @param {string} store Name of the array storage for the callbacks in oSettings\n\t *  @param {function} fn Function to be called back\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCallbackReg( settings, store, fn )\n\t{\n\t\tif ( fn ) {\n\t\t\tsettings[store].push(fn);\n\t\t}\n\t}\n\t\n\t\n\t/**\n\t * Fire callback functions and trigger events. Note that the loop over the\n\t * callback array store is done backwards! Further note that you do not want to\n\t * fire off triggers in time sensitive applications (for example cell creation)\n\t * as its slow.\n\t *  @param {object} settings dataTables settings object\n\t *  @param {string} callbackArr Name of the array storage for the callbacks in\n\t *      oSettings\n\t *  @param {string} eventName Name of the jQuery custom event to trigger. If\n\t *      null no trigger is fired\n\t *  @param {array} args Array of arguments to pass to the callback function /\n\t *      trigger\n\t *  @param {boolean} [bubbles] True if the event should bubble\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnCallbackFire( settings, callbackArr, eventName, args, bubbles )\n\t{\n\t\tvar ret = [];\n\t\n\t\tif ( callbackArr ) {\n\t\t\tret = settings[callbackArr].slice().reverse().map( function (val) {\n\t\t\t\treturn val.apply( settings.oInstance, args );\n\t\t\t} );\n\t\t}\n\t\n\t\tif ( eventName !== null) {\n\t\t\tvar e = $.Event( eventName+'.dt' );\n\t\t\tvar table = $(settings.nTable);\n\t\t\t\n\t\t\t// Expose the DataTables API on the event object for easy access\n\t\t\te.dt = settings.api;\n\t\n\t\t\ttable[bubbles ?  'trigger' : 'triggerHandler']( e, args );\n\t\n\t\t\t// If not yet attached to the document, trigger the event\n\t\t\t// on the body directly to sort of simulate the bubble\n\t\t\tif (bubbles && table.parents('body').length === 0) {\n\t\t\t\t$('body').trigger( e, args );\n\t\t\t}\n\t\n\t\t\tret.push( e.result );\n\t\t}\n\t\n\t\treturn ret;\n\t}\n\t\n\t\n\tfunction _fnLengthOverflow ( settings )\n\t{\n\t\tvar\n\t\t\tstart = settings._iDisplayStart,\n\t\t\tend = settings.fnDisplayEnd(),\n\t\t\tlen = settings._iDisplayLength;\n\t\n\t\t/* If we have space to show extra rows (backing up from the end point - then do so */\n\t\tif ( start >= end )\n\t\t{\n\t\t\tstart = end - len;\n\t\t}\n\t\n\t\t// Keep the start record on the current page\n\t\tstart -= (start % len);\n\t\n\t\tif ( len === -1 || start < 0 )\n\t\t{\n\t\t\tstart = 0;\n\t\t}\n\t\n\t\tsettings._iDisplayStart = start;\n\t}\n\t\n\t\n\tfunction _fnRenderer( settings, type )\n\t{\n\t\tvar renderer = settings.renderer;\n\t\tvar host = DataTable.ext.renderer[type];\n\t\n\t\tif ( $.isPlainObject( renderer ) && renderer[type] ) {\n\t\t\t// Specific renderer for this type. If available use it, otherwise use\n\t\t\t// the default.\n\t\t\treturn host[renderer[type]] || host._;\n\t\t}\n\t\telse if ( typeof renderer === 'string' ) {\n\t\t\t// Common renderer - if there is one available for this type use it,\n\t\t\t// otherwise use the default\n\t\t\treturn host[renderer] || host._;\n\t\t}\n\t\n\t\t// Use the default\n\t\treturn host._;\n\t}\n\t\n\t\n\t/**\n\t * Detect the data source being used for the table. Used to simplify the code\n\t * a little (ajax) and to make it compress a little smaller.\n\t *\n\t *  @param {object} settings dataTables settings object\n\t *  @returns {string} Data source\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnDataSource ( settings )\n\t{\n\t\tif ( settings.oFeatures.bServerSide ) {\n\t\t\treturn 'ssp';\n\t\t}\n\t\telse if ( settings.ajax ) {\n\t\t\treturn 'ajax';\n\t\t}\n\t\treturn 'dom';\n\t}\n\t\n\t/**\n\t * Common replacement for language strings\n\t *\n\t * @param {*} settings DT settings object\n\t * @param {*} str String with values to replace\n\t * @param {*} entries Plural number for _ENTRIES_ - can be undefined\n\t * @returns String\n\t */\n\tfunction _fnMacros ( settings, str, entries )\n\t{\n\t\t// When infinite scrolling, we are always starting at 1. _iDisplayStart is\n\t\t// used only internally\n\t\tvar\n\t\t\tformatter  = settings.fnFormatNumber,\n\t\t\tstart      = settings._iDisplayStart+1,\n\t\t\tlen        = settings._iDisplayLength,\n\t\t\tvis        = settings.fnRecordsDisplay(),\n\t\t\tmax        = settings.fnRecordsTotal(),\n\t\t\tall        = len === -1;\n\t\n\t\treturn str.\n\t\t\treplace(/_START_/g, formatter.call( settings, start ) ).\n\t\t\treplace(/_END_/g,   formatter.call( settings, settings.fnDisplayEnd() ) ).\n\t\t\treplace(/_MAX_/g,   formatter.call( settings, max ) ).\n\t\t\treplace(/_TOTAL_/g, formatter.call( settings, vis ) ).\n\t\t\treplace(/_PAGE_/g,  formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ).\n\t\t\treplace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) ).\n\t\t\treplace(/_ENTRIES_/g, settings.api.i18n('entries', '', entries) ).\n\t\t\treplace(/_ENTRIES-MAX_/g, settings.api.i18n('entries', '', max) ).\n\t\t\treplace(/_ENTRIES-TOTAL_/g, settings.api.i18n('entries', '', vis) );\n\t}\n\t\n\t/**\n\t * Add elements to an array as quickly as possible, but stack safe.\n\t *\n\t * @param {*} arr Array to add the data to\n\t * @param {*} data Data array that is to be added\n\t * @returns \n\t */\n\tfunction _fnArrayApply(arr, data) {\n\t\tif (! data) {\n\t\t\treturn;\n\t\t}\n\t\n\t\t// Chrome can throw a max stack error if apply is called with\n\t\t// too large an array, but apply is faster.\n\t\tif (data.length < 10000) {\n\t\t\tarr.push.apply(arr, data);\n\t\t}\n\t\telse {\n\t\t\tfor (i=0 ; i<data.length ; i++) {\n\t\t\t\tarr.push(data[i]);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Add one or more listeners to the table\n\t *\n\t * @param {*} that JQ for the table\n\t * @param {*} name Event name\n\t * @param {*} src Listener(s)\n\t */\n\tfunction _fnListener(that, name, src) {\n\t\tif (!Array.isArray(src)) {\n\t\t\tsrc = [src];\n\t\t}\n\t\n\t\tfor (i=0 ; i<src.length ; i++) {\n\t\t\tthat.on(name + '.dt', src[i]);\n\t\t}\n\t}\n\t\n\t/**\n\t * Escape HTML entities in strings, in an object\n\t */\n\tfunction _fnEscapeObject(obj) {\n\t\tif (DataTable.ext.escape.attributes) {\n\t\t\t$.each(obj, function (key, val) {\n\t\t\t\tobj[key] = _escapeHtml(val);\n\t\t\t})\n\t\t}\n\t\n\t\treturn obj;\n\t}\n\t\n\t\n\t\n\t/**\n\t * Computed structure of the DataTables API, defined by the options passed to\n\t * `DataTable.Api.register()` when building the API.\n\t *\n\t * The structure is built in order to speed creation and extension of the Api\n\t * objects since the extensions are effectively pre-parsed.\n\t *\n\t * The array is an array of objects with the following structure, where this\n\t * base array represents the Api prototype base:\n\t *\n\t *     [\n\t *       {\n\t *         name:      'data'                -- string   - Property name\n\t *         val:       function () {},       -- function - Api method (or undefined if just an object\n\t *         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result\n\t *         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property\n\t *       },\n\t *       {\n\t *         name:     'row'\n\t *         val:       {},\n\t *         methodExt: [ ... ],\n\t *         propExt:   [\n\t *           {\n\t *             name:      'data'\n\t *             val:       function () {},\n\t *             methodExt: [ ... ],\n\t *             propExt:   [ ... ]\n\t *           },\n\t *           ...\n\t *         ]\n\t *       }\n\t *     ]\n\t *\n\t * @type {Array}\n\t * @ignore\n\t */\n\tvar __apiStruct = [];\n\t\n\t\n\t/**\n\t * `Array.prototype` reference.\n\t *\n\t * @type object\n\t * @ignore\n\t */\n\tvar __arrayProto = Array.prototype;\n\t\n\t\n\t/**\n\t * Abstraction for `context` parameter of the `Api` constructor to allow it to\n\t * take several different forms for ease of use.\n\t *\n\t * Each of the input parameter types will be converted to a DataTables settings\n\t * object where possible.\n\t *\n\t * @param  {string|node|jQuery|object} mixed DataTable identifier. Can be one\n\t *   of:\n\t *\n\t *   * `string` - jQuery selector. Any DataTables' matching the given selector\n\t *     with be found and used.\n\t *   * `node` - `TABLE` node which has already been formed into a DataTable.\n\t *   * `jQuery` - A jQuery object of `TABLE` nodes.\n\t *   * `object` - DataTables settings object\n\t *   * `DataTables.Api` - API instance\n\t * @return {array|null} Matching DataTables settings objects. `null` or\n\t *   `undefined` is returned if no matching DataTable is found.\n\t * @ignore\n\t */\n\tvar _toSettings = function ( mixed )\n\t{\n\t\tvar idx, jq;\n\t\tvar settings = DataTable.settings;\n\t\tvar tables = _pluck(settings, 'nTable');\n\t\n\t\tif ( ! mixed ) {\n\t\t\treturn [];\n\t\t}\n\t\telse if ( mixed.nTable && mixed.oFeatures ) {\n\t\t\t// DataTables settings object\n\t\t\treturn [ mixed ];\n\t\t}\n\t\telse if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) {\n\t\t\t// Table node\n\t\t\tidx = tables.indexOf(mixed);\n\t\t\treturn idx !== -1 ? [ settings[idx] ] : null;\n\t\t}\n\t\telse if ( mixed && typeof mixed.settings === 'function' ) {\n\t\t\treturn mixed.settings().toArray();\n\t\t}\n\t\telse if ( typeof mixed === 'string' ) {\n\t\t\t// jQuery selector\n\t\t\tjq = $(mixed).get();\n\t\t}\n\t\telse if ( mixed instanceof $ ) {\n\t\t\t// jQuery object (also DataTables instance)\n\t\t\tjq = mixed.get();\n\t\t}\n\t\n\t\tif ( jq ) {\n\t\t\treturn settings.filter(function (v, idx) {\n\t\t\t\treturn jq.includes(tables[idx]);\n\t\t\t});\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * DataTables API class - used to control and interface with  one or more\n\t * DataTables enhanced tables.\n\t *\n\t * The API class is heavily based on jQuery, presenting a chainable interface\n\t * that you can use to interact with tables. Each instance of the API class has\n\t * a \"context\" - i.e. the tables that it will operate on. This could be a single\n\t * table, all tables on a page or a sub-set thereof.\n\t *\n\t * Additionally the API is designed to allow you to easily work with the data in\n\t * the tables, retrieving and manipulating it as required. This is done by\n\t * presenting the API class as an array like interface. The contents of the\n\t * array depend upon the actions requested by each method (for example\n\t * `rows().nodes()` will return an array of nodes, while `rows().data()` will\n\t * return an array of objects or arrays depending upon your table's\n\t * configuration). The API object has a number of array like methods (`push`,\n\t * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`,\n\t * `unique` etc) to assist your working with the data held in a table.\n\t *\n\t * Most methods (those which return an Api instance) are chainable, which means\n\t * the return from a method call also has all of the methods available that the\n\t * top level object had. For example, these two calls are equivalent:\n\t *\n\t *     // Not chained\n\t *     api.row.add( {...} );\n\t *     api.draw();\n\t *\n\t *     // Chained\n\t *     api.row.add( {...} ).draw();\n\t *\n\t * @class DataTable.Api\n\t * @param {array|object|string|jQuery} context DataTable identifier. This is\n\t *   used to define which DataTables enhanced tables this API will operate on.\n\t *   Can be one of:\n\t *\n\t *   * `string` - jQuery selector. Any DataTables' matching the given selector\n\t *     with be found and used.\n\t *   * `node` - `TABLE` node which has already been formed into a DataTable.\n\t *   * `jQuery` - A jQuery object of `TABLE` nodes.\n\t *   * `object` - DataTables settings object\n\t * @param {array} [data] Data to initialise the Api instance with.\n\t *\n\t * @example\n\t *   // Direct initialisation during DataTables construction\n\t *   var api = $('#example').DataTable();\n\t *\n\t * @example\n\t *   // Initialisation using a DataTables jQuery object\n\t *   var api = $('#example').dataTable().api();\n\t *\n\t * @example\n\t *   // Initialisation as a constructor\n\t *   var api = new DataTable.Api( 'table.dataTable' );\n\t */\n\t_Api = function ( context, data )\n\t{\n\t\tif ( ! (this instanceof _Api) ) {\n\t\t\treturn new _Api( context, data );\n\t\t}\n\t\n\t\tvar i;\n\t\tvar settings = [];\n\t\tvar ctxSettings = function ( o ) {\n\t\t\tvar a = _toSettings( o );\n\t\t\tif ( a ) {\n\t\t\t\tsettings.push.apply( settings, a );\n\t\t\t}\n\t\t};\n\t\n\t\tif ( Array.isArray( context ) ) {\n\t\t\tfor ( i=0 ; i<context.length ; i++ ) {\n\t\t\t\tctxSettings( context[i] );\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tctxSettings( context );\n\t\t}\n\t\n\t\t// Remove duplicates\n\t\tthis.context = settings.length > 1\n\t\t\t? _unique( settings )\n\t\t\t: settings;\n\t\n\t\t// Initial data\n\t\t_fnArrayApply(this, data);\n\t\n\t\t// selector\n\t\tthis.selector = {\n\t\t\trows: null,\n\t\t\tcols: null,\n\t\t\topts: null\n\t\t};\n\t\n\t\t_Api.extend( this, this, __apiStruct );\n\t};\n\t\n\tDataTable.Api = _Api;\n\t\n\t// Don't destroy the existing prototype, just extend it. Required for jQuery 2's\n\t// isPlainObject.\n\t$.extend( _Api.prototype, {\n\t\tany: function ()\n\t\t{\n\t\t\treturn this.count() !== 0;\n\t\t},\n\t\n\t\tcontext: [], // array of table settings objects\n\t\n\t\tcount: function ()\n\t\t{\n\t\t\treturn this.flatten().length;\n\t\t},\n\t\n\t\teach: function ( fn )\n\t\t{\n\t\t\tfor ( var i=0, iLen=this.length ; i<iLen; i++ ) {\n\t\t\t\tfn.call( this, this[i], i, this );\n\t\t\t}\n\t\n\t\t\treturn this;\n\t\t},\n\t\n\t\teq: function ( idx )\n\t\t{\n\t\t\tvar ctx = this.context;\n\t\n\t\t\treturn ctx.length > idx ?\n\t\t\t\tnew _Api( ctx[idx], this[idx] ) :\n\t\t\t\tnull;\n\t\t},\n\t\n\t\tfilter: function ( fn )\n\t\t{\n\t\t\tvar a = __arrayProto.filter.call( this, fn, this );\n\t\n\t\t\treturn new _Api( this.context, a );\n\t\t},\n\t\n\t\tflatten: function ()\n\t\t{\n\t\t\tvar a = [];\n\t\n\t\t\treturn new _Api( this.context, a.concat.apply( a, this.toArray() ) );\n\t\t},\n\t\n\t\tget: function ( idx )\n\t\t{\n\t\t\treturn this[ idx ];\n\t\t},\n\t\n\t\tjoin:    __arrayProto.join,\n\t\n\t\tincludes: function ( find ) {\n\t\t\treturn this.indexOf( find ) === -1 ? false : true;\n\t\t},\n\t\n\t\tindexOf: __arrayProto.indexOf,\n\t\n\t\titerator: function ( flatten, type, fn, alwaysNew ) {\n\t\t\tvar\n\t\t\t\ta = [], ret,\n\t\t\t\ti, iLen, j, jen,\n\t\t\t\tcontext = this.context,\n\t\t\t\trows, items, item,\n\t\t\t\tselector = this.selector;\n\t\n\t\t\t// Argument shifting\n\t\t\tif ( typeof flatten === 'string' ) {\n\t\t\t\talwaysNew = fn;\n\t\t\t\tfn = type;\n\t\t\t\ttype = flatten;\n\t\t\t\tflatten = false;\n\t\t\t}\n\t\n\t\t\tfor ( i=0, iLen=context.length ; i<iLen ; i++ ) {\n\t\t\t\tvar apiInst = new _Api( context[i] );\n\t\n\t\t\t\tif ( type === 'table' ) {\n\t\t\t\t\tret = fn.call( apiInst, context[i], i );\n\t\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( type === 'columns' || type === 'rows' ) {\n\t\t\t\t\t// this has same length as context - one entry for each table\n\t\t\t\t\tret = fn.call( apiInst, context[i], this[i], i );\n\t\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( type === 'every' || type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) {\n\t\t\t\t\t// columns and rows share the same structure.\n\t\t\t\t\t// 'this' is an array of column indexes for each context\n\t\t\t\t\titems = this[i];\n\t\n\t\t\t\t\tif ( type === 'column-rows' ) {\n\t\t\t\t\t\trows = _selector_row_indexes( context[i], selector.opts );\n\t\t\t\t\t}\n\t\n\t\t\t\t\tfor ( j=0, jen=items.length ; j<jen ; j++ ) {\n\t\t\t\t\t\titem = items[j];\n\t\n\t\t\t\t\t\tif ( type === 'cell' ) {\n\t\t\t\t\t\t\tret = fn.call( apiInst, context[i], item.row, item.column, i, j );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tret = fn.call( apiInst, context[i], item, i, j, rows );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\t\ta.push( ret );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tif ( a.length || alwaysNew ) {\n\t\t\t\tvar api = new _Api( context, flatten ? a.concat.apply( [], a ) : a );\n\t\t\t\tvar apiSelector = api.selector;\n\t\t\t\tapiSelector.rows = selector.rows;\n\t\t\t\tapiSelector.cols = selector.cols;\n\t\t\t\tapiSelector.opts = selector.opts;\n\t\t\t\treturn api;\n\t\t\t}\n\t\t\treturn this;\n\t\t},\n\t\n\t\tlastIndexOf: __arrayProto.lastIndexOf,\n\t\n\t\tlength:  0,\n\t\n\t\tmap: function ( fn )\n\t\t{\n\t\t\tvar a = __arrayProto.map.call( this, fn, this );\n\t\n\t\t\treturn new _Api( this.context, a );\n\t\t},\n\t\n\t\tpluck: function ( prop )\n\t\t{\n\t\t\tvar fn = DataTable.util.get(prop);\n\t\n\t\t\treturn this.map( function ( el ) {\n\t\t\t\treturn fn(el);\n\t\t\t} );\n\t\t},\n\t\n\t\tpop:     __arrayProto.pop,\n\t\n\t\tpush:    __arrayProto.push,\n\t\n\t\treduce: __arrayProto.reduce,\n\t\n\t\treduceRight: __arrayProto.reduceRight,\n\t\n\t\treverse: __arrayProto.reverse,\n\t\n\t\t// Object with rows, columns and opts\n\t\tselector: null,\n\t\n\t\tshift:   __arrayProto.shift,\n\t\n\t\tslice: function () {\n\t\t\treturn new _Api( this.context, this );\n\t\t},\n\t\n\t\tsort:    __arrayProto.sort,\n\t\n\t\tsplice:  __arrayProto.splice,\n\t\n\t\ttoArray: function ()\n\t\t{\n\t\t\treturn __arrayProto.slice.call( this );\n\t\t},\n\t\n\t\tto$: function ()\n\t\t{\n\t\t\treturn $( this );\n\t\t},\n\t\n\t\ttoJQuery: function ()\n\t\t{\n\t\t\treturn $( this );\n\t\t},\n\t\n\t\tunique: function ()\n\t\t{\n\t\t\treturn new _Api( this.context, _unique(this.toArray()) );\n\t\t},\n\t\n\t\tunshift: __arrayProto.unshift\n\t} );\n\t\n\t\n\tfunction _api_scope( scope, fn, struct ) {\n\t\treturn function () {\n\t\t\tvar ret = fn.apply( scope || this, arguments );\n\t\n\t\t\t// Method extension\n\t\t\t_Api.extend( ret, ret, struct.methodExt );\n\t\t\treturn ret;\n\t\t};\n\t}\n\t\n\tfunction _api_find( src, name ) {\n\t\tfor ( var i=0, iLen=src.length ; i<iLen ; i++ ) {\n\t\t\tif ( src[i].name === name ) {\n\t\t\t\treturn src[i];\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\t\n\twindow.__apiStruct = __apiStruct;\n\t\n\t_Api.extend = function ( scope, obj, ext )\n\t{\n\t\t// Only extend API instances and static properties of the API\n\t\tif ( ! ext.length || ! obj || ( ! (obj instanceof _Api) && ! obj.__dt_wrapper ) ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\ti, iLen,\n\t\t\tstruct;\n\t\n\t\tfor ( i=0, iLen=ext.length ; i<iLen ; i++ ) {\n\t\t\tstruct = ext[i];\n\t\n\t\t\tif (struct.name === '__proto__') {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\t// Value\n\t\t\tobj[ struct.name ] = struct.type === 'function' ?\n\t\t\t\t_api_scope( scope, struct.val, struct ) :\n\t\t\t\tstruct.type === 'object' ?\n\t\t\t\t\t{} :\n\t\t\t\t\tstruct.val;\n\t\n\t\t\tobj[ struct.name ].__dt_wrapper = true;\n\t\n\t\t\t// Property extension\n\t\t\t_Api.extend( scope, obj[ struct.name ], struct.propExt );\n\t\t}\n\t};\n\t\n\t//     [\n\t//       {\n\t//         name:      'data'                -- string   - Property name\n\t//         val:       function () {},       -- function - Api method (or undefined if just an object\n\t//         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result\n\t//         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property\n\t//       },\n\t//       {\n\t//         name:     'row'\n\t//         val:       {},\n\t//         methodExt: [ ... ],\n\t//         propExt:   [\n\t//           {\n\t//             name:      'data'\n\t//             val:       function () {},\n\t//             methodExt: [ ... ],\n\t//             propExt:   [ ... ]\n\t//           },\n\t//           ...\n\t//         ]\n\t//       }\n\t//     ]\n\t\n\t\n\t_Api.register = _api_register = function ( name, val )\n\t{\n\t\tif ( Array.isArray( name ) ) {\n\t\t\tfor ( var j=0, jen=name.length ; j<jen ; j++ ) {\n\t\t\t\t_Api.register( name[j], val );\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\ti, iLen,\n\t\t\their = name.split('.'),\n\t\t\tstruct = __apiStruct,\n\t\t\tkey, method;\n\t\n\t\tfor ( i=0, iLen=heir.length ; i<iLen ; i++ ) {\n\t\t\tmethod = heir[i].indexOf('()') !== -1;\n\t\t\tkey = method ?\n\t\t\t\their[i].replace('()', '') :\n\t\t\t\their[i];\n\t\n\t\t\tvar src = _api_find( struct, key );\n\t\t\tif ( ! src ) {\n\t\t\t\tsrc = {\n\t\t\t\t\tname:      key,\n\t\t\t\t\tval:       {},\n\t\t\t\t\tmethodExt: [],\n\t\t\t\t\tpropExt:   [],\n\t\t\t\t\ttype:      'object'\n\t\t\t\t};\n\t\t\t\tstruct.push( src );\n\t\t\t}\n\t\n\t\t\tif ( i === iLen-1 ) {\n\t\t\t\tsrc.val = val;\n\t\t\t\tsrc.type = typeof val === 'function' ?\n\t\t\t\t\t'function' :\n\t\t\t\t\t$.isPlainObject( val ) ?\n\t\t\t\t\t\t'object' :\n\t\t\t\t\t\t'other';\n\t\t\t}\n\t\t\telse {\n\t\t\t\tstruct = method ?\n\t\t\t\t\tsrc.methodExt :\n\t\t\t\t\tsrc.propExt;\n\t\t\t}\n\t\t}\n\t};\n\t\n\t_Api.registerPlural = _api_registerPlural = function ( pluralName, singularName, val ) {\n\t\t_Api.register( pluralName, val );\n\t\n\t\t_Api.register( singularName, function () {\n\t\t\tvar ret = val.apply( this, arguments );\n\t\n\t\t\tif ( ret === this ) {\n\t\t\t\t// Returned item is the API instance that was passed in, return it\n\t\t\t\treturn this;\n\t\t\t}\n\t\t\telse if ( ret instanceof _Api ) {\n\t\t\t\t// New API instance returned, want the value from the first item\n\t\t\t\t// in the returned array for the singular result.\n\t\t\t\treturn ret.length ?\n\t\t\t\t\tArray.isArray( ret[0] ) ?\n\t\t\t\t\t\tnew _Api( ret.context, ret[0] ) : // Array results are 'enhanced'\n\t\t\t\t\t\tret[0] :\n\t\t\t\t\tundefined;\n\t\t\t}\n\t\n\t\t\t// Non-API return - just fire it back\n\t\t\treturn ret;\n\t\t} );\n\t};\n\t\n\t\n\t/**\n\t * Selector for HTML tables. Apply the given selector to the give array of\n\t * DataTables settings objects.\n\t *\n\t * @param {string|integer} [selector] jQuery selector string or integer\n\t * @param  {array} Array of DataTables settings objects to be filtered\n\t * @return {array}\n\t * @ignore\n\t */\n\tvar __table_selector = function ( selector, a )\n\t{\n\t\tif ( Array.isArray(selector) ) {\n\t\t\tvar result = [];\n\t\n\t\t\tselector.forEach(function (sel) {\n\t\t\t\tvar inner = __table_selector(sel, a);\n\t\n\t\t\t\t_fnArrayApply(result, inner);\n\t\t\t});\n\t\n\t\t\treturn result.filter( function (item) {\n\t\t\t\treturn item;\n\t\t\t});\n\t\t}\n\t\n\t\t// Integer is used to pick out a table by index\n\t\tif ( typeof selector === 'number' ) {\n\t\t\treturn [ a[ selector ] ];\n\t\t}\n\t\n\t\t// Perform a jQuery selector on the table nodes\n\t\tvar nodes = a.map( function (el) {\n\t\t\treturn el.nTable;\n\t\t} );\n\t\n\t\treturn $(nodes)\n\t\t\t.filter( selector )\n\t\t\t.map( function () {\n\t\t\t\t// Need to translate back from the table node to the settings\n\t\t\t\tvar idx = nodes.indexOf(this);\n\t\t\t\treturn a[ idx ];\n\t\t\t} )\n\t\t\t.toArray();\n\t};\n\t\n\t\n\t\n\t/**\n\t * Context selector for the API's context (i.e. the tables the API instance\n\t * refers to.\n\t *\n\t * @name    DataTable.Api#tables\n\t * @param {string|integer} [selector] Selector to pick which tables the iterator\n\t *   should operate on. If not given, all tables in the current context are\n\t *   used. This can be given as a jQuery selector (for example `':gt(0)'`) to\n\t *   select multiple tables or as an integer to select a single table.\n\t * @returns {DataTable.Api} Returns a new API instance if a selector is given.\n\t */\n\t_api_register( 'tables()', function ( selector ) {\n\t\t// A new instance is created if there was a selector specified\n\t\treturn selector !== undefined && selector !== null ?\n\t\t\tnew _Api( __table_selector( selector, this.context ) ) :\n\t\t\tthis;\n\t} );\n\t\n\t\n\t_api_register( 'table()', function ( selector ) {\n\t\tvar tables = this.tables( selector );\n\t\tvar ctx = tables.context;\n\t\n\t\t// Truncate to the first matched table\n\t\treturn ctx.length ?\n\t\t\tnew _Api( ctx[0] ) :\n\t\t\ttables;\n\t} );\n\t\n\t// Common methods, combined to reduce size\n\t[\n\t\t['nodes', 'node', 'nTable'],\n\t\t['body', 'body', 'nTBody'],\n\t\t['header', 'header', 'nTHead'],\n\t\t['footer', 'footer', 'nTFoot'],\n\t].forEach(function (item) {\n\t\t_api_registerPlural(\n\t\t\t'tables().' + item[0] + '()',\n\t\t\t'table().' + item[1] + '()' ,\n\t\t\tfunction () {\n\t\t\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\t\t\treturn ctx[item[2]];\n\t\t\t\t}, 1 );\n\t\t\t}\n\t\t);\n\t});\n\t\n\t// Structure methods\n\t[\n\t\t['header', 'aoHeader'],\n\t\t['footer', 'aoFooter'],\n\t].forEach(function (item) {\n\t\t_api_register( 'table().' + item[0] + '.structure()' , function (selector) {\n\t\t\tvar indexes = this.columns(selector).indexes().flatten().toArray();\n\t\t\tvar ctx = this.context[0];\n\t\t\tvar structure = _fnHeaderLayout(ctx, ctx[item[1]], indexes);\n\t\n\t\t\t// The structure is in column index order - but from this method we want the return to be\n\t\t\t// in the columns() selector API order. In order to do that we need to map from one form\n\t\t\t// to the other\n\t\t\tvar orderedIndexes = indexes.slice().sort(function (a, b) {\n\t\t\t\treturn a - b;\n\t\t\t});\n\t\n\t\t\treturn structure.map(function (row) {\n\t\t\t\treturn indexes.map(function (colIdx) {\n\t\t\t\t\treturn row[orderedIndexes.indexOf(colIdx)];\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t});\n\t\n\t\n\t_api_registerPlural( 'tables().containers()', 'table().container()' , function () {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\treturn ctx.nTableWrapper;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'tables().every()', function ( fn ) {\n\t\tvar that = this;\n\t\n\t\treturn this.iterator('table', function (s, i) {\n\t\t\tfn.call(that.table(i), i);\n\t\t});\n\t});\n\t\n\t_api_register( 'caption()', function ( value, side ) {\n\t\tvar context = this.context;\n\t\n\t\t// Getter - return existing node's content\n\t\tif ( value === undefined ) {\n\t\t\tvar caption = context[0].captionNode;\n\t\n\t\t\treturn caption && context.length ?\n\t\t\t\tcaption.innerHTML : \n\t\t\t\tnull;\n\t\t}\n\t\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\tvar table = $(ctx.nTable);\n\t\t\tvar caption = $(ctx.captionNode);\n\t\t\tvar container = $(ctx.nTableWrapper);\n\t\n\t\t\t// Create the node if it doesn't exist yet\n\t\t\tif ( ! caption.length ) {\n\t\t\t\tcaption = $('<caption/>').html( value );\n\t\t\t\tctx.captionNode = caption[0];\n\t\n\t\t\t\t// If side isn't set, we need to insert into the document to let the\n\t\t\t\t// CSS decide so we can read it back, otherwise there is no way to\n\t\t\t\t// know if the CSS would put it top or bottom for scrolling\n\t\t\t\tif (! side) {\n\t\t\t\t\ttable.prepend(caption);\n\t\n\t\t\t\t\tside = caption.css('caption-side');\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tcaption.html( value );\n\t\n\t\t\tif ( side ) {\n\t\t\t\tcaption.css( 'caption-side', side );\n\t\t\t\tcaption[0]._captionSide = side;\n\t\t\t}\n\t\n\t\t\tif (container.find('div.dataTables_scroll').length) {\n\t\t\t\tvar selector = (side === 'top' ? 'Head' : 'Foot');\n\t\n\t\t\t\tcontainer.find('div.dataTables_scroll'+ selector +' table').prepend(caption);\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttable.prepend(caption);\n\t\t\t}\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'caption.node()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\treturn ctx.length ? ctx[0].captionNode : null;\n\t} );\n\t\n\t\n\t/**\n\t * Redraw the tables in the current context.\n\t */\n\t_api_register( 'draw()', function ( paging ) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( paging === 'page' ) {\n\t\t\t\t_fnDraw( settings );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif ( typeof paging === 'string' ) {\n\t\t\t\t\tpaging = paging === 'full-hold' ?\n\t\t\t\t\t\tfalse :\n\t\t\t\t\t\ttrue;\n\t\t\t\t}\n\t\n\t\t\t\t_fnReDraw( settings, paging===false );\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Get the current page index.\n\t *\n\t * @return {integer} Current page index (zero based)\n\t *//**\n\t * Set the current page.\n\t *\n\t * Note that if you attempt to show a page which does not exist, DataTables will\n\t * not throw an error, but rather reset the paging.\n\t *\n\t * @param {integer|string} action The paging action to take. This can be one of:\n\t *  * `integer` - The page index to jump to\n\t *  * `string` - An action to take:\n\t *    * `first` - Jump to first page.\n\t *    * `next` - Jump to the next page\n\t *    * `previous` - Jump to previous page\n\t *    * `last` - Jump to the last page.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'page()', function ( action ) {\n\t\tif ( action === undefined ) {\n\t\t\treturn this.page.info().page; // not an expensive call\n\t\t}\n\t\n\t\t// else, have an action to take on all tables\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnPageChange( settings, action );\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Paging information for the first table in the current context.\n\t *\n\t * If you require paging information for another table, use the `table()` method\n\t * with a suitable selector.\n\t *\n\t * @return {object} Object with the following properties set:\n\t *  * `page` - Current page index (zero based - i.e. the first page is `0`)\n\t *  * `pages` - Total number of pages\n\t *  * `start` - Display index for the first record shown on the current page\n\t *  * `end` - Display index for the last record shown on the current page\n\t *  * `length` - Display length (number of records). Note that generally `start\n\t *    + length = end`, but this is not always true, for example if there are\n\t *    only 2 records to show on the final page, with a length of 10.\n\t *  * `recordsTotal` - Full data set length\n\t *  * `recordsDisplay` - Data set length once the current filtering criterion\n\t *    are applied.\n\t */\n\t_api_register( 'page.info()', function () {\n\t\tif ( this.context.length === 0 ) {\n\t\t\treturn undefined;\n\t\t}\n\t\n\t\tvar\n\t\t\tsettings   = this.context[0],\n\t\t\tstart      = settings._iDisplayStart,\n\t\t\tlen        = settings.oFeatures.bPaginate ? settings._iDisplayLength : -1,\n\t\t\tvisRecords = settings.fnRecordsDisplay(),\n\t\t\tall        = len === -1;\n\t\n\t\treturn {\n\t\t\t\"page\":           all ? 0 : Math.floor( start / len ),\n\t\t\t\"pages\":          all ? 1 : Math.ceil( visRecords / len ),\n\t\t\t\"start\":          start,\n\t\t\t\"end\":            settings.fnDisplayEnd(),\n\t\t\t\"length\":         len,\n\t\t\t\"recordsTotal\":   settings.fnRecordsTotal(),\n\t\t\t\"recordsDisplay\": visRecords,\n\t\t\t\"serverSide\":     _fnDataSource( settings ) === 'ssp'\n\t\t};\n\t} );\n\t\n\t\n\t/**\n\t * Get the current page length.\n\t *\n\t * @return {integer} Current page length. Note `-1` indicates that all records\n\t *   are to be shown.\n\t *//**\n\t * Set the current page length.\n\t *\n\t * @param {integer} Page length to set. Use `-1` to show all records.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'page.len()', function ( len ) {\n\t\t// Note that we can't call this function 'length()' because `length`\n\t\t// is a JavaScript property of functions which defines how many arguments\n\t\t// the function expects.\n\t\tif ( len === undefined ) {\n\t\t\treturn this.context.length !== 0 ?\n\t\t\t\tthis.context[0]._iDisplayLength :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// else, set the page length\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnLengthChange( settings, len );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\tvar __reload = function ( settings, holdPosition, callback ) {\n\t\t// Use the draw event to trigger a callback\n\t\tif ( callback ) {\n\t\t\tvar api = new _Api( settings );\n\t\n\t\t\tapi.one( 'draw', function () {\n\t\t\t\tcallback( api.ajax.json() );\n\t\t\t} );\n\t\t}\n\t\n\t\tif ( _fnDataSource( settings ) == 'ssp' ) {\n\t\t\t_fnReDraw( settings, holdPosition );\n\t\t}\n\t\telse {\n\t\t\t_fnProcessingDisplay( settings, true );\n\t\n\t\t\t// Cancel an existing request\n\t\t\tvar xhr = settings.jqXHR;\n\t\t\tif ( xhr && xhr.readyState !== 4 ) {\n\t\t\t\txhr.abort();\n\t\t\t}\n\t\n\t\t\t// Trigger xhr\n\t\t\t_fnBuildAjax( settings, {}, function( json ) {\n\t\t\t\t_fnClearTable( settings );\n\t\n\t\t\t\tvar data = _fnAjaxDataSrc( settings, json );\n\t\t\t\tfor ( var i=0, iLen=data.length ; i<iLen ; i++ ) {\n\t\t\t\t\t_fnAddData( settings, data[i] );\n\t\t\t\t}\n\t\n\t\t\t\t_fnReDraw( settings, holdPosition );\n\t\t\t\t_fnInitComplete( settings );\n\t\t\t\t_fnProcessingDisplay( settings, false );\n\t\t\t} );\n\t\t}\n\t};\n\t\n\t\n\t/**\n\t * Get the JSON response from the last Ajax request that DataTables made to the\n\t * server. Note that this returns the JSON from the first table in the current\n\t * context.\n\t *\n\t * @return {object} JSON received from the server.\n\t */\n\t_api_register( 'ajax.json()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length > 0 ) {\n\t\t\treturn ctx[0].json;\n\t\t}\n\t\n\t\t// else return undefined;\n\t} );\n\t\n\t\n\t/**\n\t * Get the data submitted in the last Ajax request\n\t */\n\t_api_register( 'ajax.params()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length > 0 ) {\n\t\t\treturn ctx[0].oAjaxData;\n\t\t}\n\t\n\t\t// else return undefined;\n\t} );\n\t\n\t\n\t/**\n\t * Reload tables from the Ajax data source. Note that this function will\n\t * automatically re-draw the table when the remote data has been loaded.\n\t *\n\t * @param {boolean} [reset=true] Reset (default) or hold the current paging\n\t *   position. A full re-sort and re-filter is performed when this method is\n\t *   called, which is why the pagination reset is the default action.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.reload()', function ( callback, resetPaging ) {\n\t\treturn this.iterator( 'table', function (settings) {\n\t\t\t__reload( settings, resetPaging===false, callback );\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Get the current Ajax URL. Note that this returns the URL from the first\n\t * table in the current context.\n\t *\n\t * @return {string} Current Ajax source URL\n\t *//**\n\t * Set the Ajax URL. Note that this will set the URL for all tables in the\n\t * current context.\n\t *\n\t * @param {string} url URL to set.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.url()', function ( url ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( url === undefined ) {\n\t\t\t// get\n\t\t\tif ( ctx.length === 0 ) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tctx = ctx[0];\n\t\n\t\t\treturn $.isPlainObject( ctx.ajax ) ?\n\t\t\t\tctx.ajax.url :\n\t\t\t\tctx.ajax;\n\t\t}\n\t\n\t\t// set\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( $.isPlainObject( settings.ajax ) ) {\n\t\t\t\tsettings.ajax.url = url;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsettings.ajax = url;\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Load data from the newly set Ajax URL. Note that this method is only\n\t * available when `ajax.url()` is used to set a URL. Additionally, this method\n\t * has the same effect as calling `ajax.reload()` but is provided for\n\t * convenience when setting a new URL. Like `ajax.reload()` it will\n\t * automatically redraw the table once the remote data has been loaded.\n\t *\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'ajax.url().load()', function ( callback, resetPaging ) {\n\t\t// Same as a reload, but makes sense to present it for easy access after a\n\t\t// url change\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\t__reload( ctx, resetPaging===false, callback );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t\n\tvar _selector_run = function ( type, selector, selectFn, settings, opts )\n\t{\n\t\tvar\n\t\t\tout = [], res,\n\t\t\ti, iLen,\n\t\t\tselectorType = typeof selector;\n\t\n\t\t// Can't just check for isArray here, as an API or jQuery instance might be\n\t\t// given with their array like look\n\t\tif ( ! selector || selectorType === 'string' || selectorType === 'function' || selector.length === undefined ) {\n\t\t\tselector = [ selector ];\n\t\t}\n\t\n\t\tfor ( i=0, iLen=selector.length ; i<iLen ; i++ ) {\n\t\t\tres = selectFn( typeof selector[i] === 'string' ? selector[i].trim() : selector[i] );\n\t\n\t\t\t// Remove empty items\n\t\t\tres = res.filter( function (item) {\n\t\t\t\treturn item !== null && item !== undefined;\n\t\t\t});\n\t\n\t\t\tif ( res && res.length ) {\n\t\t\t\tout = out.concat( res );\n\t\t\t}\n\t\t}\n\t\n\t\t// selector extensions\n\t\tvar ext = _ext.selector[ type ];\n\t\tif ( ext.length ) {\n\t\t\tfor ( i=0, iLen=ext.length ; i<iLen ; i++ ) {\n\t\t\t\tout = ext[i]( settings, opts, out );\n\t\t\t}\n\t\t}\n\t\n\t\treturn _unique( out );\n\t};\n\t\n\t\n\tvar _selector_opts = function ( opts )\n\t{\n\t\tif ( ! opts ) {\n\t\t\topts = {};\n\t\t}\n\t\n\t\t// Backwards compatibility for 1.9- which used the terminology filter rather\n\t\t// than search\n\t\tif ( opts.filter && opts.search === undefined ) {\n\t\t\topts.search = opts.filter;\n\t\t}\n\t\n\t\treturn $.extend( {\n\t\t\tcolumnOrder: 'implied',\n\t\t\tsearch: 'none',\n\t\t\torder: 'current',\n\t\t\tpage: 'all'\n\t\t}, opts );\n\t};\n\t\n\t\n\t// Reduce the API instance to the first item found\n\tvar _selector_first = function ( old )\n\t{\n\t\tvar inst = new _Api(old.context[0]);\n\t\n\t\t// Use a push rather than passing to the constructor, since it will\n\t\t// merge arrays down automatically, which isn't what is wanted here\n\t\tif (old.length) {\n\t\t\tinst.push( old[0] );\n\t\t}\n\t\n\t\tinst.selector = old.selector;\n\t\n\t\t// Limit to a single row / column / cell\n\t\tif (inst.length && inst[0].length > 1) {\n\t\t\tinst[0].splice(1);\n\t\t}\n\t\n\t\treturn inst;\n\t};\n\t\n\t\n\tvar _selector_row_indexes = function ( settings, opts )\n\t{\n\t\tvar\n\t\t\ti, iLen, tmp, a=[],\n\t\t\tdisplayFiltered = settings.aiDisplay,\n\t\t\tdisplayMaster = settings.aiDisplayMaster;\n\t\n\t\tvar\n\t\t\tsearch = opts.search,  // none, applied, removed\n\t\t\torder  = opts.order,   // applied, current, index (original - compatibility with 1.9)\n\t\t\tpage   = opts.page;    // all, current\n\t\n\t\tif ( _fnDataSource( settings ) == 'ssp' ) {\n\t\t\t// In server-side processing mode, most options are irrelevant since\n\t\t\t// rows not shown don't exist and the index order is the applied order\n\t\t\t// Removed is a special case - for consistency just return an empty\n\t\t\t// array\n\t\t\treturn search === 'removed' ?\n\t\t\t\t[] :\n\t\t\t\t_range( 0, displayMaster.length );\n\t\t}\n\t\n\t\tif ( page == 'current' ) {\n\t\t\t// Current page implies that order=current and filter=applied, since it is\n\t\t\t// fairly senseless otherwise, regardless of what order and search actually\n\t\t\t// are\n\t\t\tfor ( i=settings._iDisplayStart, iLen=settings.fnDisplayEnd() ; i<iLen ; i++ ) {\n\t\t\t\ta.push( displayFiltered[i] );\n\t\t\t}\n\t\t}\n\t\telse if ( order == 'current' || order == 'applied' ) {\n\t\t\tif ( search == 'none') {\n\t\t\t\ta = displayMaster.slice();\n\t\t\t}\n\t\t\telse if ( search == 'applied' ) {\n\t\t\t\ta = displayFiltered.slice();\n\t\t\t}\n\t\t\telse if ( search == 'removed' ) {\n\t\t\t\t// O(n+m) solution by creating a hash map\n\t\t\t\tvar displayFilteredMap = {};\n\t\n\t\t\t\tfor ( i=0, iLen=displayFiltered.length ; i<iLen ; i++ ) {\n\t\t\t\t\tdisplayFilteredMap[displayFiltered[i]] = null;\n\t\t\t\t}\n\t\n\t\t\t\tdisplayMaster.forEach(function (item) {\n\t\t\t\t\tif (! Object.prototype.hasOwnProperty.call(displayFilteredMap, item)) {\n\t\t\t\t\t\ta.push(item);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\telse if ( order == 'index' || order == 'original' ) {\n\t\t\tfor ( i=0, iLen=settings.aoData.length ; i<iLen ; i++ ) {\n\t\t\t\tif (! settings.aoData[i]) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\n\t\t\t\tif ( search == 'none' ) {\n\t\t\t\t\ta.push( i );\n\t\t\t\t}\n\t\t\t\telse { // applied | removed\n\t\t\t\t\ttmp = displayFiltered.indexOf(i);\n\t\n\t\t\t\t\tif ((tmp === -1 && search == 'removed') ||\n\t\t\t\t\t\t(tmp >= 0   && search == 'applied') )\n\t\t\t\t\t{\n\t\t\t\t\t\ta.push( i );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse if ( typeof order === 'number' ) {\n\t\t\t// Order the rows by the given column\n\t\t\tvar ordered = _fnSort(settings, order, 'asc');\n\t\n\t\t\tif (search === 'none') {\n\t\t\t\ta = ordered;\n\t\t\t}\n\t\t\telse { // applied | removed\n\t\t\t\tfor (i=0; i<ordered.length; i++) {\n\t\t\t\t\ttmp = displayFiltered.indexOf(ordered[i]);\n\t\n\t\t\t\t\tif ((tmp === -1 && search == 'removed') ||\n\t\t\t\t\t\t(tmp >= 0   && search == 'applied') )\n\t\t\t\t\t{\n\t\t\t\t\t\ta.push( ordered[i] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn a;\n\t};\n\t\n\t\n\t/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\t * Rows\n\t *\n\t * {}          - no selector - use all available rows\n\t * {integer}   - row aoData index\n\t * {node}      - TR node\n\t * {string}    - jQuery selector to apply to the TR elements\n\t * {array}     - jQuery array of nodes, or simply an array of TR nodes\n\t *\n\t */\n\tvar __row_selector = function ( settings, selector, opts )\n\t{\n\t\tvar rows;\n\t\tvar run = function ( sel ) {\n\t\t\tvar selInt = _intVal( sel );\n\t\t\tvar aoData = settings.aoData;\n\t\n\t\t\t// Short cut - selector is a number and no options provided (default is\n\t\t\t// all records, so no need to check if the index is in there, since it\n\t\t\t// must be - dev error if the index doesn't exist).\n\t\t\tif ( selInt !== null && ! opts ) {\n\t\t\t\treturn [ selInt ];\n\t\t\t}\n\t\n\t\t\tif ( ! rows ) {\n\t\t\t\trows = _selector_row_indexes( settings, opts );\n\t\t\t}\n\t\n\t\t\tif ( selInt !== null && rows.indexOf(selInt) !== -1 ) {\n\t\t\t\t// Selector - integer\n\t\t\t\treturn [ selInt ];\n\t\t\t}\n\t\t\telse if ( sel === null || sel === undefined || sel === '' ) {\n\t\t\t\t// Selector - none\n\t\t\t\treturn rows;\n\t\t\t}\n\t\n\t\t\t// Selector - function\n\t\t\tif ( typeof sel === 'function' ) {\n\t\t\t\treturn rows.map( function (idx) {\n\t\t\t\t\tvar row = aoData[ idx ];\n\t\t\t\t\treturn sel( idx, row._aData, row.nTr ) ? idx : null;\n\t\t\t\t} );\n\t\t\t}\n\t\n\t\t\t// Selector - node\n\t\t\tif ( sel.nodeName ) {\n\t\t\t\tvar rowIdx = sel._DT_RowIndex;  // Property added by DT for fast lookup\n\t\t\t\tvar cellIdx = sel._DT_CellIndex;\n\t\n\t\t\t\tif ( rowIdx !== undefined ) {\n\t\t\t\t\t// Make sure that the row is actually still present in the table\n\t\t\t\t\treturn aoData[ rowIdx ] && aoData[ rowIdx ].nTr === sel ?\n\t\t\t\t\t\t[ rowIdx ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t\telse if ( cellIdx ) {\n\t\t\t\t\treturn aoData[ cellIdx.row ] && aoData[ cellIdx.row ].nTr === sel.parentNode ?\n\t\t\t\t\t\t[ cellIdx.row ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tvar host = $(sel).closest('*[data-dt-row]');\n\t\t\t\t\treturn host.length ?\n\t\t\t\t\t\t[ host.data('dt-row') ] :\n\t\t\t\t\t\t[];\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// ID selector. Want to always be able to select rows by id, regardless\n\t\t\t// of if the tr element has been created or not, so can't rely upon\n\t\t\t// jQuery here - hence a custom implementation. This does not match\n\t\t\t// Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,\n\t\t\t// but to select it using a CSS selector engine (like Sizzle or\n\t\t\t// querySelect) it would need to need to be escaped for some characters.\n\t\t\t// DataTables simplifies this for row selectors since you can select\n\t\t\t// only a row. A # indicates an id any anything that follows is the id -\n\t\t\t// unescaped.\n\t\t\tif ( typeof sel === 'string' && sel.charAt(0) === '#' ) {\n\t\t\t\t// get row index from id\n\t\t\t\tvar rowObj = settings.aIds[ sel.replace( /^#/, '' ) ];\n\t\t\t\tif ( rowObj !== undefined ) {\n\t\t\t\t\treturn [ rowObj.idx ];\n\t\t\t\t}\n\t\n\t\t\t\t// need to fall through to jQuery in case there is DOM id that\n\t\t\t\t// matches\n\t\t\t}\n\t\t\t\n\t\t\t// Get nodes in the order from the `rows` array with null values removed\n\t\t\tvar nodes = _removeEmpty(\n\t\t\t\t_pluck_order( settings.aoData, rows, 'nTr' )\n\t\t\t);\n\t\n\t\t\t// Selector - jQuery selector string, array of nodes or jQuery object/\n\t\t\t// As jQuery's .filter() allows jQuery objects to be passed in filter,\n\t\t\t// it also allows arrays, so this will cope with all three options\n\t\t\treturn $(nodes)\n\t\t\t\t.filter( sel )\n\t\t\t\t.map( function () {\n\t\t\t\t\treturn this._DT_RowIndex;\n\t\t\t\t} )\n\t\t\t\t.toArray();\n\t\t};\n\t\n\t\tvar matched = _selector_run( 'row', selector, run, settings, opts );\n\t\n\t\tif (opts.order === 'current' || opts.order === 'applied') {\n\t\t\t_fnSortDisplay(settings, matched);\n\t\t}\n\t\n\t\treturn matched;\n\t};\n\t\n\t\n\t_api_register( 'rows()', function ( selector, opts ) {\n\t\t// argument shifting\n\t\tif ( selector === undefined ) {\n\t\t\tselector = '';\n\t\t}\n\t\telse if ( $.isPlainObject( selector ) ) {\n\t\t\topts = selector;\n\t\t\tselector = '';\n\t\t}\n\t\n\t\topts = _selector_opts( opts );\n\t\n\t\tvar inst = this.iterator( 'table', function ( settings ) {\n\t\t\treturn __row_selector( settings, selector, opts );\n\t\t}, 1 );\n\t\n\t\t// Want argument shifting here and in __row_selector?\n\t\tinst.selector.rows = selector;\n\t\tinst.selector.opts = opts;\n\t\n\t\treturn inst;\n\t} );\n\t\n\t_api_register( 'rows().nodes()', function () {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\treturn settings.aoData[ row ].nTr || undefined;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'rows().data()', function () {\n\t\treturn this.iterator( true, 'rows', function ( settings, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows, '_aData' );\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\tvar r = settings.aoData[ row ];\n\t\t\treturn type === 'search' ? r._aFilterData : r._aSortData;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\t_fnInvalidate( settings, row, src );\n\t\t} );\n\t} );\n\t\n\t_api_registerPlural( 'rows().indexes()', 'row().index()', function () {\n\t\treturn this.iterator( 'row', function ( settings, row ) {\n\t\t\treturn row;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'rows().ids()', 'row().id()', function ( hash ) {\n\t\tvar a = [];\n\t\tvar context = this.context;\n\t\n\t\t// `iterator` will drop undefined values, but in this case we want them\n\t\tfor ( var i=0, iLen=context.length ; i<iLen ; i++ ) {\n\t\t\tfor ( var j=0, jen=this[i].length ; j<jen ; j++ ) {\n\t\t\t\tvar id = context[i].rowIdFn( context[i].aoData[ this[i][j] ]._aData );\n\t\t\t\ta.push( (hash === true ? '#' : '' )+ id );\n\t\t\t}\n\t\t}\n\t\n\t\treturn new _Api( context, a );\n\t} );\n\t\n\t_api_registerPlural( 'rows().remove()', 'row().remove()', function () {\n\t\tthis.iterator( 'row', function ( settings, row ) {\n\t\t\tvar data = settings.aoData;\n\t\t\tvar rowData = data[ row ];\n\t\n\t\t\t// Delete from the display arrays\n\t\t\tvar idx = settings.aiDisplayMaster.indexOf(row);\n\t\t\tif (idx !== -1) {\n\t\t\t\tsettings.aiDisplayMaster.splice(idx, 1);\n\t\t\t}\n\t\n\t\t\t// For server-side processing tables - subtract the deleted row from the count\n\t\t\tif ( settings._iRecordsDisplay > 0 ) {\n\t\t\t\tsettings._iRecordsDisplay--;\n\t\t\t}\n\t\n\t\t\t// Check for an 'overflow' they case for displaying the table\n\t\t\t_fnLengthOverflow( settings );\n\t\n\t\t\t// Remove the row's ID reference if there is one\n\t\t\tvar id = settings.rowIdFn( rowData._aData );\n\t\t\tif ( id !== undefined ) {\n\t\t\t\tdelete settings.aIds[ id ];\n\t\t\t}\n\t\n\t\t\tdata[row] = null;\n\t\t} );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( 'rows.add()', function ( rows ) {\n\t\tvar newRows = this.iterator( 'table', function ( settings ) {\n\t\t\t\tvar row, i, iLen;\n\t\t\t\tvar out = [];\n\t\n\t\t\t\tfor ( i=0, iLen=rows.length ; i<iLen ; i++ ) {\n\t\t\t\t\trow = rows[i];\n\t\n\t\t\t\t\tif ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {\n\t\t\t\t\t\tout.push( _fnAddTr( settings, row )[0] );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tout.push( _fnAddData( settings, row ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn out;\n\t\t\t}, 1 );\n\t\n\t\t// Return an Api.rows() extended instance, so rows().nodes() etc can be used\n\t\tvar modRows = this.rows( -1 );\n\t\tmodRows.pop();\n\t\t_fnArrayApply(modRows, newRows);\n\t\n\t\treturn modRows;\n\t} );\n\t\n\t\n\t\n\t\n\t\n\t/**\n\t *\n\t */\n\t_api_register( 'row()', function ( selector, opts ) {\n\t\treturn _selector_first( this.rows( selector, opts ) );\n\t} );\n\t\n\t\n\t_api_register( 'row().data()', function ( data ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// Get\n\t\t\treturn ctx.length && this.length && this[0].length ?\n\t\t\t\tctx[0].aoData[ this[0] ]._aData :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// Set\n\t\tvar row = ctx[0].aoData[ this[0] ];\n\t\trow._aData = data;\n\t\n\t\t// If the DOM has an id, and the data source is an array\n\t\tif ( Array.isArray( data ) && row.nTr && row.nTr.id ) {\n\t\t\t_fnSetObjectDataFn( ctx[0].rowId )( data, row.nTr.id );\n\t\t}\n\t\n\t\t// Automatically invalidate\n\t\t_fnInvalidate( ctx[0], this[0], 'data' );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( 'row().node()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif (ctx.length && this.length && this[0].length) {\n\t\t\tvar row = ctx[0].aoData[ this[0] ];\n\t\n\t\t\tif (row && row.nTr) {\n\t\t\t\treturn row.nTr;\n\t\t\t}\n\t\t}\n\t\n\t\treturn null;\n\t} );\n\t\n\t\n\t_api_register( 'row.add()', function ( row ) {\n\t\t// Allow a jQuery object to be passed in - only a single row is added from\n\t\t// it though - the first element in the set\n\t\tif ( row instanceof $ && row.length ) {\n\t\t\trow = row[0];\n\t\t}\n\t\n\t\tvar rows = this.iterator( 'table', function ( settings ) {\n\t\t\tif ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {\n\t\t\t\treturn _fnAddTr( settings, row )[0];\n\t\t\t}\n\t\t\treturn _fnAddData( settings, row );\n\t\t} );\n\t\n\t\t// Return an Api.rows() extended instance, with the newly added row selected\n\t\treturn this.row( rows[0] );\n\t} );\n\t\n\t\n\t$(document).on('plugin-init.dt', function (e, context) {\n\t\tvar api = new _Api( context );\n\t\n\t\tapi.on( 'stateSaveParams.DT', function ( e, settings, d ) {\n\t\t\t// This could be more compact with the API, but it is a lot faster as a simple\n\t\t\t// internal loop\n\t\t\tvar idFn = settings.rowIdFn;\n\t\t\tvar rows = settings.aiDisplayMaster;\n\t\t\tvar ids = [];\n\t\n\t\t\tfor (var i=0 ; i<rows.length ; i++) {\n\t\t\t\tvar rowIdx = rows[i];\n\t\t\t\tvar data = settings.aoData[rowIdx];\n\t\n\t\t\t\tif (data._detailsShow) {\n\t\t\t\t\tids.push( '#' + idFn(data._aData) );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\td.childRows = ids;\n\t\t});\n\t\n\t\t// For future state loads (e.g. with StateRestore)\n\t\tapi.on( 'stateLoaded.DT', function (e, settings, state) {\n\t\t\t__details_state_load( api, state );\n\t\t});\n\t\n\t\t// And the initial load state\n\t\t__details_state_load( api, api.state.loaded() );\n\t});\n\t\n\tvar __details_state_load = function (api, state)\n\t{\n\t\tif ( state && state.childRows ) {\n\t\t\tapi\n\t\t\t\t.rows( state.childRows.map(function (id) {\n\t\t\t\t\t// Escape any `:` characters from the row id. Accounts for\n\t\t\t\t\t// already escaped characters.\n\t\t\t\t\treturn id.replace(/([^:\\\\]*(?:\\\\.[^:\\\\]*)*):/g, \"$1\\\\:\");\n\t\t\t\t}) )\n\t\t\t\t.every( function () {\n\t\t\t\t\t_fnCallbackFire( api.settings()[0], null, 'requestChild', [ this ] )\n\t\t\t\t});\n\t\t}\n\t}\n\t\n\tvar __details_add = function ( ctx, row, data, klass )\n\t{\n\t\t// Convert to array of TR elements\n\t\tvar rows = [];\n\t\tvar addRow = function ( r, k ) {\n\t\t\t// Recursion to allow for arrays of jQuery objects\n\t\t\tif ( Array.isArray( r ) || r instanceof $ ) {\n\t\t\t\tfor ( var i=0, iLen=r.length ; i<iLen ; i++ ) {\n\t\t\t\t\taddRow( r[i], k );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t// If we get a TR element, then just add it directly - up to the dev\n\t\t\t// to add the correct number of columns etc\n\t\t\tif ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) {\n\t\t\t\tr.setAttribute( 'data-dt-row', row.idx );\n\t\t\t\trows.push( r );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Otherwise create a row with a wrapper\n\t\t\t\tvar created = $('<tr><td></td></tr>')\n\t\t\t\t\t.attr( 'data-dt-row', row.idx )\n\t\t\t\t\t.addClass( k );\n\t\t\t\t\n\t\t\t\t$('td', created)\n\t\t\t\t\t.addClass( k )\n\t\t\t\t\t.html( r )[0].colSpan = _fnVisibleColumns( ctx );\n\t\n\t\t\t\trows.push( created[0] );\n\t\t\t}\n\t\t};\n\t\n\t\taddRow( data, klass );\n\t\n\t\tif ( row._details ) {\n\t\t\trow._details.detach();\n\t\t}\n\t\n\t\trow._details = $(rows);\n\t\n\t\t// If the children were already shown, that state should be retained\n\t\tif ( row._detailsShow ) {\n\t\t\trow._details.insertAfter( row.nTr );\n\t\t}\n\t};\n\t\n\t\n\t// Make state saving of child row details async to allow them to be batch processed\n\tvar __details_state = DataTable.util.throttle(\n\t\tfunction (ctx) {\n\t\t\t_fnSaveState( ctx[0] )\n\t\t},\n\t\t500\n\t);\n\t\n\t\n\tvar __details_remove = function ( api, idx )\n\t{\n\t\tvar ctx = api.context;\n\t\n\t\tif ( ctx.length ) {\n\t\t\tvar row = ctx[0].aoData[ idx !== undefined ? idx : api[0] ];\n\t\n\t\t\tif ( row && row._details ) {\n\t\t\t\trow._details.detach();\n\t\n\t\t\t\trow._detailsShow = undefined;\n\t\t\t\trow._details = undefined;\n\t\t\t\t$( row.nTr ).removeClass( 'dt-hasChild' );\n\t\t\t\t__details_state( ctx );\n\t\t\t}\n\t\t}\n\t};\n\t\n\t\n\tvar __details_display = function ( api, show ) {\n\t\tvar ctx = api.context;\n\t\n\t\tif ( ctx.length && api.length ) {\n\t\t\tvar row = ctx[0].aoData[ api[0] ];\n\t\n\t\t\tif ( row._details ) {\n\t\t\t\trow._detailsShow = show;\n\t\n\t\t\t\tif ( show ) {\n\t\t\t\t\trow._details.insertAfter( row.nTr );\n\t\t\t\t\t$( row.nTr ).addClass( 'dt-hasChild' );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\trow._details.detach();\n\t\t\t\t\t$( row.nTr ).removeClass( 'dt-hasChild' );\n\t\t\t\t}\n\t\n\t\t\t\t_fnCallbackFire( ctx[0], null, 'childRow', [ show, api.row( api[0] ) ] )\n\t\n\t\t\t\t__details_events( ctx[0] );\n\t\t\t\t__details_state( ctx );\n\t\t\t}\n\t\t}\n\t};\n\t\n\t\n\tvar __details_events = function ( settings )\n\t{\n\t\tvar api = new _Api( settings );\n\t\tvar namespace = '.dt.DT_details';\n\t\tvar drawEvent = 'draw'+namespace;\n\t\tvar colvisEvent = 'column-sizing'+namespace;\n\t\tvar destroyEvent = 'destroy'+namespace;\n\t\tvar data = settings.aoData;\n\t\n\t\tapi.off( drawEvent +' '+ colvisEvent +' '+ destroyEvent );\n\t\n\t\tif ( _pluck( data, '_details' ).length > 0 ) {\n\t\t\t// On each draw, insert the required elements into the document\n\t\t\tapi.on( drawEvent, function ( e, ctx ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tapi.rows( {page:'current'} ).eq(0).each( function (idx) {\n\t\t\t\t\t// Internal data grab\n\t\t\t\t\tvar row = data[ idx ];\n\t\n\t\t\t\t\tif ( row._detailsShow ) {\n\t\t\t\t\t\trow._details.insertAfter( row.nTr );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} );\n\t\n\t\t\t// Column visibility change - update the colspan\n\t\t\tapi.on( colvisEvent, function ( e, ctx ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\t// Update the colspan for the details rows (note, only if it already has\n\t\t\t\t// a colspan)\n\t\t\t\tvar row, visible = _fnVisibleColumns( ctx );\n\t\n\t\t\t\tfor ( var i=0, iLen=data.length ; i<iLen ; i++ ) {\n\t\t\t\t\trow = data[i];\n\t\n\t\t\t\t\tif ( row && row._details ) {\n\t\t\t\t\t\trow._details.each(function () {\n\t\t\t\t\t\t\tvar el = $(this).children('td');\n\t\n\t\t\t\t\t\t\tif (el.length == 1) {\n\t\t\t\t\t\t\t\tel.attr('colspan', visible);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\n\t\t\t// Table destroyed - nuke any child rows\n\t\t\tapi.on( destroyEvent, function ( e, ctx ) {\n\t\t\t\tif ( settings !== ctx ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tfor ( var i=0, iLen=data.length ; i<iLen ; i++ ) {\n\t\t\t\t\tif ( data[i] && data[i]._details ) {\n\t\t\t\t\t\t__details_remove( api, i );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t};\n\t\n\t// Strings for the method names to help minification\n\tvar _emp = '';\n\tvar _child_obj = _emp+'row().child';\n\tvar _child_mth = _child_obj+'()';\n\t\n\t// data can be:\n\t//  tr\n\t//  string\n\t//  jQuery or array of any of the above\n\t_api_register( _child_mth, function ( data, klass ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length && this.length && ctx[0].aoData[ this[0] ]\n\t\t\t\t? ctx[0].aoData[ this[0] ]._details\n\t\t\t\t: undefined;\n\t\t}\n\t\telse if ( data === true ) {\n\t\t\t// show\n\t\t\tthis.child.show();\n\t\t}\n\t\telse if ( data === false ) {\n\t\t\t// remove\n\t\t\t__details_remove( this );\n\t\t}\n\t\telse if ( ctx.length && this.length ) {\n\t\t\t// set\n\t\t\t__details_add( ctx[0], ctx[0].aoData[ this[0] ], data, klass );\n\t\t}\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.show()',\n\t\t_child_mth+'.show()' // only when `child()` was called with parameters (without\n\t], function () {         // it returns an object and this method is not executed)\n\t\t__details_display( this, true );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.hide()',\n\t\t_child_mth+'.hide()' // only when `child()` was called with parameters (without\n\t], function () {         // it returns an object and this method is not executed)\n\t\t__details_display( this, false );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( [\n\t\t_child_obj+'.remove()',\n\t\t_child_mth+'.remove()' // only when `child()` was called with parameters (without\n\t], function () {           // it returns an object and this method is not executed)\n\t\t__details_remove( this );\n\t\treturn this;\n\t} );\n\t\n\t\n\t_api_register( _child_obj+'.isShown()', function () {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( ctx.length && this.length && ctx[0].aoData[ this[0] ] ) {\n\t\t\t// _detailsShown as false or undefined will fall through to return false\n\t\t\treturn ctx[0].aoData[ this[0] ]._detailsShow || false;\n\t\t}\n\t\treturn false;\n\t} );\n\t\n\t\n\t\n\t/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n\t * Columns\n\t *\n\t * {integer}           - column index (>=0 count from left, <0 count from right)\n\t * \"{integer}:visIdx\"  - visible column index (i.e. translate to column index)  (>=0 count from left, <0 count from right)\n\t * \"{integer}:visible\" - alias for {integer}:visIdx  (>=0 count from left, <0 count from right)\n\t * \"{string}:name\"     - column name\n\t * \"{string}\"          - jQuery selector on column header nodes\n\t *\n\t */\n\t\n\t// can be an array of these items, comma separated list, or an array of comma\n\t// separated lists\n\t\n\tvar __re_column_selector = /^([^:]+)?:(name|title|visIdx|visible)$/;\n\t\n\t\n\t// r1 and r2 are redundant - but it means that the parameters match for the\n\t// iterator callback in columns().data()\n\tvar __columnData = function ( settings, column, r1, r2, rows, type ) {\n\t\tvar a = [];\n\t\tfor ( var row=0, iLen=rows.length ; row<iLen ; row++ ) {\n\t\t\ta.push( _fnGetCellData( settings, rows[row], column, type ) );\n\t\t}\n\t\treturn a;\n\t};\n\t\n\t\n\tvar __column_header = function ( settings, column, row ) {\n\t\tvar header = settings.aoHeader;\n\t\tvar titleRow = settings.titleRow;\n\t\tvar target = null;\n\t\n\t\tif (row !== undefined) {\n\t\t\ttarget = row;\n\t\t}\n\t\telse if (titleRow === true) { // legacy orderCellsTop support\n\t\t\ttarget = 0;\n\t\t}\n\t\telse if (titleRow === false) {\n\t\t\ttarget = header.length - 1;\n\t\t}\n\t\telse if (titleRow !== null) {\n\t\t\ttarget = titleRow;\n\t\t}\n\t\telse {\n\t\t\t// Automatic - find the _last_ unique cell from the top that is not empty (last for\n\t\t\t// backwards compatibility)\n\t\t\tfor (var i=0 ; i<header.length ; i++) {\n\t\t\t\tif (header[i][column].unique && $('.dt-column-title', header[i][column].cell).text()) {\n\t\t\t\t\ttarget = i;\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\tif (target === null) {\n\t\t\t\ttarget = 0;\n\t\t\t}\n\t\t}\n\t\n\t\treturn header[target][column].cell;\n\t};\n\t\n\tvar __column_header_cells = function (header) {\n\t\tvar out = [];\n\t\n\t\tfor (var i=0 ; i<header.length ; i++) {\n\t\t\tfor (var j=0 ; j<header[i].length ; j++) {\n\t\t\t\tvar cell = header[i][j].cell;\n\t\n\t\t\t\tif (!out.includes(cell)) {\n\t\t\t\t\tout.push(cell);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\treturn out;\n\t}\n\t\n\tvar __column_selector = function ( settings, selector, opts )\n\t{\n\t\tvar\n\t\t\tcolumns = settings.aoColumns,\n\t\t\tnames, titles,\n\t\t\tnodes = __column_header_cells(settings.aoHeader);\n\t\t\n\t\tvar run = function ( s ) {\n\t\t\tvar selInt = _intVal( s );\n\t\n\t\t\t// Selector - all\n\t\t\tif ( s === '' ) {\n\t\t\t\treturn _range( columns.length );\n\t\t\t}\n\t\n\t\t\t// Selector - index\n\t\t\tif ( selInt !== null ) {\n\t\t\t\treturn [ selInt >= 0 ?\n\t\t\t\t\tselInt : // Count from left\n\t\t\t\t\tcolumns.length + selInt // Count from right (+ because its a negative value)\n\t\t\t\t];\n\t\t\t}\n\t\n\t\t\t// Selector = function\n\t\t\tif ( typeof s === 'function' ) {\n\t\t\t\tvar rows = _selector_row_indexes( settings, opts );\n\t\n\t\t\t\treturn columns.map(function (col, idx) {\n\t\t\t\t\treturn s(\n\t\t\t\t\t\t\tidx,\n\t\t\t\t\t\t\t__columnData( settings, idx, 0, 0, rows ),\n\t\t\t\t\t\t\t__column_header( settings, idx )\n\t\t\t\t\t\t) ? idx : null;\n\t\t\t\t});\n\t\t\t}\n\t\n\t\t\t// jQuery or string selector\n\t\t\tvar match = typeof s === 'string' ?\n\t\t\t\ts.match( __re_column_selector ) :\n\t\t\t\t'';\n\t\n\t\t\tif ( match ) {\n\t\t\t\tswitch( match[2] ) {\n\t\t\t\t\tcase 'visIdx':\n\t\t\t\t\tcase 'visible':\n\t\t\t\t\t\t// Selector is a column index\n\t\t\t\t\t\tif (match[1] && match[1].match(/^\\d+$/)) {\n\t\t\t\t\t\t\tvar idx = parseInt( match[1], 10 );\n\t\n\t\t\t\t\t\t\t// Visible index given, convert to column index\n\t\t\t\t\t\t\tif ( idx < 0 ) {\n\t\t\t\t\t\t\t\t// Counting from the right\n\t\t\t\t\t\t\t\tvar visColumns = columns.map( function (col,i) {\n\t\t\t\t\t\t\t\t\treturn col.bVisible ? i : null;\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t\treturn [ visColumns[ visColumns.length + idx ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Counting from the left\n\t\t\t\t\t\t\treturn [ _fnVisibleToColumnIndex( settings, idx ) ];\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\treturn columns.map( function (col, idx) {\n\t\t\t\t\t\t\t// Not visible, can't match\n\t\t\t\t\t\t\tif (! col.bVisible) {\n\t\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\tif (col.responsiveVisible === false) {\n\t\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t// Selector\n\t\t\t\t\t\t\tif (match[1]) {\n\t\t\t\t\t\t\t\treturn $(nodes[idx]).filter(match[1]).length > 0 ? idx : null;\n\t\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t\t// `:visible` on its own\n\t\t\t\t\t\t\treturn idx;\n\t\t\t\t\t\t} );\n\t\n\t\t\t\t\tcase 'name':\n\t\t\t\t\t\t// Don't get names, unless needed, and only get once if it is\n\t\t\t\t\t\tif (!names) {\n\t\t\t\t\t\t\tnames = _pluck( columns, 'sName' );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// match by name. `names` is column index complete and in order\n\t\t\t\t\t\treturn names.map( function (name, i) {\n\t\t\t\t\t\t\treturn name === match[1] ? i : null;\n\t\t\t\t\t\t} );\n\t\n\t\t\t\t\tcase 'title':\n\t\t\t\t\t\tif (!titles) {\n\t\t\t\t\t\t\ttitles = _pluck( columns, 'sTitle' );\n\t\t\t\t\t\t}\n\t\n\t\t\t\t\t\t// match by column title\n\t\t\t\t\t\treturn titles.map( function (title, i) {\n\t\t\t\t\t\t\treturn title === match[1] ? i : null;\n\t\t\t\t\t\t} );\n\t\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\t// Cell in the table body\n\t\t\tif ( s.nodeName && s._DT_CellIndex ) {\n\t\t\t\treturn [ s._DT_CellIndex.column ];\n\t\t\t}\n\t\n\t\t\t// jQuery selector on the TH elements for the columns\n\t\t\tvar jqResult = $( nodes )\n\t\t\t\t.filter( s )\n\t\t\t\t.map( function () {\n\t\t\t\t\treturn _fnColumnsFromHeader( this ); // `nodes` is column index complete and in order\n\t\t\t\t} )\n\t\t\t\t.toArray()\n\t\t\t\t.sort(function (a, b) {\n\t\t\t\t\treturn a - b;\n\t\t\t\t});\n\t\n\t\t\tif ( jqResult.length || ! s.nodeName ) {\n\t\t\t\treturn jqResult;\n\t\t\t}\n\t\n\t\t\t// Otherwise a node which might have a `dt-column` data attribute, or be\n\t\t\t// a child or such an element\n\t\t\tvar host = $(s).closest('*[data-dt-column]');\n\t\t\treturn host.length ?\n\t\t\t\t[ host.data('dt-column') ] :\n\t\t\t\t[];\n\t\t};\n\t\n\t\tvar selected = _selector_run( 'column', selector, run, settings, opts );\n\t\n\t\treturn opts.columnOrder && opts.columnOrder === 'index'\n\t\t\t? selected.sort(function (a, b) { return a - b; })\n\t\t\t: selected; // implied\n\t};\n\t\n\t\n\tvar __setColumnVis = function ( settings, column, vis ) {\n\t\tvar\n\t\t\tcols = settings.aoColumns,\n\t\t\tcol  = cols[ column ],\n\t\t\tdata = settings.aoData,\n\t\t\tcells, i, iLen, tr;\n\t\n\t\t// Get\n\t\tif ( vis === undefined ) {\n\t\t\treturn col.bVisible;\n\t\t}\n\t\n\t\t// Set\n\t\t// No change\n\t\tif ( col.bVisible === vis ) {\n\t\t\treturn false;\n\t\t}\n\t\n\t\tif ( vis ) {\n\t\t\t// Insert column\n\t\t\t// Need to decide if we should use appendChild or insertBefore\n\t\t\tvar insertBefore = _pluck(cols, 'bVisible').indexOf(true, column+1);\n\t\n\t\t\tfor ( i=0, iLen=data.length ; i<iLen ; i++ ) {\n\t\t\t\tif (data[i]) {\n\t\t\t\t\ttr = data[i].nTr;\n\t\t\t\t\tcells = data[i].anCells;\n\t\n\t\t\t\t\tif ( tr ) {\n\t\t\t\t\t\t// insertBefore can act like appendChild if 2nd arg is null\n\t\t\t\t\t\ttr.insertBefore( cells[ column ], cells[ insertBefore ] || null );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// Remove column\n\t\t\t$( _pluck( settings.aoData, 'anCells', column ) ).detach();\n\t\t}\n\t\n\t\t// Common actions\n\t\tcol.bVisible = vis;\n\t\n\t\t_colGroup(settings);\n\t\t\n\t\treturn true;\n\t};\n\t\n\t\n\t_api_register( 'columns()', function ( selector, opts ) {\n\t\t// argument shifting\n\t\tif ( selector === undefined ) {\n\t\t\tselector = '';\n\t\t}\n\t\telse if ( $.isPlainObject( selector ) ) {\n\t\t\topts = selector;\n\t\t\tselector = '';\n\t\t}\n\t\n\t\topts = _selector_opts( opts );\n\t\n\t\tvar inst = this.iterator( 'table', function ( settings ) {\n\t\t\treturn __column_selector( settings, selector, opts );\n\t\t}, 1 );\n\t\n\t\t// Want argument shifting here and in _row_selector?\n\t\tinst.selector.cols = selector;\n\t\tinst.selector.opts = opts;\n\t\n\t\treturn inst;\n\t} );\n\t\n\t_api_registerPlural( 'columns().header()', 'column().header()', function ( row ) {\n\t\treturn this.iterator( 'column', function (settings, column) {\n\t\t\treturn __column_header(settings, column, row);\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().footer()', 'column().footer()', function ( row ) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\tvar footer = settings.aoFooter;\n\t\n\t\t\tif (! footer.length) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\n\t\t\treturn settings.aoFooter[row !== undefined ? row : 0][column].cell;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().data()', 'column().data()', function () {\n\t\treturn this.iterator( 'column-rows', __columnData, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().render()', 'column().render()', function ( type ) {\n\t\treturn this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {\n\t\t\treturn __columnData( settings, column, i, j, rows, type );\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().dataSrc()', 'column().dataSrc()', function () {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column].mData;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().cache()', 'column().cache()', function ( type ) {\n\t\treturn this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows,\n\t\t\t\ttype === 'search' ? '_aFilterData' : '_aSortData', column\n\t\t\t);\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().init()', 'column().init()', function () {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column];\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().names()', 'column().name()', function () {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn settings.aoColumns[column].sName;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {\n\t\treturn this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {\n\t\t\treturn _pluck_order( settings.aoData, rows, 'anCells', column ) ;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().titles()', 'column().title()', function (title, row) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\t// Argument shifting\n\t\t\tif (typeof title === 'number') {\n\t\t\t\trow = title;\n\t\t\t\ttitle = undefined;\n\t\t\t}\n\t\n\t\t\tvar span = $('.dt-column-title', this.column(column).header(row));\n\t\n\t\t\tif (title !== undefined) {\n\t\t\t\tspan.html(title);\n\t\t\t\treturn this;\n\t\t\t}\n\t\n\t\t\treturn span.html();\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().types()', 'column().type()', function () {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\tvar colObj = settings.aoColumns[column]\n\t\t\tvar type = colObj.sType;\n\t\n\t\t\t// If the type was invalidated, then resolve it. This actually does\n\t\t\t// all columns at the moment. Would only happen once if getting all\n\t\t\t// column's data types.\n\t\t\tif (! type) {\n\t\t\t\t_fnColumnTypes(settings);\n\t\n\t\t\t\ttype = colObj.sType;\n\t\t\t}\n\t\n\t\t\treturn type;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis, calc ) {\n\t\tvar that = this;\n\t\tvar changed = [];\n\t\tvar ret = this.iterator( 'column', function ( settings, column ) {\n\t\t\tif ( vis === undefined ) {\n\t\t\t\treturn settings.aoColumns[ column ].bVisible;\n\t\t\t} // else\n\t\t\t\n\t\t\tif (__setColumnVis( settings, column, vis )) {\n\t\t\t\tchanged.push(column);\n\t\t\t}\n\t\t} );\n\t\n\t\t// Group the column visibility changes\n\t\tif ( vis !== undefined ) {\n\t\t\tthis.iterator( 'table', function ( settings ) {\n\t\t\t\t// Redraw the header after changes\n\t\t\t\t_fnDrawHead( settings, settings.aoHeader );\n\t\t\t\t_fnDrawHead( settings, settings.aoFooter );\n\t\t\n\t\t\t\t// Update colspan for no records display. Child rows and extensions will use their own\n\t\t\t\t// listeners to do this - only need to update the empty table item here\n\t\t\t\tif ( ! settings.aiDisplay.length ) {\n\t\t\t\t\t$(settings.nTBody).find('td[colspan]').attr('colspan', _fnVisibleColumns(settings));\n\t\t\t\t}\n\t\t\n\t\t\t\t_fnSaveState( settings );\n\t\n\t\t\t\t// Second loop once the first is done for events\n\t\t\t\tthat.iterator( 'column', function ( settings, column ) {\n\t\t\t\t\tif (changed.includes(column)) {\n\t\t\t\t\t\t_fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\n\t\t\t\tif ( changed.length && (calc === undefined || calc) ) {\n\t\t\t\t\tthat.columns.adjust();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\n\t\treturn ret;\n\t} );\n\t\n\t_api_registerPlural( 'columns().widths()', 'column().width()', function () {\n\t\t// Injects a fake row into the table for just a moment so the widths can\n\t\t// be read, regardless of colspan in the header and rows being present in\n\t\t// the body\n\t\tvar columns = this.columns(':visible').count();\n\t\tvar row = $('<tr>').html('<td>' + Array(columns).join('</td><td>') + '</td>');\n\t\n\t\t$(this.table().body()).append(row);\n\t\n\t\tvar widths = row.children().map(function () {\n\t\t\treturn $(this).outerWidth();\n\t\t});\n\t\n\t\trow.remove();\n\t\t\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\tvar visIdx = _fnColumnIndexToVisible( settings, column );\n\t\n\t\t\treturn visIdx !== null ? widths[visIdx] : 0;\n\t\t}, 1);\n\t} );\n\t\n\t_api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) {\n\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\treturn type === 'visible' ?\n\t\t\t\t_fnColumnIndexToVisible( settings, column ) :\n\t\t\t\tcolumn;\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'columns.adjust()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t// Force a column sizing to happen with a manual call - otherwise it can skip\n\t\t\t// if the size hasn't changed\n\t\t\tsettings.containerWidth = -1;\n\t\n\t\t\t_fnAdjustColumnSizing( settings );\n\t\t}, 1 );\n\t} );\n\t\n\t_api_register( 'column.index()', function ( type, idx ) {\n\t\tif ( this.context.length !== 0 ) {\n\t\t\tvar ctx = this.context[0];\n\t\n\t\t\tif ( type === 'fromVisible' || type === 'toData' ) {\n\t\t\t\treturn _fnVisibleToColumnIndex( ctx, idx );\n\t\t\t}\n\t\t\telse if ( type === 'fromData' || type === 'toVisible' ) {\n\t\t\t\treturn _fnColumnIndexToVisible( ctx, idx );\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t_api_register( 'column()', function ( selector, opts ) {\n\t\treturn _selector_first( this.columns( selector, opts ) );\n\t} );\n\t\n\tvar __cell_selector = function ( settings, selector, opts )\n\t{\n\t\tvar data = settings.aoData;\n\t\tvar rows = _selector_row_indexes( settings, opts );\n\t\tvar cells = _removeEmpty( _pluck_order( data, rows, 'anCells' ) );\n\t\tvar allCells = $(_flatten( [], cells ));\n\t\tvar row;\n\t\tvar columns = settings.aoColumns.length;\n\t\tvar a, i, iLen, j, o, host;\n\t\n\t\tvar run = function ( s ) {\n\t\t\tvar fnSelector = typeof s === 'function';\n\t\n\t\t\tif ( s === null || s === undefined || fnSelector ) {\n\t\t\t\t// All cells and function selectors\n\t\t\t\ta = [];\n\t\n\t\t\t\tfor ( i=0, iLen=rows.length ; i<iLen ; i++ ) {\n\t\t\t\t\trow = rows[i];\n\t\n\t\t\t\t\tfor ( j=0 ; j<columns ; j++ ) {\n\t\t\t\t\t\to = {\n\t\t\t\t\t\t\trow: row,\n\t\t\t\t\t\t\tcolumn: j\n\t\t\t\t\t\t};\n\t\n\t\t\t\t\t\tif ( fnSelector ) {\n\t\t\t\t\t\t\t// Selector - function\n\t\t\t\t\t\t\thost = data[ row ];\n\t\n\t\t\t\t\t\t\tif ( s( o, _fnGetCellData(settings, row, j), host.anCells ? host.anCells[j] : null ) ) {\n\t\t\t\t\t\t\t\ta.push( o );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t// Selector - all\n\t\t\t\t\t\t\ta.push( o );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn a;\n\t\t\t}\n\t\t\t\n\t\t\t// Selector - index\n\t\t\tif ( $.isPlainObject( s ) ) {\n\t\t\t\t// Valid cell index and its in the array of selectable rows\n\t\t\t\treturn s.column !== undefined && s.row !== undefined && rows.indexOf(s.row) !== -1 ?\n\t\t\t\t\t[s] :\n\t\t\t\t\t[];\n\t\t\t}\n\t\n\t\t\t// Selector - jQuery filtered cells\n\t\t\tvar jqResult = allCells\n\t\t\t\t.filter( s )\n\t\t\t\t.map( function (i, el) {\n\t\t\t\t\treturn { // use a new object, in case someone changes the values\n\t\t\t\t\t\trow:    el._DT_CellIndex.row,\n\t\t\t\t\t\tcolumn: el._DT_CellIndex.column\n\t\t\t\t\t};\n\t\t\t\t} )\n\t\t\t\t.toArray();\n\t\n\t\t\tif ( jqResult.length || ! s.nodeName ) {\n\t\t\t\treturn jqResult;\n\t\t\t}\n\t\n\t\t\t// Otherwise the selector is a node, and there is one last option - the\n\t\t\t// element might be a child of an element which has dt-row and dt-column\n\t\t\t// data attributes\n\t\t\thost = $(s).closest('*[data-dt-row]');\n\t\t\treturn host.length ?\n\t\t\t\t[ {\n\t\t\t\t\trow: host.data('dt-row'),\n\t\t\t\t\tcolumn: host.data('dt-column')\n\t\t\t\t} ] :\n\t\t\t\t[];\n\t\t};\n\t\n\t\treturn _selector_run( 'cell', selector, run, settings, opts );\n\t};\n\t\n\t\n\t\n\t\n\t_api_register( 'cells()', function ( rowSelector, columnSelector, opts ) {\n\t\t// Argument shifting\n\t\tif ( $.isPlainObject( rowSelector ) ) {\n\t\t\t// Indexes\n\t\t\tif ( rowSelector.row === undefined ) {\n\t\t\t\t// Selector options in first parameter\n\t\t\t\topts = rowSelector;\n\t\t\t\trowSelector = null;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Cell index objects in first parameter\n\t\t\t\topts = columnSelector;\n\t\t\t\tcolumnSelector = null;\n\t\t\t}\n\t\t}\n\t\tif ( $.isPlainObject( columnSelector ) ) {\n\t\t\topts = columnSelector;\n\t\t\tcolumnSelector = null;\n\t\t}\n\t\n\t\t// Cell selector\n\t\tif ( columnSelector === null || columnSelector === undefined ) {\n\t\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t\treturn __cell_selector( settings, rowSelector, _selector_opts( opts ) );\n\t\t\t} );\n\t\t}\n\t\n\t\t// The default built in options need to apply to row and columns\n\t\tvar internalOpts = opts ? {\n\t\t\tpage: opts.page,\n\t\t\torder: opts.order,\n\t\t\tsearch: opts.search\n\t\t} : {};\n\t\n\t\t// Row + column selector\n\t\tvar columns = this.columns( columnSelector, internalOpts );\n\t\tvar rows = this.rows( rowSelector, internalOpts );\n\t\tvar i, iLen, j, jen;\n\t\n\t\tvar cellsNoOpts = this.iterator( 'table', function ( settings, idx ) {\n\t\t\tvar a = [];\n\t\n\t\t\tfor ( i=0, iLen=rows[idx].length ; i<iLen ; i++ ) {\n\t\t\t\tfor ( j=0, jen=columns[idx].length ; j<jen ; j++ ) {\n\t\t\t\t\ta.push( {\n\t\t\t\t\t\trow:    rows[idx][i],\n\t\t\t\t\t\tcolumn: columns[idx][j]\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\n\t\t\treturn a;\n\t\t}, 1 );\n\t\n\t\t// There is currently only one extension which uses a cell selector extension\n\t\t// It is a _major_ performance drag to run this if it isn't needed, so this is\n\t\t// an extension specific check at the moment\n\t\tvar cells = opts && opts.selected ?\n\t\t\tthis.cells( cellsNoOpts, opts ) :\n\t\t\tcellsNoOpts;\n\t\n\t\t$.extend( cells.selector, {\n\t\t\tcols: columnSelector,\n\t\t\trows: rowSelector,\n\t\t\topts: opts\n\t\t} );\n\t\n\t\treturn cells;\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().nodes()', 'cell().node()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\tvar data = settings.aoData[ row ];\n\t\n\t\t\treturn data && data.anCells ?\n\t\t\t\tdata.anCells[ column ] :\n\t\t\t\tundefined;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_register( 'cells().data()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn _fnGetCellData( settings, row, column );\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().cache()', 'cell().cache()', function ( type ) {\n\t\ttype = type === 'search' ? '_aFilterData' : '_aSortData';\n\t\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn settings.aoData[ row ][ type ][ column ];\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().render()', 'cell().render()', function ( type ) {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn _fnGetCellData( settings, row, column, type );\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().indexes()', 'cell().index()', function () {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\treturn {\n\t\t\t\trow: row,\n\t\t\t\tcolumn: column,\n\t\t\t\tcolumnVisible: _fnColumnIndexToVisible( settings, column )\n\t\t\t};\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_registerPlural( 'cells().invalidate()', 'cell().invalidate()', function ( src ) {\n\t\treturn this.iterator( 'cell', function ( settings, row, column ) {\n\t\t\t_fnInvalidate( settings, row, src, column );\n\t\t} );\n\t} );\n\t\n\t\n\t\n\t_api_register( 'cell()', function ( rowSelector, columnSelector, opts ) {\n\t\treturn _selector_first( this.cells( rowSelector, columnSelector, opts ) );\n\t} );\n\t\n\t\n\t_api_register( 'cell().data()', function ( data ) {\n\t\tvar ctx = this.context;\n\t\tvar cell = this[0];\n\t\n\t\tif ( data === undefined ) {\n\t\t\t// Get\n\t\t\treturn ctx.length && cell.length ?\n\t\t\t\t_fnGetCellData( ctx[0], cell[0].row, cell[0].column ) :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// Set\n\t\t_fnSetCellData( ctx[0], cell[0].row, cell[0].column, data );\n\t\t_fnInvalidate( ctx[0], cell[0].row, 'data', cell[0].column );\n\t\n\t\treturn this;\n\t} );\n\t\n\t\n\t\n\t/**\n\t * Get current ordering (sorting) that has been applied to the table.\n\t *\n\t * @returns {array} 2D array containing the sorting information for the first\n\t *   table in the current context. Each element in the parent array represents\n\t *   a column being sorted upon (i.e. multi-sorting with two columns would have\n\t *   2 inner arrays). The inner arrays may have 2 or 3 elements. The first is\n\t *   the column index that the sorting condition applies to, the second is the\n\t *   direction of the sort (`desc` or `asc`) and, optionally, the third is the\n\t *   index of the sorting order from the `column.sorting` initialisation array.\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {integer} order Column index to sort upon.\n\t * @param {string} direction Direction of the sort to be applied (`asc` or `desc`)\n\t * @returns {DataTables.Api} this\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {array} order 1D array of sorting information to be applied.\n\t * @param {array} [...] Optional additional sorting conditions\n\t * @returns {DataTables.Api} this\n\t *//**\n\t * Set the ordering for the table.\n\t *\n\t * @param {array} order 2D array of sorting information to be applied.\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'order()', function ( order, dir ) {\n\t\tvar ctx = this.context;\n\t\tvar args = Array.prototype.slice.call( arguments );\n\t\n\t\tif ( order === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length !== 0 ?\n\t\t\t\tctx[0].aaSorting :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// set\n\t\tif ( typeof order === 'number' ) {\n\t\t\t// Simple column / direction passed in\n\t\t\torder = [ [ order, dir ] ];\n\t\t}\n\t\telse if ( args.length > 1 ) {\n\t\t\t// Arguments passed in (list of 1D arrays)\n\t\t\torder = args;\n\t\t}\n\t\t// otherwise a 2D array was passed in\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tvar resolved = [];\n\t\t\t_fnSortResolve(settings, resolved, order);\n\t\n\t\t\tsettings.aaSorting = resolved;\n\t\t} );\n\t} );\n\t\n\t\n\t/**\n\t * Attach a sort listener to an element for a given column\n\t *\n\t * @param {node|jQuery|string} node Identifier for the element(s) to attach the\n\t *   listener to. This can take the form of a single DOM node, a jQuery\n\t *   collection of nodes or a jQuery selector which will identify the node(s).\n\t * @param {integer} column the column that a click on this node will sort on\n\t * @param {function} [callback] callback function when sort is run\n\t * @returns {DataTables.Api} this\n\t */\n\t_api_register( 'order.listener()', function ( node, column, callback ) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnSortAttachListener(settings, node, {}, column, callback);\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'order.fixed()', function ( set ) {\n\t\tif ( ! set ) {\n\t\t\tvar ctx = this.context;\n\t\t\tvar fixed = ctx.length ?\n\t\t\t\tctx[0].aaSortingFixed :\n\t\t\t\tundefined;\n\t\n\t\t\treturn Array.isArray( fixed ) ?\n\t\t\t\t{ pre: fixed } :\n\t\t\t\tfixed;\n\t\t}\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tsettings.aaSortingFixed = $.extend( true, {}, set );\n\t\t} );\n\t} );\n\t\n\t\n\t// Order by the selected column(s)\n\t_api_register( [\n\t\t'columns().order()',\n\t\t'column().order()'\n\t], function ( dir ) {\n\t\tvar that = this;\n\t\n\t\tif ( ! dir ) {\n\t\t\treturn this.iterator( 'column', function ( settings, idx ) {\n\t\t\t\tvar sort = _fnSortFlatten( settings );\n\t\n\t\t\t\tfor ( var i=0, iLen=sort.length ; i<iLen ; i++ ) {\n\t\t\t\t\tif ( sort[i].col === idx ) {\n\t\t\t\t\t\treturn sort[i].dir;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\treturn null;\n\t\t\t}, 1 );\n\t\t}\n\t\telse {\n\t\t\treturn this.iterator( 'table', function ( settings, i ) {\n\t\t\t\tsettings.aaSorting = that[i].map( function (col) {\n\t\t\t\t\treturn [ col, dir ];\n\t\t\t\t} );\n\t\t\t} );\n\t\t}\n\t} );\n\t\n\t_api_registerPlural('columns().orderable()', 'column().orderable()', function ( directions ) {\n\t\treturn this.iterator( 'column', function ( settings, idx ) {\n\t\t\tvar col = settings.aoColumns[idx];\n\t\n\t\t\treturn directions ?\n\t\t\t\tcol.asSorting :\n\t\t\t\tcol.bSortable;\n\t\t}, 1 );\n\t} );\n\t\n\t\n\t_api_register( 'processing()', function ( show ) {\n\t\treturn this.iterator( 'table', function ( ctx ) {\n\t\t\t_fnProcessingDisplay( ctx, show );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'search()', function ( input, regex, smart, caseInsen ) {\n\t\tvar ctx = this.context;\n\t\n\t\tif ( input === undefined ) {\n\t\t\t// get\n\t\t\treturn ctx.length !== 0 ?\n\t\t\t\tctx[0].oPreviousSearch.search :\n\t\t\t\tundefined;\n\t\t}\n\t\n\t\t// set\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( ! settings.oFeatures.bFilter ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\tif (typeof regex === 'object') {\n\t\t\t\t// New style options to pass to the search builder\n\t\t\t\t_fnFilterComplete( settings, $.extend( settings.oPreviousSearch, regex, {\n\t\t\t\t\tsearch: input\n\t\t\t\t} ) );\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Compat for the old options\n\t\t\t\t_fnFilterComplete( settings, $.extend( settings.oPreviousSearch, {\n\t\t\t\t\tsearch: input,\n\t\t\t\t\tregex:  regex === null ? false : regex,\n\t\t\t\t\tsmart:  smart === null ? true  : smart,\n\t\t\t\t\tcaseInsensitive: caseInsen === null ? true : caseInsen\n\t\t\t\t} ) );\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t_api_register( 'search.fixed()', function ( name, search ) {\n\t\tvar ret = this.iterator( true, 'table', function ( settings ) {\n\t\t\tvar fixed = settings.searchFixed;\n\t\n\t\t\tif (! name) {\n\t\t\t\treturn Object.keys(fixed);\n\t\t\t}\n\t\t\telse if (search === undefined) {\n\t\t\t\treturn fixed[name];\n\t\t\t}\n\t\t\telse if (search === null) {\n\t\t\t\tdelete fixed[name];\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfixed[name] = search;\n\t\t\t}\n\t\n\t\t\treturn this;\n\t\t} );\n\t\n\t\treturn name !== undefined && search === undefined\n\t\t\t? ret[0]\n\t\t\t: ret;\n\t} );\n\t\n\t_api_registerPlural(\n\t\t'columns().search()',\n\t\t'column().search()',\n\t\tfunction ( input, regex, smart, caseInsen ) {\n\t\t\treturn this.iterator( 'column', function ( settings, column ) {\n\t\t\t\tvar preSearch = settings.aoPreSearchCols;\n\t\n\t\t\t\tif ( input === undefined ) {\n\t\t\t\t\t// get\n\t\t\t\t\treturn preSearch[ column ].search;\n\t\t\t\t}\n\t\n\t\t\t\t// set\n\t\t\t\tif ( ! settings.oFeatures.bFilter ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\tif (typeof regex === 'object') {\n\t\t\t\t\t// New style options to pass to the search builder\n\t\t\t\t\t$.extend( preSearch[ column ], regex, {\n\t\t\t\t\t\tsearch: input\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Old style (with not all options available)\n\t\t\t\t\t$.extend( preSearch[ column ], {\n\t\t\t\t\t\tsearch: input,\n\t\t\t\t\t\tregex:  regex === null ? false : regex,\n\t\t\t\t\t\tsmart:  smart === null ? true  : smart,\n\t\t\t\t\t\tcaseInsensitive: caseInsen === null ? true : caseInsen\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\n\t\t\t\t_fnFilterComplete( settings, settings.oPreviousSearch );\n\t\t\t} );\n\t\t}\n\t);\n\t\n\t_api_register([\n\t\t\t'columns().search.fixed()',\n\t\t\t'column().search.fixed()'\n\t\t],\n\t\tfunction ( name, search ) {\n\t\t\tvar ret = this.iterator( true, 'column', function ( settings, colIdx ) {\n\t\t\t\tvar fixed = settings.aoColumns[colIdx].searchFixed;\n\t\n\t\t\t\tif (! name) {\n\t\t\t\t\treturn Object.keys(fixed);\n\t\t\t\t}\n\t\t\t\telse if (search === undefined) {\n\t\t\t\t\treturn fixed[name] || null;\n\t\t\t\t}\n\t\t\t\telse if (search === null) {\n\t\t\t\t\tdelete fixed[name];\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tfixed[name] = search;\n\t\t\t\t}\n\t\n\t\t\t\treturn this;\n\t\t\t} );\n\t\n\t\t\treturn name !== undefined && search === undefined\n\t\t\t\t? ret[0]\n\t\t\t\t: ret;\n\t\t}\n\t);\n\t/*\n\t * State API methods\n\t */\n\t\n\t_api_register( 'state()', function ( set, ignoreTime ) {\n\t\t// getter\n\t\tif ( ! set ) {\n\t\t\treturn this.context.length ?\n\t\t\t\tthis.context[0].oSavedState :\n\t\t\t\tnull;\n\t\t}\n\t\n\t\tvar setMutate = $.extend( true, {}, set );\n\t\n\t\t// setter\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tif ( ignoreTime !== false ) {\n\t\t\t\tsetMutate.time = +new Date() + 100;\n\t\t\t}\n\t\n\t\t\t_fnImplementState( settings, setMutate, function(){} );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'state.clear()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t// Save an empty object\n\t\t\tsettings.fnStateSaveCallback.call( settings.oInstance, settings, {} );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'state.loaded()', function () {\n\t\treturn this.context.length ?\n\t\t\tthis.context[0].oLoadedState :\n\t\t\tnull;\n\t} );\n\t\n\t\n\t_api_register( 'state.save()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnSaveState( settings );\n\t\t} );\n\t} );\n\t\n\t// Can be assigned in DateTable.use() - note luxon and moment vars are in helpers.js\n\tvar __bootstrap;\n\tvar __foundation;\n\t\n\t/**\n\t * Set the libraries that DataTables uses, or the global objects.\n\t * Note that the arguments can be either way around (legacy support)\n\t * and the second is optional. See docs.\n\t */\n\tDataTable.use = function (arg1, arg2) {\n\t\t// Reverse arguments for legacy support\n\t\tvar module = typeof arg1 === 'string'\n\t\t\t? arg2\n\t\t\t: arg1;\n\t\tvar type = typeof arg2 === 'string'\n\t\t\t? arg2\n\t\t\t: arg1;\n\t\n\t\t// Getter\n\t\tif (module === undefined && typeof type === 'string') {\n\t\t\tswitch (type) {\n\t\t\t\tcase 'lib':\n\t\t\t\tcase 'jq':\n\t\t\t\t\treturn $;\n\t\n\t\t\t\tcase 'win':\n\t\t\t\t\treturn window;\n\t\n\t\t\t\tcase 'datetime':\n\t\t\t\t\treturn DataTable.DateTime;\n\t\n\t\t\t\tcase 'luxon':\n\t\t\t\t\treturn __luxon;\n\t\n\t\t\t\tcase 'moment':\n\t\t\t\t\treturn __moment;\n\t\n\t\t\t\tcase 'bootstrap':\n\t\t\t\t\t// Use local if set, otherwise try window, which could be undefined\n\t\t\t\t\treturn __bootstrap || window.bootstrap;\n\t\n\t\t\t\tcase 'foundation':\n\t\t\t\t\t// Ditto\n\t\t\t\t\treturn __foundation || window.Foundation;\n\t\n\t\t\t\tdefault:\n\t\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\n\t\t// Setter\n\t\tif (type === 'lib' || type === 'jq' || (module && module.fn && module.fn.jquery)) {\n\t\t\t$ = module;\n\t\t}\n\t\telse if (type === 'win' || (module && module.document)) {\n\t\t\twindow = module;\n\t\t\tdocument = module.document;\n\t\t}\n\t\telse if (type === 'datetime' || (module && module.type === 'DateTime')) {\n\t\t\tDataTable.DateTime = module;\n\t\t}\n\t\telse if (type === 'luxon' || (module && module.FixedOffsetZone)) {\n\t\t\t__luxon = module;\n\t\t}\n\t\telse if (type === 'moment' || (module && module.isMoment)) {\n\t\t\t__moment = module;\n\t\t}\n\t\telse if (type === 'bootstrap' || (module && module.Modal && module.Modal.NAME === 'modal'))\n\t\t{\n\t\t\t// This is currently for BS5 only. BS3/4 attach to jQuery, so no need to use `.use()`\n\t\t\t__bootstrap = module;\n\t\t}\n\t\telse if (type === 'foundation' || (module && module.Reveal)) {\n\t\t\t__foundation = module;\n\t\t}\n\t}\n\t\n\t/**\n\t * CommonJS factory function pass through. This will check if the arguments\n\t * given are a window object or a jQuery object. If so they are set\n\t * accordingly.\n\t * @param {*} root Window\n\t * @param {*} jq jQUery\n\t * @returns {boolean} Indicator\n\t */\n\tDataTable.factory = function (root, jq) {\n\t\tvar is = false;\n\t\n\t\t// Test if the first parameter is a window object\n\t\tif (root && root.document) {\n\t\t\twindow = root;\n\t\t\tdocument = root.document;\n\t\t}\n\t\n\t\t// Test if the second parameter is a jQuery object\n\t\tif (jq && jq.fn && jq.fn.jquery) {\n\t\t\t$ = jq;\n\t\t\tis = true;\n\t\t}\n\t\n\t\treturn is;\n\t}\n\t\n\t/**\n\t * Provide a common method for plug-ins to check the version of DataTables being\n\t * used, in order to ensure compatibility.\n\t *\n\t *  @param {string} version Version string to check for, in the format \"X.Y.Z\".\n\t *    Note that the formats \"X\" and \"X.Y\" are also acceptable.\n\t *  @param {string} [version2=current DataTables version] As above, but optional.\n\t *   If not given the current DataTables version will be used.\n\t *  @returns {boolean} true if this version of DataTables is greater or equal to\n\t *    the required version, or false if this version of DataTales is not\n\t *    suitable\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    alert( $.fn.dataTable.versionCheck( '1.9.0' ) );\n\t */\n\tDataTable.versionCheck = function( version, version2 )\n\t{\n\t\tvar aThis = version2 ?\n\t\t\tversion2.split('.') :\n\t\t\tDataTable.version.split('.');\n\t\tvar aThat = version.split('.');\n\t\tvar iThis, iThat;\n\t\n\t\tfor ( var i=0, iLen=aThat.length ; i<iLen ; i++ ) {\n\t\t\tiThis = parseInt( aThis[i], 10 ) || 0;\n\t\t\tiThat = parseInt( aThat[i], 10 ) || 0;\n\t\n\t\t\t// Parts are the same, keep comparing\n\t\t\tif (iThis === iThat) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\n\t\t\t// Parts are different, return immediately\n\t\t\treturn iThis > iThat;\n\t\t}\n\t\n\t\treturn true;\n\t};\n\t\n\t\n\t/**\n\t * Check if a `<table>` node is a DataTable table already or not.\n\t *\n\t *  @param {node|jquery|string} table Table node, jQuery object or jQuery\n\t *      selector for the table to test. Note that if more than more than one\n\t *      table is passed on, only the first will be checked\n\t *  @returns {boolean} true the table given is a DataTable, or false otherwise\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    if ( ! $.fn.DataTable.isDataTable( '#example' ) ) {\n\t *      $('#example').dataTable();\n\t *    }\n\t */\n\tDataTable.isDataTable = function ( table )\n\t{\n\t\tvar t = $(table).get(0);\n\t\tvar is = false;\n\t\n\t\tif ( table instanceof DataTable.Api ) {\n\t\t\treturn true;\n\t\t}\n\t\n\t\t$.each( DataTable.settings, function (i, o) {\n\t\t\tvar head = o.nScrollHead ? $('table', o.nScrollHead)[0] : null;\n\t\t\tvar foot = o.nScrollFoot ? $('table', o.nScrollFoot)[0] : null;\n\t\n\t\t\tif ( o.nTable === t || head === t || foot === t ) {\n\t\t\t\tis = true;\n\t\t\t}\n\t\t} );\n\t\n\t\treturn is;\n\t};\n\t\n\t\n\t/**\n\t * Get all DataTable tables that have been initialised - optionally you can\n\t * select to get only currently visible tables.\n\t *\n\t *  @param {boolean} [visible=false] Flag to indicate if you want all (default)\n\t *    or visible tables only.\n\t *  @returns {array} Array of `table` nodes (not DataTable instances) which are\n\t *    DataTables\n\t *  @static\n\t *  @dtopt API-Static\n\t *\n\t *  @example\n\t *    $.each( $.fn.dataTable.tables(true), function () {\n\t *      $(table).DataTable().columns.adjust();\n\t *    } );\n\t */\n\tDataTable.tables = function ( visible )\n\t{\n\t\tvar api = false;\n\t\n\t\tif ( $.isPlainObject( visible ) ) {\n\t\t\tapi = visible.api;\n\t\t\tvisible = visible.visible;\n\t\t}\n\t\n\t\tvar a = DataTable.settings\n\t\t\t.filter( function (o) {\n\t\t\t\treturn !visible || (visible && $(o.nTable).is(':visible')) \n\t\t\t\t\t? true\n\t\t\t\t\t: false;\n\t\t\t} )\n\t\t\t.map( function (o) {\n\t\t\t\treturn o.nTable;\n\t\t\t});\n\t\n\t\treturn api ?\n\t\t\tnew _Api( a ) :\n\t\t\ta;\n\t};\n\t\n\t\n\t/**\n\t * Convert from camel case parameters to Hungarian notation. This is made public\n\t * for the extensions to provide the same ability as DataTables core to accept\n\t * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase\n\t * parameters.\n\t *\n\t *  @param {object} src The model object which holds all parameters that can be\n\t *    mapped.\n\t *  @param {object} user The object to convert from camel case to Hungarian.\n\t *  @param {boolean} force When set to `true`, properties which already have a\n\t *    Hungarian value in the `user` object will be overwritten. Otherwise they\n\t *    won't be.\n\t */\n\tDataTable.camelToHungarian = _fnCamelToHungarian;\n\t\n\t\n\t\n\t/**\n\t *\n\t */\n\t_api_register( '$()', function ( selector, opts ) {\n\t\tvar\n\t\t\trows   = this.rows( opts ).nodes(), // Get all rows\n\t\t\tjqRows = $(rows);\n\t\n\t\treturn $( [].concat(\n\t\t\tjqRows.filter( selector ).toArray(),\n\t\t\tjqRows.find( selector ).toArray()\n\t\t) );\n\t} );\n\t\n\t\n\t// jQuery functions to operate on the tables\n\t$.each( [ 'on', 'one', 'off' ], function (i, key) {\n\t\t_api_register( key+'()', function ( /* event, handler */ ) {\n\t\t\tvar args = Array.prototype.slice.call(arguments);\n\t\n\t\t\t// Add the `dt` namespace automatically if it isn't already present\n\t\t\targs[0] = args[0].split( /\\s/ ).map( function ( e ) {\n\t\t\t\treturn ! e.match(/\\.dt\\b/) ?\n\t\t\t\t\te+'.dt' :\n\t\t\t\t\te;\n\t\t\t\t} ).join( ' ' );\n\t\n\t\t\tvar inst = $( this.tables().nodes() );\n\t\t\tinst[key].apply( inst, args );\n\t\t\treturn this;\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'clear()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnClearTable( settings );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'error()', function (msg) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\t_fnLog( settings, 0, msg );\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'settings()', function () {\n\t\treturn new _Api( this.context, this.context );\n\t} );\n\t\n\t\n\t_api_register( 'init()', function () {\n\t\tvar ctx = this.context;\n\t\treturn ctx.length ? ctx[0].oInit : null;\n\t} );\n\t\n\t\n\t_api_register( 'data()', function () {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\treturn _pluck( settings.aoData, '_aData' );\n\t\t} ).flatten();\n\t} );\n\t\n\t\n\t_api_register( 'trigger()', function ( name, args, bubbles ) {\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\treturn _fnCallbackFire( settings, null, name, args, bubbles );\n\t\t} ).flatten();\n\t} );\n\t\n\t\n\t_api_register( 'ready()', function ( fn ) {\n\t\tvar ctx = this.context;\n\t\n\t\t// Get status of first table\n\t\tif (! fn) {\n\t\t\treturn ctx.length\n\t\t\t\t? (ctx[0]._bInitComplete || false)\n\t\t\t\t: null;\n\t\t}\n\t\n\t\t// Function to run either once the table becomes ready or\n\t\t// immediately if it is already ready.\n\t\treturn this.tables().every(function () {\n\t\t\tvar api = this;\n\t\n\t\t\tif (this.context[0]._bInitComplete) {\n\t\t\t\tfn.call(api);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.on('init.dt.DT', function () {\n\t\t\t\t\tfn.call(api);\n\t\t\t\t});\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t_api_register( 'destroy()', function ( remove ) {\n\t\tremove = remove || false;\n\t\n\t\treturn this.iterator( 'table', function ( settings ) {\n\t\t\tvar classes   = settings.oClasses;\n\t\t\tvar table     = settings.nTable;\n\t\t\tvar tbody     = settings.nTBody;\n\t\t\tvar thead     = settings.nTHead;\n\t\t\tvar tfoot     = settings.nTFoot;\n\t\t\tvar jqTable   = $(table);\n\t\t\tvar jqTbody   = $(tbody);\n\t\t\tvar jqWrapper = $(settings.nTableWrapper);\n\t\t\tvar rows      = settings.aoData.map( function (r) { return r ? r.nTr : null; } );\n\t\t\tvar orderClasses = classes.order;\n\t\n\t\t\t// Flag to note that the table is currently being destroyed - no action\n\t\t\t// should be taken\n\t\t\tsettings.bDestroying = true;\n\t\n\t\t\t// Fire off the destroy callbacks for plug-ins etc\n\t\t\t_fnCallbackFire( settings, \"aoDestroyCallback\", \"destroy\", [settings], true );\n\t\n\t\t\t// If not being removed from the document, make all columns visible\n\t\t\tif ( ! remove ) {\n\t\t\t\tnew _Api( settings ).columns().visible( true );\n\t\t\t}\n\t\n\t\t\t// Container width change listener\n\t\t\tif (settings.resizeObserver) {\n\t\t\t\tsettings.resizeObserver.disconnect();\n\t\t\t}\n\t\n\t\t\t// Blitz all `DT` namespaced events (these are internal events, the\n\t\t\t// lowercase, `dt` events are user subscribed and they are responsible\n\t\t\t// for removing them\n\t\t\tjqWrapper.off('.DT').find(':not(tbody *)').off('.DT');\n\t\t\t$(window).off('.DT-'+settings.sInstance);\n\t\n\t\t\t// When scrolling we had to break the table up - restore it\n\t\t\tif ( table != thead.parentNode ) {\n\t\t\t\tjqTable.children('thead').detach();\n\t\t\t\tjqTable.append( thead );\n\t\t\t}\n\t\n\t\t\tif ( tfoot && table != tfoot.parentNode ) {\n\t\t\t\tjqTable.children('tfoot').detach();\n\t\t\t\tjqTable.append( tfoot );\n\t\t\t}\n\t\n\t\t\t// Clean up the header / footer\n\t\t\tcleanHeader(thead, 'header');\n\t\t\tcleanHeader(tfoot, 'footer');\n\t\t\tsettings.colgroup.remove();\n\t\n\t\t\tsettings.aaSorting = [];\n\t\t\tsettings.aaSortingFixed = [];\n\t\t\t_fnSortingClasses( settings );\n\t\n\t\t\t$(jqTable).find('th, td').removeClass(\n\t\t\t\t$.map(DataTable.ext.type.className, function (v) {\n\t\t\t\t\treturn v;\n\t\t\t\t}).join(' ')\n\t\t\t);\n\t\n\t\t\t$('th, td', thead)\n\t\t\t\t.removeClass(\n\t\t\t\t\torderClasses.none + ' ' +\n\t\t\t\t\torderClasses.canAsc + ' ' +\n\t\t\t\t\torderClasses.canDesc + ' ' +\n\t\t\t\t\torderClasses.isAsc + ' ' +\n\t\t\t\t\torderClasses.isDesc\n\t\t\t\t)\n\t\t\t\t.css('width', '')\n\t\t\t\t.removeAttr('aria-sort');\n\t\n\t\t\t// Add the TR elements back into the table in their original order\n\t\t\tjqTbody.children().detach();\n\t\t\tjqTbody.append( rows );\n\t\n\t\t\tvar orig = settings.nTableWrapper.parentNode;\n\t\t\tvar insertBefore = settings.nTableWrapper.nextSibling;\n\t\n\t\t\t// Remove the DataTables generated nodes, events and classes\n\t\t\tvar removedMethod = remove ? 'remove' : 'detach';\n\t\t\tjqTable[ removedMethod ]();\n\t\t\tjqWrapper[ removedMethod ]();\n\t\n\t\t\t// If we need to reattach the table to the document\n\t\t\tif ( ! remove && orig ) {\n\t\t\t\t// insertBefore acts like appendChild if !arg[1]\n\t\t\t\torig.insertBefore( table, insertBefore );\n\t\n\t\t\t\t// Restore the width of the original table - was read from the style property,\n\t\t\t\t// so we can restore directly to that\n\t\t\t\tjqTable\n\t\t\t\t\t.css( 'width', settings.sDestroyWidth )\n\t\t\t\t\t.removeClass( classes.table );\n\t\t\t}\n\t\n\t\t\t/* Remove the settings object from the settings array */\n\t\t\tvar idx = DataTable.settings.indexOf(settings);\n\t\t\tif ( idx !== -1 ) {\n\t\t\t\tDataTable.settings.splice( idx, 1 );\n\t\t\t}\n\t\t} );\n\t} );\n\t\n\t\n\t// Add the `every()` method for rows, columns and cells in a compact form\n\t$.each( [ 'column', 'row', 'cell' ], function ( i, type ) {\n\t\t_api_register( type+'s().every()', function ( fn ) {\n\t\t\tvar opts = this.selector.opts;\n\t\t\tvar api = this;\n\t\t\tvar inst;\n\t\t\tvar counter = 0;\n\t\n\t\t\treturn this.iterator( 'every', function ( settings, selectedIdx, tableIdx ) {\n\t\t\t\tinst = api[ type ](selectedIdx, opts);\n\t\n\t\t\t\tif (type === 'cell') {\n\t\t\t\t\tfn.call(inst, inst[0][0].row, inst[0][0].column, tableIdx, counter);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tfn.call(inst, selectedIdx, tableIdx, counter);\n\t\t\t\t}\n\t\n\t\t\t\tcounter++;\n\t\t\t} );\n\t\t} );\n\t} );\n\t\n\t\n\t// i18n method for extensions to be able to use the language object from the\n\t// DataTable\n\t_api_register( 'i18n()', function ( token, def, plural ) {\n\t\tvar ctx = this.context[0];\n\t\tvar resolved = _fnGetObjectDataFn( token )( ctx.oLanguage );\n\t\n\t\tif ( resolved === undefined ) {\n\t\t\tresolved = def;\n\t\t}\n\t\n\t\tif ( $.isPlainObject( resolved ) ) {\n\t\t\tresolved = plural !== undefined && resolved[ plural ] !== undefined\n\t\t\t\t? resolved[ plural ]\n\t\t\t\t: plural === false\n\t\t\t\t\t? resolved\n\t\t\t\t\t: resolved._;\n\t\t}\n\t\n\t\treturn typeof resolved === 'string'\n\t\t\t? resolved.replace( '%d', plural ) // nb: plural might be undefined,\n\t\t\t: resolved;\n\t} );\n\t\n\t// Needed for header and footer, so pulled into its own function\n\tfunction cleanHeader(node, className) {\n\t\t$(node).find('.dt-column-order').remove();\n\t\t$(node).find('.dt-column-title').each(function () {\n\t\t\tvar title = $(this).html();\n\t\t\t$(this).parent().parent().append(title);\n\t\t\t$(this).remove();\n\t\t});\n\t\t$(node).find('div.dt-column-' + className).remove();\n\t\n\t\t$('th, td', node).removeAttr('data-dt-column');\n\t}\n\t\n\t/**\n\t * Version string for plug-ins to check compatibility. Allowed format is\n\t * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used\n\t * only for non-release builds. See https://semver.org/ for more information.\n\t *  @member\n\t *  @type string\n\t *  @default Version number\n\t */\n\tDataTable.version = \"2.3.7\";\n\t\n\t/**\n\t * Private data store, containing all of the settings objects that are\n\t * created for the tables on a given page.\n\t *\n\t * Note that the `DataTable.settings` object is aliased to\n\t * `jQuery.fn.dataTableExt` through which it may be accessed and\n\t * manipulated, or `jQuery.fn.dataTable.settings`.\n\t *  @member\n\t *  @type array\n\t *  @default []\n\t *  @private\n\t */\n\tDataTable.settings = [];\n\t\n\t/**\n\t * Object models container, for the various models that DataTables has\n\t * available to it. These models define the objects that are used to hold\n\t * the active state and configuration of the table.\n\t *  @namespace\n\t */\n\tDataTable.models = {};\n\t\n\t\n\t\n\t/**\n\t * Template object for the way in which DataTables holds information about\n\t * search information for the global filter and individual column filters.\n\t *  @namespace\n\t */\n\tDataTable.models.oSearch = {\n\t\t/**\n\t\t * Flag to whether or not the filtering should be case-insensitive\n\t\t */\n\t\t\"caseInsensitive\": true,\n\t\n\t\t/**\n\t\t * Applied search term\n\t\t */\n\t\t\"search\": \"\",\n\t\n\t\t/**\n\t\t * Flag to indicate if the search term should be interpreted as a\n\t\t * regular expression (true) or not (false) and therefore and special\n\t\t * regex characters escaped.\n\t\t */\n\t\t\"regex\": false,\n\t\n\t\t/**\n\t\t * Flag to indicate if DataTables is to use its smart filtering or not.\n\t\t */\n\t\t\"smart\": true,\n\t\n\t\t/**\n\t\t * Flag to indicate if DataTables should only trigger a search when\n\t\t * the return key is pressed.\n\t\t */\n\t\t\"return\": false\n\t};\n\t\n\t\n\t\n\t\n\t/**\n\t * Template object for the way in which DataTables holds information about\n\t * each individual row. This is the object format used for the settings\n\t * aoData array.\n\t *  @namespace\n\t */\n\tDataTable.models.oRow = {\n\t\t/**\n\t\t * TR element for the row\n\t\t */\n\t\t\"nTr\": null,\n\t\n\t\t/**\n\t\t * Array of TD elements for each row. This is null until the row has been\n\t\t * created.\n\t\t */\n\t\t\"anCells\": null,\n\t\n\t\t/**\n\t\t * Data object from the original data source for the row. This is either\n\t\t * an array if using the traditional form of DataTables, or an object if\n\t\t * using mData options. The exact type will depend on the passed in\n\t\t * data from the data source, or will be an array if using DOM a data\n\t\t * source.\n\t\t */\n\t\t\"_aData\": [],\n\t\n\t\t/**\n\t\t * Sorting data cache - this array is ostensibly the same length as the\n\t\t * number of columns (although each index is generated only as it is\n\t\t * needed), and holds the data that is used for sorting each column in the\n\t\t * row. We do this cache generation at the start of the sort in order that\n\t\t * the formatting of the sort data need be done only once for each cell\n\t\t * per sort. This array should not be read from or written to by anything\n\t\t * other than the master sorting methods.\n\t\t */\n\t\t\"_aSortData\": null,\n\t\n\t\t/**\n\t\t * Per cell filtering data cache. As per the sort data cache, used to\n\t\t * increase the performance of the filtering in DataTables\n\t\t */\n\t\t\"_aFilterData\": null,\n\t\n\t\t/**\n\t\t * Filtering data cache. This is the same as the cell filtering cache, but\n\t\t * in this case a string rather than an array. This is easily computed with\n\t\t * a join on `_aFilterData`, but is provided as a cache so the join isn't\n\t\t * needed on every search (memory traded for performance)\n\t\t */\n\t\t\"_sFilterRow\": null,\n\t\n\t\t/**\n\t\t * Denote if the original data source was from the DOM, or the data source\n\t\t * object. This is used for invalidating data, so DataTables can\n\t\t * automatically read data from the original source, unless uninstructed\n\t\t * otherwise.\n\t\t */\n\t\t\"src\": null,\n\t\n\t\t/**\n\t\t * Index in the aoData array. This saves an indexOf lookup when we have the\n\t\t * object, but want to know the index\n\t\t */\n\t\t\"idx\": -1,\n\t\n\t\t/**\n\t\t * Cached display value\n\t\t */\n\t\tdisplayData: null\n\t};\n\t\n\t\n\t/**\n\t * Template object for the column information object in DataTables. This object\n\t * is held in the settings aoColumns array and contains all the information that\n\t * DataTables needs about each individual column.\n\t *\n\t * Note that this object is related to {@link DataTable.defaults.column}\n\t * but this one is the internal data store for DataTables's cache of columns.\n\t * It should NOT be manipulated outside of DataTables. Any configuration should\n\t * be done through the initialisation options.\n\t *  @namespace\n\t */\n\tDataTable.models.oColumn = {\n\t\t/**\n\t\t * Column index.\n\t\t */\n\t\t\"idx\": null,\n\t\n\t\t/**\n\t\t * A list of the columns that sorting should occur on when this column\n\t\t * is sorted. That this property is an array allows multi-column sorting\n\t\t * to be defined for a column (for example first name / last name columns\n\t\t * would benefit from this). The values are integers pointing to the\n\t\t * columns to be sorted on (typically it will be a single integer pointing\n\t\t * at itself, but that doesn't need to be the case).\n\t\t */\n\t\t\"aDataSort\": null,\n\t\n\t\t/**\n\t\t * Define the sorting directions that are applied to the column, in sequence\n\t\t * as the column is repeatedly sorted upon - i.e. the first value is used\n\t\t * as the sorting direction when the column if first sorted (clicked on).\n\t\t * Sort it again (click again) and it will move on to the next index.\n\t\t * Repeat until loop.\n\t\t */\n\t\t\"asSorting\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is searchable, and thus should be included\n\t\t * in the filtering or not.\n\t\t */\n\t\t\"bSearchable\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is sortable or not.\n\t\t */\n\t\t\"bSortable\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if the column is currently visible in the table or not\n\t\t */\n\t\t\"bVisible\": null,\n\t\n\t\t/**\n\t\t * Store for manual type assignment using the `column.type` option. This\n\t\t * is held in store so we can manipulate the column's `sType` property.\n\t\t */\n\t\t\"_sManualType\": null,\n\t\n\t\t/**\n\t\t * Flag to indicate if HTML5 data attributes should be used as the data\n\t\t * source for filtering or sorting. True is either are.\n\t\t */\n\t\t\"_bAttrSrc\": false,\n\t\n\t\t/**\n\t\t * Developer definable function that is called whenever a cell is created (Ajax source,\n\t\t * etc) or processed for input (DOM source). This can be used as a compliment to mRender\n\t\t * allowing you to modify the DOM element (add background colour for example) when the\n\t\t * element is available.\n\t\t */\n\t\t\"fnCreatedCell\": null,\n\t\n\t\t/**\n\t\t * Function to get data from a cell in a column. You should <b>never</b>\n\t\t * access data directly through _aData internally in DataTables - always use\n\t\t * the method attached to this property. It allows mData to function as\n\t\t * required. This function is automatically assigned by the column\n\t\t * initialisation method\n\t\t */\n\t\t\"fnGetData\": null,\n\t\n\t\t/**\n\t\t * Function to set data for a cell in the column. You should <b>never</b>\n\t\t * set the data directly to _aData internally in DataTables - always use\n\t\t * this method. It allows mData to function as required. This function\n\t\t * is automatically assigned by the column initialisation method\n\t\t */\n\t\t\"fnSetData\": null,\n\t\n\t\t/**\n\t\t * Property to read the value for the cells in the column from the data\n\t\t * source array / object. If null, then the default content is used, if a\n\t\t * function is given then the return from the function is used.\n\t\t */\n\t\t\"mData\": null,\n\t\n\t\t/**\n\t\t * Partner property to mData which is used (only when defined) to get\n\t\t * the data - i.e. it is basically the same as mData, but without the\n\t\t * 'set' option, and also the data fed to it is the result from mData.\n\t\t * This is the rendering method to match the data method of mData.\n\t\t */\n\t\t\"mRender\": null,\n\t\n\t\t/**\n\t\t * The class to apply to all TD elements in the table's TBODY for the column\n\t\t */\n\t\t\"sClass\": null,\n\t\n\t\t/**\n\t\t * When DataTables calculates the column widths to assign to each column,\n\t\t * it finds the longest string in each column and then constructs a\n\t\t * temporary table and reads the widths from that. The problem with this\n\t\t * is that \"mmm\" is much wider then \"iiii\", but the latter is a longer\n\t\t * string - thus the calculation can go wrong (doing it properly and putting\n\t\t * it into an DOM object and measuring that is horribly(!) slow). Thus as\n\t\t * a \"work around\" we provide this option. It will append its value to the\n\t\t * text that is found to be the longest string for the column - i.e. padding.\n\t\t */\n\t\t\"sContentPadding\": null,\n\t\n\t\t/**\n\t\t * Allows a default value to be given for a column's data, and will be used\n\t\t * whenever a null data source is encountered (this can be because mData\n\t\t * is set to null, or because the data source itself is null).\n\t\t */\n\t\t\"sDefaultContent\": null,\n\t\n\t\t/**\n\t\t * Name for the column, allowing reference to the column by name as well as\n\t\t * by index (needs a lookup to work by name).\n\t\t */\n\t\t\"sName\": null,\n\t\n\t\t/**\n\t\t * Custom sorting data type - defines which of the available plug-ins in\n\t\t * afnSortData the custom sorting will use - if any is defined.\n\t\t */\n\t\t\"sSortDataType\": 'std',\n\t\n\t\t/**\n\t\t * Class to be applied to the header element when sorting on this column\n\t\t */\n\t\t\"sSortingClass\": null,\n\t\n\t\t/**\n\t\t * Title of the column - what is seen in the TH element (nTh).\n\t\t */\n\t\t\"sTitle\": null,\n\t\n\t\t/**\n\t\t * Column sorting and filtering type\n\t\t */\n\t\t\"sType\": null,\n\t\n\t\t/**\n\t\t * Width of the column\n\t\t */\n\t\t\"sWidth\": null,\n\t\n\t\t/**\n\t\t * Width of the column when it was first \"encountered\"\n\t\t */\n\t\t\"sWidthOrig\": null,\n\t\n\t\t/** Cached longest strings from a column */\n\t\twideStrings: null,\n\t\n\t\t/**\n\t\t * Store for named searches\n\t\t */\n\t\tsearchFixed: null\n\t};\n\t\n\t\n\t/*\n\t * Developer note: The properties of the object below are given in Hungarian\n\t * notation, that was used as the interface for DataTables prior to v1.10, however\n\t * from v1.10 onwards the primary interface is camel case. In order to avoid\n\t * breaking backwards compatibility utterly with this change, the Hungarian\n\t * version is still, internally the primary interface, but is is not documented\n\t * - hence the @name tags in each doc comment. This allows a JavaScript function\n\t * to create a map from Hungarian notation to camel case (going the other direction\n\t * would require each property to be listed, which would add around 3K to the size\n\t * of DataTables, while this method is about a 0.5K hit).\n\t *\n\t * Ultimately this does pave the way for Hungarian notation to be dropped\n\t * completely, but that is a massive amount of work and will break current\n\t * installs (therefore is on-hold until v2).\n\t */\n\t\n\t/**\n\t * Initialisation options that can be given to DataTables at initialisation\n\t * time.\n\t *  @namespace\n\t */\n\tDataTable.defaults = {\n\t\t/**\n\t\t * An array of data to use for the table, passed in at initialisation which\n\t\t * will be used in preference to any data which is already in the DOM. This is\n\t\t * particularly useful for constructing tables purely in JavaScript, for\n\t\t * example with a custom Ajax call.\n\t\t */\n\t\t\"aaData\": null,\n\t\n\t\n\t\t/**\n\t\t * If ordering is enabled, then DataTables will perform a first pass sort on\n\t\t * initialisation. You can define which column(s) the sort is performed\n\t\t * upon, and the sorting direction, with this variable. The `sorting` array\n\t\t * should contain an array for each column to be sorted initially containing\n\t\t * the column's index and a direction string ('asc' or 'desc').\n\t\t */\n\t\t\"aaSorting\": [[0,'asc']],\n\t\n\t\n\t\t/**\n\t\t * This parameter is basically identical to the `sorting` parameter, but\n\t\t * cannot be overridden by user interaction with the table. What this means\n\t\t * is that you could have a column (visible or hidden) which the sorting\n\t\t * will always be forced on first - any sorting after that (from the user)\n\t\t * will then be performed as required. This can be useful for grouping rows\n\t\t * together.\n\t\t */\n\t\t\"aaSortingFixed\": [],\n\t\n\t\n\t\t/**\n\t\t * DataTables can be instructed to load data to display in the table from a\n\t\t * Ajax source. This option defines how that Ajax call is made and where to.\n\t\t *\n\t\t * The `ajax` property has three different modes of operation, depending on\n\t\t * how it is defined. These are:\n\t\t *\n\t\t * * `string` - Set the URL from where the data should be loaded from.\n\t\t * * `object` - Define properties for `jQuery.ajax`.\n\t\t * * `function` - Custom data get function\n\t\t *\n\t\t * `string`\n\t\t * --------\n\t\t *\n\t\t * As a string, the `ajax` property simply defines the URL from which\n\t\t * DataTables will load data.\n\t\t *\n\t\t * `object`\n\t\t * --------\n\t\t *\n\t\t * As an object, the parameters in the object are passed to\n\t\t * [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) allowing fine control\n\t\t * of the Ajax request. DataTables has a number of default parameters which\n\t\t * you can override using this option. Please refer to the jQuery\n\t\t * documentation for a full description of the options available, although\n\t\t * the following parameters provide additional options in DataTables or\n\t\t * require special consideration:\n\t\t *\n\t\t * * `data` - As with jQuery, `data` can be provided as an object, but it\n\t\t *   can also be used as a function to manipulate the data DataTables sends\n\t\t *   to the server. The function takes a single parameter, an object of\n\t\t *   parameters with the values that DataTables has readied for sending. An\n\t\t *   object may be returned which will be merged into the DataTables\n\t\t *   defaults, or you can add the items to the object that was passed in and\n\t\t *   not return anything from the function. This supersedes `fnServerParams`\n\t\t *   from DataTables 1.9-.\n\t\t *\n\t\t * * `dataSrc` - By default DataTables will look for the property `data` (or\n\t\t *   `aaData` for compatibility with DataTables 1.9-) when obtaining data\n\t\t *   from an Ajax source or for server-side processing - this parameter\n\t\t *   allows that property to be changed. You can use JavaScript dotted\n\t\t *   object notation to get a data source for multiple levels of nesting, or\n\t\t *   it my be used as a function. As a function it takes a single parameter,\n\t\t *   the JSON returned from the server, which can be manipulated as\n\t\t *   required, with the returned value being that used by DataTables as the\n\t\t *   data source for the table.\n\t\t *\n\t\t * * `success` - Should not be overridden it is used internally in\n\t\t *   DataTables. To manipulate / transform the data returned by the server\n\t\t *   use `ajax.dataSrc`, or use `ajax` as a function (see below).\n\t\t *\n\t\t * `function`\n\t\t * ----------\n\t\t *\n\t\t * As a function, making the Ajax call is left up to yourself allowing\n\t\t * complete control of the Ajax request. Indeed, if desired, a method other\n\t\t * than Ajax could be used to obtain the required data, such as Web storage\n\t\t * or an AIR database.\n\t\t *\n\t\t * The function is given four parameters and no return is required. The\n\t\t * parameters are:\n\t\t *\n\t\t * 1. _object_ - Data to send to the server\n\t\t * 2. _function_ - Callback function that must be executed when the required\n\t\t *    data has been obtained. That data should be passed into the callback\n\t\t *    as the only parameter\n\t\t * 3. _object_ - DataTables settings object for the table\n\t\t */\n\t\t\"ajax\": null,\n\t\n\t\n\t\t/**\n\t\t * This parameter allows you to readily specify the entries in the length drop\n\t\t * down menu that DataTables shows when pagination is enabled. It can be\n\t\t * either a 1D array of options which will be used for both the displayed\n\t\t * option and the value, or a 2D array which will use the array in the first\n\t\t * position as the value, and the array in the second position as the\n\t\t * displayed options (useful for language strings such as 'All').\n\t\t *\n\t\t * Note that the `pageLength` property will be automatically set to the\n\t\t * first value given in this array, unless `pageLength` is also provided.\n\t\t */\n\t\t\"aLengthMenu\": [ 10, 25, 50, 100 ],\n\t\n\t\n\t\t/**\n\t\t * The `columns` option in the initialisation parameter allows you to define\n\t\t * details about the way individual columns behave. For a full list of\n\t\t * column options that can be set, please see\n\t\t * {@link DataTable.defaults.column}. Note that if you use `columns` to\n\t\t * define your columns, you must have an entry in the array for every single\n\t\t * column that you have in your table (these can be null if you don't which\n\t\t * to specify any options).\n\t\t */\n\t\t\"aoColumns\": null,\n\t\n\t\t/**\n\t\t * Very similar to `columns`, `columnDefs` allows you to target a specific\n\t\t * column, multiple columns, or all columns, using the `targets` property of\n\t\t * each object in the array. This allows great flexibility when creating\n\t\t * tables, as the `columnDefs` arrays can be of any length, targeting the\n\t\t * columns you specifically want. `columnDefs` may use any of the column\n\t\t * options available: {@link DataTable.defaults.column}, but it _must_\n\t\t * have `targets` defined in each object in the array. Values in the `targets`\n\t\t * array may be:\n\t\t *   <ul>\n\t\t *     <li>a string - class name will be matched on the TH for the column</li>\n\t\t *     <li>0 or a positive integer - column index counting from the left</li>\n\t\t *     <li>a negative integer - column index counting from the right</li>\n\t\t *     <li>the string \"_all\" - all columns (i.e. assign a default)</li>\n\t\t *   </ul>\n\t\t */\n\t\t\"aoColumnDefs\": null,\n\t\n\t\n\t\t/**\n\t\t * Basically the same as `search`, this parameter defines the individual column\n\t\t * filtering state at initialisation time. The array must be of the same size\n\t\t * as the number of columns, and each element be an object with the parameters\n\t\t * `search` and `escapeRegex` (the latter is optional). 'null' is also\n\t\t * accepted and the default will be used.\n\t\t */\n\t\t\"aoSearchCols\": [],\n\t\n\t\n\t\t/**\n\t\t * Enable or disable automatic column width calculation. This can be disabled\n\t\t * as an optimisation (it takes some time to calculate the widths) if the\n\t\t * tables widths are passed in using `columns`.\n\t\t */\n\t\t\"bAutoWidth\": true,\n\t\n\t\n\t\t/**\n\t\t * Deferred rendering can provide DataTables with a huge speed boost when you\n\t\t * are using an Ajax or JS data source for the table. This option, when set to\n\t\t * true, will cause DataTables to defer the creation of the table elements for\n\t\t * each row until they are needed for a draw - saving a significant amount of\n\t\t * time.\n\t\t */\n\t\t\"bDeferRender\": true,\n\t\n\t\n\t\t/**\n\t\t * Replace a DataTable which matches the given selector and replace it with\n\t\t * one which has the properties of the new initialisation object passed. If no\n\t\t * table matches the selector, then the new DataTable will be constructed as\n\t\t * per normal.\n\t\t */\n\t\t\"bDestroy\": false,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable filtering of data. Filtering in DataTables is \"smart\" in\n\t\t * that it allows the end user to input multiple words (space separated) and\n\t\t * will match a row containing those words, even if not in the order that was\n\t\t * specified (this allow matching across multiple columns). Note that if you\n\t\t * wish to use filtering in DataTables this must remain 'true' - to remove the\n\t\t * default filtering input box and retain filtering abilities, please use\n\t\t * {@link DataTable.defaults.dom}.\n\t\t */\n\t\t\"bFilter\": true,\n\t\n\t\t/**\n\t\t * Used only for compatibility with DT1\n\t\t * @deprecated\n\t\t */\n\t\t\"bInfo\": true,\n\t\n\t\t/**\n\t\t * Used only for compatibility with DT1\n\t\t * @deprecated\n\t\t */\n\t\t\"bLengthChange\": true,\n\t\n\t\t/**\n\t\t * Enable or disable pagination.\n\t\t */\n\t\t\"bPaginate\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the display of a 'processing' indicator when the table is\n\t\t * being processed (e.g. a sort). This is particularly useful for tables with\n\t\t * large amounts of data where it can take a noticeable amount of time to sort\n\t\t * the entries.\n\t\t */\n\t\t\"bProcessing\": false,\n\t\n\t\n\t\t/**\n\t\t * Retrieve the DataTables object for the given selector. Note that if the\n\t\t * table has already been initialised, this parameter will cause DataTables\n\t\t * to simply return the object that has already been set up - it will not take\n\t\t * account of any changes you might have made to the initialisation object\n\t\t * passed to DataTables (setting this parameter to true is an acknowledgement\n\t\t * that you understand this). `destroy` can be used to reinitialise a table if\n\t\t * you need.\n\t\t */\n\t\t\"bRetrieve\": false,\n\t\n\t\n\t\t/**\n\t\t * When vertical (y) scrolling is enabled, DataTables will force the height of\n\t\t * the table's viewport to the given height at all times (useful for layout).\n\t\t * However, this can look odd when filtering data down to a small data set,\n\t\t * and the footer is left \"floating\" further down. This parameter (when\n\t\t * enabled) will cause DataTables to collapse the table's viewport down when\n\t\t * the result set will fit within the given Y height.\n\t\t */\n\t\t\"bScrollCollapse\": false,\n\t\n\t\n\t\t/**\n\t\t * Configure DataTables to use server-side processing. Note that the\n\t\t * `ajax` parameter must also be given in order to give DataTables a\n\t\t * source to obtain the required data for each draw.\n\t\t */\n\t\t\"bServerSide\": false,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable sorting of columns. Sorting of individual columns can be\n\t\t * disabled by the `sortable` option for each column.\n\t\t */\n\t\t\"bSort\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or display DataTables' ability to sort multiple columns at the\n\t\t * same time (activated by shift-click by the user).\n\t\t */\n\t\t\"bSortMulti\": true,\n\t\n\t\n\t\t/**\n\t\t * Allows control over whether DataTables should use the top (true) unique\n\t\t * cell that is found for a single column, or the bottom (false - default).\n\t\t * This is useful when using complex headers.\n\t\t */\n\t\t\"bSortCellsTop\": null,\n\t\n\t\n\t\t/** Specify which row is the title row in the header. Replacement for bSortCellsTop */\n\t\ttitleRow: null,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the addition of the classes `sorting\\_1`, `sorting\\_2` and\n\t\t * `sorting\\_3` to the columns which are currently being sorted on. This is\n\t\t * presented as a feature switch as it can increase processing time (while\n\t\t * classes are removed and added) so for large data sets you might want to\n\t\t * turn this off.\n\t\t */\n\t\t\"bSortClasses\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable state saving. When enabled HTML5 `localStorage` will be\n\t\t * used to save table display information such as pagination information,\n\t\t * display length, filtering and sorting. As such when the end user reloads\n\t\t * the page the display will match what thy had previously set up.\n\t\t */\n\t\t\"bStateSave\": false,\n\t\n\t\n\t\t/**\n\t\t * This function is called when a TR element is created (and all TD child\n\t\t * elements have been inserted), or registered if using a DOM source, allowing\n\t\t * manipulation of the TR element (adding classes etc).\n\t\t */\n\t\t\"fnCreatedRow\": null,\n\t\n\t\n\t\t/**\n\t\t * This function is called on every 'draw' event, and allows you to\n\t\t * dynamically modify any aspect you want about the created DOM.\n\t\t */\n\t\t\"fnDrawCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * Identical to fnHeaderCallback() but for the table footer this function\n\t\t * allows you to modify the table footer on every 'draw' event.\n\t\t */\n\t\t\"fnFooterCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * When rendering large numbers in the information element for the table\n\t\t * (i.e. \"Showing 1 to 10 of 57 entries\") DataTables will render large numbers\n\t\t * to have a comma separator for the 'thousands' units (e.g. 1 million is\n\t\t * rendered as \"1,000,000\") to help readability for the end user. This\n\t\t * function will override the default method DataTables uses.\n\t\t */\n\t\t\"fnFormatNumber\": function ( toFormat ) {\n\t\t\treturn toFormat.toString().replace(\n\t\t\t\t/\\B(?=(\\d{3})+(?!\\d))/g,\n\t\t\t\tthis.oLanguage.sThousands\n\t\t\t);\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * This function is called on every 'draw' event, and allows you to\n\t\t * dynamically modify the header row. This can be used to calculate and\n\t\t * display useful information about the table.\n\t\t */\n\t\t\"fnHeaderCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * The information element can be used to convey information about the current\n\t\t * state of the table. Although the internationalisation options presented by\n\t\t * DataTables are quite capable of dealing with most customisations, there may\n\t\t * be times where you wish to customise the string further. This callback\n\t\t * allows you to do exactly that.\n\t\t */\n\t\t\"fnInfoCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * Called when the table has been initialised. Normally DataTables will\n\t\t * initialise sequentially and there will be no need for this function,\n\t\t * however, this does not hold true when using external language information\n\t\t * since that is obtained using an async XHR call.\n\t\t */\n\t\t\"fnInitComplete\": null,\n\t\n\t\n\t\t/**\n\t\t * Called at the very start of each table draw and can be used to cancel the\n\t\t * draw by returning false, any other return (including undefined) results in\n\t\t * the full draw occurring).\n\t\t */\n\t\t\"fnPreDrawCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * This function allows you to 'post process' each row after it have been\n\t\t * generated for each table draw, but before it is rendered on screen. This\n\t\t * function might be used for setting the row class name etc.\n\t\t */\n\t\t\"fnRowCallback\": null,\n\t\n\t\n\t\t/**\n\t\t * Load the table state. With this function you can define from where, and how, the\n\t\t * state of a table is loaded. By default DataTables will load from `localStorage`\n\t\t * but you might wish to use a server-side database or cookies.\n\t\t */\n\t\t\"fnStateLoadCallback\": function ( settings ) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(\n\t\t\t\t\t(settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem(\n\t\t\t\t\t\t'DataTables_'+settings.sInstance+'_'+location.pathname\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t} catch (e) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Callback which allows modification of the saved state prior to loading that state.\n\t\t * This callback is called when the table is loading state from the stored data, but\n\t\t * prior to the settings object being modified by the saved state. Note that for\n\t\t * plug-in authors, you should use the `stateLoadParams` event to load parameters for\n\t\t * a plug-in.\n\t\t */\n\t\t\"fnStateLoadParams\": null,\n\t\n\t\n\t\t/**\n\t\t * Callback that is called when the state has been loaded from the state saving method\n\t\t * and the DataTables settings object has been modified as a result of the loaded state.\n\t\t */\n\t\t\"fnStateLoaded\": null,\n\t\n\t\n\t\t/**\n\t\t * Save the table state. This function allows you to define where and how the state\n\t\t * information for the table is stored By default DataTables will use `localStorage`\n\t\t * but you might wish to use a server-side database or cookies.\n\t\t */\n\t\t\"fnStateSaveCallback\": function ( settings, data ) {\n\t\t\ttry {\n\t\t\t\t(settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem(\n\t\t\t\t\t'DataTables_'+settings.sInstance+'_'+location.pathname,\n\t\t\t\t\tJSON.stringify( data )\n\t\t\t\t);\n\t\t\t} catch (e) {\n\t\t\t\t// noop\n\t\t\t}\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Callback which allows modification of the state to be saved. Called when the table\n\t\t * has changed state a new state save is required. This method allows modification of\n\t\t * the state saving object prior to actually doing the save, including addition or\n\t\t * other state properties or modification. Note that for plug-in authors, you should\n\t\t * use the `stateSaveParams` event to save parameters for a plug-in.\n\t\t */\n\t\t\"fnStateSaveParams\": null,\n\t\n\t\n\t\t/**\n\t\t * Duration for which the saved state information is considered valid. After this period\n\t\t * has elapsed the state will be returned to the default.\n\t\t * Value is given in seconds.\n\t\t */\n\t\t\"iStateDuration\": 7200,\n\t\n\t\n\t\t/**\n\t\t * Number of rows to display on a single page when using pagination. If\n\t\t * feature enabled (`lengthChange`) then the end user will be able to override\n\t\t * this to a custom setting using a pop-up menu.\n\t\t */\n\t\t\"iDisplayLength\": 10,\n\t\n\t\n\t\t/**\n\t\t * Define the starting point for data display when using DataTables with\n\t\t * pagination. Note that this parameter is the number of records, rather than\n\t\t * the page number, so if you have 10 records per page and want to start on\n\t\t * the third page, it should be \"20\".\n\t\t */\n\t\t\"iDisplayStart\": 0,\n\t\n\t\n\t\t/**\n\t\t * By default DataTables allows keyboard navigation of the table (sorting, paging,\n\t\t * and filtering) by adding a `tabindex` attribute to the required elements. This\n\t\t * allows you to tab through the controls and press the enter key to activate them.\n\t\t * The tabindex is default 0, meaning that the tab follows the flow of the document.\n\t\t * You can overrule this using this parameter if you wish. Use a value of -1 to\n\t\t * disable built-in keyboard navigation.\n\t\t */\n\t\t\"iTabIndex\": 0,\n\t\n\t\n\t\t/**\n\t\t * Classes that DataTables assigns to the various components and features\n\t\t * that it adds to the HTML table. This allows classes to be configured\n\t\t * during initialisation in addition to through the static\n\t\t * {@link DataTable.ext.oStdClasses} object).\n\t\t */\n\t\t\"oClasses\": {},\n\t\n\t\n\t\t/**\n\t\t * All strings that DataTables uses in the user interface that it creates\n\t\t * are defined in this object, allowing you to modified them individually or\n\t\t * completely replace them all as required.\n\t\t */\n\t\t\"oLanguage\": {\n\t\t\t/**\n\t\t\t * Strings that are used for WAI-ARIA labels and controls only (these are not\n\t\t\t * actually visible on the page, but will be read by screenreaders, and thus\n\t\t\t * must be internationalised as well).\n\t\t\t */\n\t\t\t\"oAria\": {\n\t\t\t\t/**\n\t\t\t\t * ARIA label that is added to the table headers when the column may be sorted\n\t\t\t\t */\n\t\t\t\t\"orderable\": \": Activate to sort\",\n\t\n\t\t\t\t/**\n\t\t\t\t * ARIA label that is added to the table headers when the column is currently being sorted\n\t\t\t\t */\n\t\t\t\t\"orderableReverse\": \": Activate to invert sorting\",\n\t\n\t\t\t\t/**\n\t\t\t\t * ARIA label that is added to the table headers when the column is currently being \n\t\t\t\t * sorted and next step is to remove sorting\n\t\t\t\t */\n\t\t\t\t\"orderableRemove\": \": Activate to remove sorting\",\n\t\n\t\t\t\tpaginate: {\n\t\t\t\t\tfirst: 'First',\n\t\t\t\t\tlast: 'Last',\n\t\t\t\t\tnext: 'Next',\n\t\t\t\t\tprevious: 'Previous',\n\t\t\t\t\tnumber: ''\n\t\t\t\t}\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * Pagination string used by DataTables for the built-in pagination\n\t\t\t * control types.\n\t\t\t */\n\t\t\t\"oPaginate\": {\n\t\t\t\t/**\n\t\t\t\t * Label and character for first page button («)\n\t\t\t\t */\n\t\t\t\t\"sFirst\": \"\\u00AB\",\n\t\n\t\t\t\t/**\n\t\t\t\t * Last page button (»)\n\t\t\t\t */\n\t\t\t\t\"sLast\": \"\\u00BB\",\n\t\n\t\t\t\t/**\n\t\t\t\t * Next page button (›)\n\t\t\t\t */\n\t\t\t\t\"sNext\": \"\\u203A\",\n\t\n\t\t\t\t/**\n\t\t\t\t * Previous page button (‹)\n\t\t\t\t */\n\t\t\t\t\"sPrevious\": \"\\u2039\",\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * Plural object for the data type the table is showing\n\t\t\t */\n\t\t\tentries: {\n\t\t\t\t_: \"entries\",\n\t\t\t\t1: \"entry\"\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * Page length options\n\t\t\t */\n\t\t\tlengthLabels: {\n\t\t\t\t'-1': 'All'\n\t\t\t},\n\t\n\t\t\t/**\n\t\t\t * This string is shown in preference to `zeroRecords` when the table is\n\t\t\t * empty of data (regardless of filtering). Note that this is an optional\n\t\t\t * parameter - if it is not given, the value of `zeroRecords` will be used\n\t\t\t * instead (either the default or given value).\n\t\t\t */\n\t\t\t\"sEmptyTable\": \"No data available in table\",\n\t\n\t\n\t\t\t/**\n\t\t\t * This string gives information to the end user about the information\n\t\t\t * that is current on display on the page. The following tokens can be\n\t\t\t * used in the string and will be dynamically replaced as the table\n\t\t\t * display updates. This tokens can be placed anywhere in the string, or\n\t\t\t * removed as needed by the language requires:\n\t\t\t *\n\t\t\t * * `\\_START\\_` - Display index of the first record on the current page\n\t\t\t * * `\\_END\\_` - Display index of the last record on the current page\n\t\t\t * * `\\_TOTAL\\_` - Number of records in the table after filtering\n\t\t\t * * `\\_MAX\\_` - Number of records in the table without filtering\n\t\t\t * * `\\_PAGE\\_` - Current page number\n\t\t\t * * `\\_PAGES\\_` - Total number of pages of data in the table\n\t\t\t */\n\t\t\t\"sInfo\": \"Showing _START_ to _END_ of _TOTAL_ _ENTRIES-TOTAL_\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Display information string for when the table is empty. Typically the\n\t\t\t * format of this string should match `info`.\n\t\t\t */\n\t\t\t\"sInfoEmpty\": \"Showing 0 to 0 of 0 _ENTRIES-TOTAL_\",\n\t\n\t\n\t\t\t/**\n\t\t\t * When a user filters the information in a table, this string is appended\n\t\t\t * to the information (`info`) to give an idea of how strong the filtering\n\t\t\t * is. The variable _MAX_ is dynamically updated.\n\t\t\t */\n\t\t\t\"sInfoFiltered\": \"(filtered from _MAX_ total _ENTRIES-MAX_)\",\n\t\n\t\n\t\t\t/**\n\t\t\t * If can be useful to append extra information to the info string at times,\n\t\t\t * and this variable does exactly that. This information will be appended to\n\t\t\t * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are\n\t\t\t * being used) at all times.\n\t\t\t */\n\t\t\t\"sInfoPostFix\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * This decimal place operator is a little different from the other\n\t\t\t * language options since DataTables doesn't output floating point\n\t\t\t * numbers, so it won't ever use this for display of a number. Rather,\n\t\t\t * what this parameter does is modify the sort methods of the table so\n\t\t\t * that numbers which are in a format which has a character other than\n\t\t\t * a period (`.`) as a decimal place will be sorted numerically.\n\t\t\t *\n\t\t\t * Note that numbers with different decimal places cannot be shown in\n\t\t\t * the same table and still be sortable, the table must be consistent.\n\t\t\t * However, multiple different tables on the page can use different\n\t\t\t * decimal place characters.\n\t\t\t */\n\t\t\t\"sDecimal\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * DataTables has a build in number formatter (`formatNumber`) which is\n\t\t\t * used to format large numbers that are used in the table information.\n\t\t\t * By default a comma is used, but this can be trivially changed to any\n\t\t\t * character you wish with this parameter.\n\t\t\t */\n\t\t\t\"sThousands\": \",\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Detail the action that will be taken when the drop down menu for the\n\t\t\t * pagination length option is changed. The '_MENU_' variable is replaced\n\t\t\t * with a default select list of 10, 25, 50 and 100, and can be replaced\n\t\t\t * with a custom select box if required.\n\t\t\t */\n\t\t\t\"sLengthMenu\": \"_MENU_ _ENTRIES_ per page\",\n\t\n\t\n\t\t\t/**\n\t\t\t * When using Ajax sourced data and during the first draw when DataTables is\n\t\t\t * gathering the data, this message is shown in an empty row in the table to\n\t\t\t * indicate to the end user the data is being loaded. Note that this\n\t\t\t * parameter is not used when loading data by server-side processing, just\n\t\t\t * Ajax sourced data with client-side processing.\n\t\t\t */\n\t\t\t\"sLoadingRecords\": \"Loading...\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Text which is displayed when the table is processing a user action\n\t\t\t * (usually a sort command or similar).\n\t\t\t */\n\t\t\t\"sProcessing\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Details the actions that will be taken when the user types into the\n\t\t\t * filtering input text box. The variable \"_INPUT_\", if used in the string,\n\t\t\t * is replaced with the HTML text box for the filtering input allowing\n\t\t\t * control over where it appears in the string. If \"_INPUT_\" is not given\n\t\t\t * then the input box is appended to the string automatically.\n\t\t\t */\n\t\t\t\"sSearch\": \"Search:\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Assign a `placeholder` attribute to the search `input` element\n\t\t\t *  @type string\n\t\t\t *  @default \n\t\t\t *\n\t\t\t *  @dtopt Language\n\t\t\t *  @name DataTable.defaults.language.searchPlaceholder\n\t\t\t */\n\t\t\t\"sSearchPlaceholder\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * All of the language information can be stored in a file on the\n\t\t\t * server-side, which DataTables will look up if this parameter is passed.\n\t\t\t * It must store the URL of the language file, which is in a JSON format,\n\t\t\t * and the object has the same properties as the oLanguage object in the\n\t\t\t * initialiser object (i.e. the above parameters). Please refer to one of\n\t\t\t * the example language files to see how this works in action.\n\t\t\t */\n\t\t\t\"sUrl\": \"\",\n\t\n\t\n\t\t\t/**\n\t\t\t * Text shown inside the table records when the is no information to be\n\t\t\t * displayed after filtering. `emptyTable` is shown when there is simply no\n\t\t\t * information in the table at all (regardless of filtering).\n\t\t\t */\n\t\t\t\"sZeroRecords\": \"No matching records found\"\n\t\t},\n\t\n\t\n\t\t/** The initial data order is reversed when `desc` ordering */\n\t\torderDescReverse: true,\n\t\n\t\n\t\t/**\n\t\t * This parameter allows you to have define the global filtering state at\n\t\t * initialisation time. As an object the `search` parameter must be\n\t\t * defined, but all other parameters are optional. When `regex` is true,\n\t\t * the search string will be treated as a regular expression, when false\n\t\t * (default) it will be treated as a straight string. When `smart`\n\t\t * DataTables will use it's smart filtering methods (to word match at\n\t\t * any point in the data), when false this will not be done.\n\t\t */\n\t\t\"oSearch\": $.extend( {}, DataTable.models.oSearch ),\n\t\n\t\n\t\t/**\n\t\t * Table and control layout. This replaces the legacy `dom` option.\n\t\t */\n\t\tlayout: {\n\t\t\ttopStart: 'pageLength',\n\t\t\ttopEnd: 'search',\n\t\t\tbottomStart: 'info',\n\t\t\tbottomEnd: 'paging'\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Legacy DOM layout option\n\t\t */\n\t\t\"sDom\": null,\n\t\n\t\n\t\t/**\n\t\t * Search delay option. This will throttle full table searches that use the\n\t\t * DataTables provided search input element (it does not effect calls to\n\t\t * `dt-api search()`, providing a delay before the search is made.\n\t\t */\n\t\t\"searchDelay\": null,\n\t\n\t\n\t\t/**\n\t\t * DataTables features six different built-in options for the buttons to\n\t\t * display for pagination control:\n\t\t *\n\t\t * * `numbers` - Page number buttons only\n\t\t * * `simple` - 'Previous' and 'Next' buttons only\n\t\t * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers\n\t\t * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons\n\t\t * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers\n\t\t * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers\n\t\t */\n\t\t\"sPaginationType\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Enable horizontal scrolling. When a table is too wide to fit into a\n\t\t * certain layout, or you have a large number of columns in the table, you\n\t\t * can enable x-scrolling to show the table in a viewport, which can be\n\t\t * scrolled. This property can be `true` which will allow the table to\n\t\t * scroll horizontally when needed, or any CSS unit, or a number (in which\n\t\t * case it will be treated as a pixel measurement). Setting as simply `true`\n\t\t * is recommended.\n\t\t */\n\t\t\"sScrollX\": \"\",\n\t\n\t\n\t\t/**\n\t\t * This property can be used to force a DataTable to use more width than it\n\t\t * might otherwise do when x-scrolling is enabled. For example if you have a\n\t\t * table which requires to be well spaced, this parameter is useful for\n\t\t * \"over-sizing\" the table, and thus forcing scrolling. This property can by\n\t\t * any CSS unit, or a number (in which case it will be treated as a pixel\n\t\t * measurement).\n\t\t */\n\t\t\"sScrollXInner\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Enable vertical scrolling. Vertical scrolling will constrain the DataTable\n\t\t * to the given height, and enable scrolling for any data which overflows the\n\t\t * current viewport. This can be used as an alternative to paging to display\n\t\t * a lot of data in a small area (although paging and scrolling can both be\n\t\t * enabled at the same time). This property can be any CSS unit, or a number\n\t\t * (in which case it will be treated as a pixel measurement).\n\t\t */\n\t\t\"sScrollY\": \"\",\n\t\n\t\n\t\t/**\n\t\t * __Deprecated__ The functionality provided by this parameter has now been\n\t\t * superseded by that provided through `ajax`, which should be used instead.\n\t\t *\n\t\t * Set the HTTP method that is used to make the Ajax call for server-side\n\t\t * processing or Ajax sourced data.\n\t\t */\n\t\t\"sServerMethod\": \"GET\",\n\t\n\t\n\t\t/**\n\t\t * DataTables makes use of renderers when displaying HTML elements for\n\t\t * a table. These renderers can be added or modified by plug-ins to\n\t\t * generate suitable mark-up for a site. For example the Bootstrap\n\t\t * integration plug-in for DataTables uses a paging button renderer to\n\t\t * display pagination buttons in the mark-up required by Bootstrap.\n\t\t *\n\t\t * For further information about the renderers available see\n\t\t * DataTable.ext.renderer\n\t\t */\n\t\t\"renderer\": null,\n\t\n\t\n\t\t/**\n\t\t * Set the data property name that DataTables should use to get a row's id\n\t\t * to set as the `id` property in the node.\n\t\t */\n\t\t\"rowId\": \"DT_RowId\",\n\t\n\t\n\t\t/**\n\t\t * Caption value\n\t\t */\n\t\t\"caption\": null,\n\t\n\t\n\t\t/**\n\t\t * For server-side processing - use the data from the DOM for the first draw\n\t\t */\n\t\tiDeferLoading: null,\n\t\n\t\t/** Event listeners */\n\t\ton: null,\n\t\n\t\t/** Title wrapper element type */\n\t\tcolumnTitleTag: 'span'\n\t};\n\t\n\t_fnHungarianMap( DataTable.defaults );\n\t\n\t\n\t\n\t/*\n\t * Developer note - See note in model.defaults.js about the use of Hungarian\n\t * notation and camel case.\n\t */\n\t\n\t/**\n\t * Column options that can be given to DataTables at initialisation time.\n\t *  @namespace\n\t */\n\tDataTable.defaults.column = {\n\t\t/**\n\t\t * Define which column(s) an order will occur on for this column. This\n\t\t * allows a column's ordering to take multiple columns into account when\n\t\t * doing a sort or use the data from a different column. For example first\n\t\t * name / last name columns make sense to do a multi-column sort over the\n\t\t * two columns.\n\t\t */\n\t\t\"aDataSort\": null,\n\t\t\"iDataSort\": -1,\n\t\n\t\tariaTitle: '',\n\t\n\t\n\t\t/**\n\t\t * You can control the default ordering direction, and even alter the\n\t\t * behaviour of the sort handler (i.e. only allow ascending ordering etc)\n\t\t * using this parameter.\n\t\t */\n\t\t\"asSorting\": [ 'asc', 'desc', '' ],\n\t\n\t\n\t\t/**\n\t\t * Enable or disable filtering on the data in this column.\n\t\t */\n\t\t\"bSearchable\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable ordering on this column.\n\t\t */\n\t\t\"bSortable\": true,\n\t\n\t\n\t\t/**\n\t\t * Enable or disable the display of this column.\n\t\t */\n\t\t\"bVisible\": true,\n\t\n\t\n\t\t/**\n\t\t * Developer definable function that is called whenever a cell is created (Ajax source,\n\t\t * etc) or processed for input (DOM source). This can be used as a compliment to mRender\n\t\t * allowing you to modify the DOM element (add background colour for example) when the\n\t\t * element is available.\n\t\t */\n\t\t\"fnCreatedCell\": null,\n\t\n\t\n\t\t/**\n\t\t * This property can be used to read data from any data source property,\n\t\t * including deeply nested objects / properties. `data` can be given in a\n\t\t * number of different ways which effect its behaviour:\n\t\t *\n\t\t * * `integer` - treated as an array index for the data source. This is the\n\t\t *   default that DataTables uses (incrementally increased for each column).\n\t\t * * `string` - read an object property from the data source. There are\n\t\t *   three 'special' options that can be used in the string to alter how\n\t\t *   DataTables reads the data from the source object:\n\t\t *    * `.` - Dotted JavaScript notation. Just as you use a `.` in\n\t\t *      JavaScript to read from nested objects, so to can the options\n\t\t *      specified in `data`. For example: `browser.version` or\n\t\t *      `browser.name`. If your object parameter name contains a period, use\n\t\t *      `\\\\` to escape it - i.e. `first\\\\.name`.\n\t\t *    * `[]` - Array notation. DataTables can automatically combine data\n\t\t *      from and array source, joining the data with the characters provided\n\t\t *      between the two brackets. For example: `name[, ]` would provide a\n\t\t *      comma-space separated list from the source array. If no characters\n\t\t *      are provided between the brackets, the original array source is\n\t\t *      returned.\n\t\t *    * `()` - Function notation. Adding `()` to the end of a parameter will\n\t\t *      execute a function of the name given. For example: `browser()` for a\n\t\t *      simple function on the data source, `browser.version()` for a\n\t\t *      function in a nested property or even `browser().version` to get an\n\t\t *      object property if the function called returns an object. Note that\n\t\t *      function notation is recommended for use in `render` rather than\n\t\t *      `data` as it is much simpler to use as a renderer.\n\t\t * * `null` - use the original data source for the row rather than plucking\n\t\t *   data directly from it. This action has effects on two other\n\t\t *   initialisation options:\n\t\t *    * `defaultContent` - When null is given as the `data` option and\n\t\t *      `defaultContent` is specified for the column, the value defined by\n\t\t *      `defaultContent` will be used for the cell.\n\t\t *    * `render` - When null is used for the `data` option and the `render`\n\t\t *      option is specified for the column, the whole data source for the\n\t\t *      row is used for the renderer.\n\t\t * * `function` - the function given will be executed whenever DataTables\n\t\t *   needs to set or get the data for a cell in the column. The function\n\t\t *   takes three parameters:\n\t\t *    * Parameters:\n\t\t *      * `{array|object}` The data source for the row\n\t\t *      * `{string}` The type call data requested - this will be 'set' when\n\t\t *        setting data or 'filter', 'display', 'type', 'sort' or undefined\n\t\t *        when gathering data. Note that when `undefined` is given for the\n\t\t *        type DataTables expects to get the raw data for the object back<\n\t\t *      * `{*}` Data to set when the second parameter is 'set'.\n\t\t *    * Return:\n\t\t *      * The return value from the function is not required when 'set' is\n\t\t *        the type of call, but otherwise the return is what will be used\n\t\t *        for the data requested.\n\t\t *\n\t\t * Note that `data` is a getter and setter option. If you just require\n\t\t * formatting of data for output, you will likely want to use `render` which\n\t\t * is simply a getter and thus simpler to use.\n\t\t *\n\t\t * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The\n\t\t * name change reflects the flexibility of this property and is consistent\n\t\t * with the naming of mRender. If 'mDataProp' is given, then it will still\n\t\t * be used by DataTables, as it automatically maps the old name to the new\n\t\t * if required.\n\t\t */\n\t\t\"mData\": null,\n\t\n\t\n\t\t/**\n\t\t * This property is the rendering partner to `data` and it is suggested that\n\t\t * when you want to manipulate data for display (including filtering,\n\t\t * sorting etc) without altering the underlying data for the table, use this\n\t\t * property. `render` can be considered to be the read only companion to\n\t\t * `data` which is read / write (then as such more complex). Like `data`\n\t\t * this option can be given in a number of different ways to effect its\n\t\t * behaviour:\n\t\t *\n\t\t * * `integer` - treated as an array index for the data source. This is the\n\t\t *   default that DataTables uses (incrementally increased for each column).\n\t\t * * `string` - read an object property from the data source. There are\n\t\t *   three 'special' options that can be used in the string to alter how\n\t\t *   DataTables reads the data from the source object:\n\t\t *    * `.` - Dotted JavaScript notation. Just as you use a `.` in\n\t\t *      JavaScript to read from nested objects, so to can the options\n\t\t *      specified in `data`. For example: `browser.version` or\n\t\t *      `browser.name`. If your object parameter name contains a period, use\n\t\t *      `\\\\` to escape it - i.e. `first\\\\.name`.\n\t\t *    * `[]` - Array notation. DataTables can automatically combine data\n\t\t *      from and array source, joining the data with the characters provided\n\t\t *      between the two brackets. For example: `name[, ]` would provide a\n\t\t *      comma-space separated list from the source array. If no characters\n\t\t *      are provided between the brackets, the original array source is\n\t\t *      returned.\n\t\t *    * `()` - Function notation. Adding `()` to the end of a parameter will\n\t\t *      execute a function of the name given. For example: `browser()` for a\n\t\t *      simple function on the data source, `browser.version()` for a\n\t\t *      function in a nested property or even `browser().version` to get an\n\t\t *      object property if the function called returns an object.\n\t\t * * `object` - use different data for the different data types requested by\n\t\t *   DataTables ('filter', 'display', 'type' or 'sort'). The property names\n\t\t *   of the object is the data type the property refers to and the value can\n\t\t *   defined using an integer, string or function using the same rules as\n\t\t *   `render` normally does. Note that an `_` option _must_ be specified.\n\t\t *   This is the default value to use if you haven't specified a value for\n\t\t *   the data type requested by DataTables.\n\t\t * * `function` - the function given will be executed whenever DataTables\n\t\t *   needs to set or get the data for a cell in the column. The function\n\t\t *   takes three parameters:\n\t\t *    * Parameters:\n\t\t *      * {array|object} The data source for the row (based on `data`)\n\t\t *      * {string} The type call data requested - this will be 'filter',\n\t\t *        'display', 'type' or 'sort'.\n\t\t *      * {array|object} The full data source for the row (not based on\n\t\t *        `data`)\n\t\t *    * Return:\n\t\t *      * The return value from the function is what will be used for the\n\t\t *        data requested.\n\t\t */\n\t\t\"mRender\": null,\n\t\n\t\n\t\t/**\n\t\t * Change the cell type created for the column - either TD cells or TH cells. This\n\t\t * can be useful as TH cells have semantic meaning in the table body, allowing them\n\t\t * to act as a header for a row (you may wish to add scope='row' to the TH elements).\n\t\t */\n\t\t\"sCellType\": \"td\",\n\t\n\t\n\t\t/**\n\t\t * Class to give to each cell in this column.\n\t\t */\n\t\t\"sClass\": \"\",\n\t\n\t\t/**\n\t\t * When DataTables calculates the column widths to assign to each column,\n\t\t * it finds the longest string in each column and then constructs a\n\t\t * temporary table and reads the widths from that. The problem with this\n\t\t * is that \"mmm\" is much wider then \"iiii\", but the latter is a longer\n\t\t * string - thus the calculation can go wrong (doing it properly and putting\n\t\t * it into an DOM object and measuring that is horribly(!) slow). Thus as\n\t\t * a \"work around\" we provide this option. It will append its value to the\n\t\t * text that is found to be the longest string for the column - i.e. padding.\n\t\t * Generally you shouldn't need this!\n\t\t */\n\t\t\"sContentPadding\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Allows a default value to be given for a column's data, and will be used\n\t\t * whenever a null data source is encountered (this can be because `data`\n\t\t * is set to null, or because the data source itself is null).\n\t\t */\n\t\t\"sDefaultContent\": null,\n\t\n\t\n\t\t/**\n\t\t * This parameter is only used in DataTables' server-side processing. It can\n\t\t * be exceptionally useful to know what columns are being displayed on the\n\t\t * client side, and to map these to database fields. When defined, the names\n\t\t * also allow DataTables to reorder information from the server if it comes\n\t\t * back in an unexpected order (i.e. if you switch your columns around on the\n\t\t * client-side, your server-side code does not also need updating).\n\t\t */\n\t\t\"sName\": \"\",\n\t\n\t\n\t\t/**\n\t\t * Defines a data source type for the ordering which can be used to read\n\t\t * real-time information from the table (updating the internally cached\n\t\t * version) prior to ordering. This allows ordering to occur on user\n\t\t * editable elements such as form inputs.\n\t\t */\n\t\t\"sSortDataType\": \"std\",\n\t\n\t\n\t\t/**\n\t\t * The title of this column.\n\t\t */\n\t\t\"sTitle\": null,\n\t\n\t\n\t\t/**\n\t\t * The type allows you to specify how the data for this column will be\n\t\t * ordered. Four types (string, numeric, date and html (which will strip\n\t\t * HTML tags before ordering)) are currently available. Note that only date\n\t\t * formats understood by JavaScript's Date() object will be accepted as type\n\t\t * date. For example: \"Mar 26, 2008 5:03 PM\". May take the values: 'string',\n\t\t * 'numeric', 'date' or 'html' (by default). Further types can be adding\n\t\t * through plug-ins.\n\t\t */\n\t\t\"sType\": null,\n\t\n\t\n\t\t/**\n\t\t * Defining the width of the column, this parameter may take any CSS value\n\t\t * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not\n\t\t * been given a specific width through this interface ensuring that the table\n\t\t * remains readable.\n\t\t */\n\t\t\"sWidth\": null\n\t};\n\t\n\t_fnHungarianMap( DataTable.defaults.column );\n\t\n\t\n\t\n\t/**\n\t * DataTables settings object - this holds all the information needed for a\n\t * given table, including configuration, data and current application of the\n\t * table options. DataTables does not have a single instance for each DataTable\n\t * with the settings attached to that instance, but rather instances of the\n\t * DataTable \"class\" are created on-the-fly as needed (typically by a\n\t * $().dataTable() call) and the settings object is then applied to that\n\t * instance.\n\t *\n\t * Note that this object is related to {@link DataTable.defaults} but this\n\t * one is the internal data store for DataTables's cache of columns. It should\n\t * NOT be manipulated outside of DataTables. Any configuration should be done\n\t * through the initialisation options.\n\t */\n\tDataTable.models.oSettings = {\n\t\t/**\n\t\t * Primary features of DataTables and their enablement state.\n\t\t */\n\t\t\"oFeatures\": {\n\t\n\t\t\t/**\n\t\t\t * Flag to say if DataTables should automatically try to calculate the\n\t\t\t * optimum table and columns widths (true) or not (false).\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bAutoWidth\": null,\n\t\n\t\t\t/**\n\t\t\t * Delay the creation of TR and TD elements until they are actually\n\t\t\t * needed by a driven page draw. This can give a significant speed\n\t\t\t * increase for Ajax source and JavaScript source data, but makes no\n\t\t\t * difference at all for DOM and server-side processing tables.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bDeferRender\": null,\n\t\n\t\t\t/**\n\t\t\t * Enable filtering on the table or not. Note that if this is disabled\n\t\t\t * then there is no filtering at all on the table, including fnFilter.\n\t\t\t * To just remove the filtering input use sDom and remove the 'f' option.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bFilter\": null,\n\t\n\t\t\t/**\n\t\t\t * Used only for compatibility with DT1\n\t\t\t * @deprecated\n\t\t\t */\n\t\t\t\"bInfo\": true,\n\t\n\t\t\t/**\n\t\t\t * Used only for compatibility with DT1\n\t\t\t * @deprecated\n\t\t\t */\n\t\t\t\"bLengthChange\": true,\n\t\n\t\t\t/**\n\t\t\t * Pagination enabled or not. Note that if this is disabled then length\n\t\t\t * changing must also be disabled.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bPaginate\": null,\n\t\n\t\t\t/**\n\t\t\t * Processing indicator enable flag whenever DataTables is enacting a\n\t\t\t * user request - typically an Ajax request for server-side processing.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bProcessing\": null,\n\t\n\t\t\t/**\n\t\t\t * Server-side processing enabled flag - when enabled DataTables will\n\t\t\t * get all data from the server for every draw - there is no filtering,\n\t\t\t * sorting or paging done on the client-side.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bServerSide\": null,\n\t\n\t\t\t/**\n\t\t\t * Sorting enablement flag.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bSort\": null,\n\t\n\t\t\t/**\n\t\t\t * Multi-column sorting\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bSortMulti\": null,\n\t\n\t\t\t/**\n\t\t\t * Apply a class to the columns which are being sorted to provide a\n\t\t\t * visual highlight or not. This can slow things down when enabled since\n\t\t\t * there is a lot of DOM interaction.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bSortClasses\": null,\n\t\n\t\t\t/**\n\t\t\t * State saving enablement flag.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bStateSave\": null\n\t\t},\n\t\n\t\n\t\t/**\n\t\t * Scrolling settings for a table.\n\t\t */\n\t\t\"oScroll\": {\n\t\t\t/**\n\t\t\t * When the table is shorter in height than sScrollY, collapse the\n\t\t\t * table container down to the height of the table (when true).\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"bCollapse\": null,\n\t\n\t\t\t/**\n\t\t\t * Width of the scrollbar for the web-browser's platform. Calculated\n\t\t\t * during table initialisation.\n\t\t\t */\n\t\t\t\"iBarWidth\": 0,\n\t\n\t\t\t/**\n\t\t\t * Viewport width for horizontal scrolling. Horizontal scrolling is\n\t\t\t * disabled if an empty string.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"sX\": null,\n\t\n\t\t\t/**\n\t\t\t * Width to expand the table to when using x-scrolling. Typically you\n\t\t\t * should not need to use this.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t *  @deprecated\n\t\t\t */\n\t\t\t\"sXInner\": null,\n\t\n\t\t\t/**\n\t\t\t * Viewport height for vertical scrolling. Vertical scrolling is disabled\n\t\t\t * if an empty string.\n\t\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t\t * set a default use {@link DataTable.defaults}.\n\t\t\t */\n\t\t\t\"sY\": null\n\t\t},\n\t\n\t\t/**\n\t\t * Language information for the table.\n\t\t */\n\t\t\"oLanguage\": {\n\t\t\t/**\n\t\t\t * Information callback function. See\n\t\t\t * {@link DataTable.defaults.fnInfoCallback}\n\t\t\t */\n\t\t\t\"fnInfoCallback\": null\n\t\t},\n\t\n\t\t/**\n\t\t * Browser support parameters\n\t\t */\n\t\t\"oBrowser\": {\n\t\t\t/**\n\t\t\t * Determine if the vertical scrollbar is on the right or left of the\n\t\t\t * scrolling container - needed for rtl language layout, although not\n\t\t\t * all browsers move the scrollbar (Safari).\n\t\t\t */\n\t\t\t\"bScrollbarLeft\": false,\n\t\n\t\t\t/**\n\t\t\t * Browser scrollbar width\n\t\t\t */\n\t\t\t\"barWidth\": 0\n\t\t},\n\t\n\t\n\t\t\"ajax\": null,\n\t\n\t\n\t\t/**\n\t\t * Array referencing the nodes which are used for the features. The\n\t\t * parameters of this object match what is allowed by sDom - i.e.\n\t\t *   <ul>\n\t\t *     <li>'l' - Length changing</li>\n\t\t *     <li>'f' - Filtering input</li>\n\t\t *     <li>'t' - The table!</li>\n\t\t *     <li>'i' - Information</li>\n\t\t *     <li>'p' - Pagination</li>\n\t\t *     <li>'r' - pRocessing</li>\n\t\t *   </ul>\n\t\t */\n\t\t\"aanFeatures\": [],\n\t\n\t\t/**\n\t\t * Store data information - see {@link DataTable.models.oRow} for detailed\n\t\t * information.\n\t\t */\n\t\t\"aoData\": [],\n\t\n\t\t/**\n\t\t * Array of indexes which are in the current display (after filtering etc)\n\t\t */\n\t\t\"aiDisplay\": [],\n\t\n\t\t/**\n\t\t * Array of indexes for display - no filtering\n\t\t */\n\t\t\"aiDisplayMaster\": [],\n\t\n\t\t/**\n\t\t * Map of row ids to data indexes\n\t\t */\n\t\t\"aIds\": {},\n\t\n\t\t/**\n\t\t * Store information about each column that is in use\n\t\t */\n\t\t\"aoColumns\": [],\n\t\n\t\t/**\n\t\t * Store information about the table's header\n\t\t */\n\t\t\"aoHeader\": [],\n\t\n\t\t/**\n\t\t * Store information about the table's footer\n\t\t */\n\t\t\"aoFooter\": [],\n\t\n\t\t/**\n\t\t * Store the applied global search information in case we want to force a\n\t\t * research or compare the old search to a new one.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"oPreviousSearch\": {},\n\t\n\t\t/**\n\t\t * Store for named searches\n\t\t */\n\t\tsearchFixed: {},\n\t\n\t\t/**\n\t\t * Store the applied search for each column - see\n\t\t * {@link DataTable.models.oSearch} for the format that is used for the\n\t\t * filtering information for each column.\n\t\t */\n\t\t\"aoPreSearchCols\": [],\n\t\n\t\t/**\n\t\t * Sorting that is applied to the table. Note that the inner arrays are\n\t\t * used in the following manner:\n\t\t * <ul>\n\t\t *   <li>Index 0 - column number</li>\n\t\t *   <li>Index 1 - current sorting direction</li>\n\t\t * </ul>\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"aaSorting\": null,\n\t\n\t\t/**\n\t\t * Sorting that is always applied to the table (i.e. prefixed in front of\n\t\t * aaSorting).\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"aaSortingFixed\": [],\n\t\n\t\t/**\n\t\t * If restoring a table - we should restore its width\n\t\t */\n\t\t\"sDestroyWidth\": 0,\n\t\n\t\t/**\n\t\t * Callback functions array for every time a row is inserted (i.e. on a draw).\n\t\t */\n\t\t\"aoRowCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for the header on each draw.\n\t\t */\n\t\t\"aoHeaderCallback\": [],\n\t\n\t\t/**\n\t\t * Callback function for the footer on each draw.\n\t\t */\n\t\t\"aoFooterCallback\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for draw callback functions\n\t\t */\n\t\t\"aoDrawCallback\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for row created function\n\t\t */\n\t\t\"aoRowCreatedCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for just before the table is redrawn. A return of\n\t\t * false will be used to cancel the draw.\n\t\t */\n\t\t\"aoPreDrawCallback\": [],\n\t\n\t\t/**\n\t\t * Callback functions for when the table has been initialised.\n\t\t */\n\t\t\"aoInitComplete\": [],\n\t\n\t\n\t\t/**\n\t\t * Callbacks for modifying the settings to be stored for state saving, prior to\n\t\t * saving state.\n\t\t */\n\t\t\"aoStateSaveParams\": [],\n\t\n\t\t/**\n\t\t * Callbacks for modifying the settings that have been stored for state saving\n\t\t * prior to using the stored values to restore the state.\n\t\t */\n\t\t\"aoStateLoadParams\": [],\n\t\n\t\t/**\n\t\t * Callbacks for operating on the settings object once the saved state has been\n\t\t * loaded\n\t\t */\n\t\t\"aoStateLoaded\": [],\n\t\n\t\t/**\n\t\t * Cache the table ID for quick access\n\t\t */\n\t\t\"sTableId\": \"\",\n\t\n\t\t/**\n\t\t * The TABLE node for the main table\n\t\t */\n\t\t\"nTable\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the thead element\n\t\t */\n\t\t\"nTHead\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the tfoot element - if it exists\n\t\t */\n\t\t\"nTFoot\": null,\n\t\n\t\t/**\n\t\t * Permanent ref to the tbody element\n\t\t */\n\t\t\"nTBody\": null,\n\t\n\t\t/**\n\t\t * Cache the wrapper node (contains all DataTables controlled elements)\n\t\t */\n\t\t\"nTableWrapper\": null,\n\t\n\t\t/**\n\t\t * Indicate if all required information has been read in\n\t\t */\n\t\t\"bInitialised\": false,\n\t\n\t\t/**\n\t\t * Information about open rows. Each object in the array has the parameters\n\t\t * 'nTr' and 'nParent'\n\t\t */\n\t\t\"aoOpenRows\": [],\n\t\n\t\t/**\n\t\t * Dictate the positioning of DataTables' control elements - see\n\t\t * {@link DataTable.model.oInit.sDom}.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"sDom\": null,\n\t\n\t\t/**\n\t\t * Search delay (in mS)\n\t\t */\n\t\t\"searchDelay\": null,\n\t\n\t\t/**\n\t\t * Which type of pagination should be used.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"sPaginationType\": \"two_button\",\n\t\n\t\t/**\n\t\t * Number of paging controls on the page. Only used for backwards compatibility\n\t\t */\n\t\tpagingControls: 0,\n\t\n\t\t/**\n\t\t * The state duration (for `stateSave`) in seconds.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"iStateDuration\": 0,\n\t\n\t\t/**\n\t\t * Array of callback functions for state saving. Each array element is an\n\t\t * object with the following parameters:\n\t\t *   <ul>\n\t\t *     <li>function:fn - function to call. Takes two parameters, oSettings\n\t\t *       and the JSON string to save that has been thus far created. Returns\n\t\t *       a JSON string to be inserted into a json object\n\t\t *       (i.e. '\"param\": [ 0, 1, 2]')</li>\n\t\t *     <li>string:sName - name of callback</li>\n\t\t *   </ul>\n\t\t */\n\t\t\"aoStateSave\": [],\n\t\n\t\t/**\n\t\t * Array of callback functions for state loading. Each array element is an\n\t\t * object with the following parameters:\n\t\t *   <ul>\n\t\t *     <li>function:fn - function to call. Takes two parameters, oSettings\n\t\t *       and the object stored. May return false to cancel state loading</li>\n\t\t *     <li>string:sName - name of callback</li>\n\t\t *   </ul>\n\t\t */\n\t\t\"aoStateLoad\": [],\n\t\n\t\t/**\n\t\t * State that was saved. Useful for back reference\n\t\t */\n\t\t\"oSavedState\": null,\n\t\n\t\t/**\n\t\t * State that was loaded. Useful for back reference\n\t\t */\n\t\t\"oLoadedState\": null,\n\t\n\t\t/**\n\t\t * Note if draw should be blocked while getting data\n\t\t */\n\t\t\"bAjaxDataGet\": true,\n\t\n\t\t/**\n\t\t * The last jQuery XHR object that was used for server-side data gathering.\n\t\t * This can be used for working with the XHR information in one of the\n\t\t * callbacks\n\t\t */\n\t\t\"jqXHR\": null,\n\t\n\t\t/**\n\t\t * JSON returned from the server in the last Ajax request\n\t\t */\n\t\t\"json\": undefined,\n\t\n\t\t/**\n\t\t * Data submitted as part of the last Ajax request\n\t\t */\n\t\t\"oAjaxData\": undefined,\n\t\n\t\t/**\n\t\t * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if\n\t\t * required).\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"sServerMethod\": null,\n\t\n\t\t/**\n\t\t * Format numbers for display.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"fnFormatNumber\": null,\n\t\n\t\t/**\n\t\t * List of options that can be used for the user selectable length menu.\n\t\t * Note that this parameter will be set by the initialisation routine. To\n\t\t * set a default use {@link DataTable.defaults}.\n\t\t */\n\t\t\"aLengthMenu\": null,\n\t\n\t\t/**\n\t\t * Counter for the draws that the table does. Also used as a tracker for\n\t\t * server-side processing\n\t\t */\n\t\t\"iDraw\": 0,\n\t\n\t\t/**\n\t\t * Indicate if a redraw is being done - useful for Ajax\n\t\t */\n\t\t\"bDrawing\": false,\n\t\n\t\t/**\n\t\t * Draw index (iDraw) of the last error when parsing the returned data\n\t\t */\n\t\t\"iDrawError\": -1,\n\t\n\t\t/**\n\t\t * Paging display length\n\t\t */\n\t\t\"_iDisplayLength\": 10,\n\t\n\t\t/**\n\t\t * Paging start point - aiDisplay index\n\t\t */\n\t\t\"_iDisplayStart\": 0,\n\t\n\t\t/**\n\t\t * Server-side processing - number of records in the result set\n\t\t * (i.e. before filtering), Use fnRecordsTotal rather than\n\t\t * this property to get the value of the number of records, regardless of\n\t\t * the server-side processing setting.\n\t\t */\n\t\t\"_iRecordsTotal\": 0,\n\t\n\t\t/**\n\t\t * Server-side processing - number of records in the current display set\n\t\t * (i.e. after filtering). Use fnRecordsDisplay rather than\n\t\t * this property to get the value of the number of records, regardless of\n\t\t * the server-side processing setting.\n\t\t */\n\t\t\"_iRecordsDisplay\": 0,\n\t\n\t\t/**\n\t\t * The classes to use for the table\n\t\t */\n\t\t\"oClasses\": {},\n\t\n\t\t/**\n\t\t * Flag attached to the settings object so you can check in the draw\n\t\t * callback if filtering has been done in the draw. Deprecated in favour of\n\t\t * events.\n\t\t *  @deprecated\n\t\t */\n\t\t\"bFiltered\": false,\n\t\n\t\t/**\n\t\t * Flag attached to the settings object so you can check in the draw\n\t\t * callback if sorting has been done in the draw. Deprecated in favour of\n\t\t * events.\n\t\t *  @deprecated\n\t\t */\n\t\t\"bSorted\": false,\n\t\n\t\t/**\n\t\t * Indicate that if multiple rows are in the header and there is more than\n\t\t * one unique cell per column. Replaced by titleRow\n\t\t */\n\t\t\"bSortCellsTop\": null,\n\t\n\t\t/**\n\t\t * Initialisation object that is used for the table\n\t\t */\n\t\t\"oInit\": null,\n\t\n\t\t/**\n\t\t * Destroy callback functions - for plug-ins to attach themselves to the\n\t\t * destroy so they can clean up markup and events.\n\t\t */\n\t\t\"aoDestroyCallback\": [],\n\t\n\t\n\t\t/**\n\t\t * Get the number of records in the current record set, before filtering\n\t\t */\n\t\t\"fnRecordsTotal\": function ()\n\t\t{\n\t\t\treturn _fnDataSource( this ) == 'ssp' ?\n\t\t\t\tthis._iRecordsTotal * 1 :\n\t\t\t\tthis.aiDisplayMaster.length;\n\t\t},\n\t\n\t\t/**\n\t\t * Get the number of records in the current record set, after filtering\n\t\t */\n\t\t\"fnRecordsDisplay\": function ()\n\t\t{\n\t\t\treturn _fnDataSource( this ) == 'ssp' ?\n\t\t\t\tthis._iRecordsDisplay * 1 :\n\t\t\t\tthis.aiDisplay.length;\n\t\t},\n\t\n\t\t/**\n\t\t * Get the display end point - aiDisplay index\n\t\t */\n\t\t\"fnDisplayEnd\": function ()\n\t\t{\n\t\t\tvar\n\t\t\t\tlen      = this._iDisplayLength,\n\t\t\t\tstart    = this._iDisplayStart,\n\t\t\t\tcalc     = start + len,\n\t\t\t\trecords  = this.aiDisplay.length,\n\t\t\t\tfeatures = this.oFeatures,\n\t\t\t\tpaginate = features.bPaginate;\n\t\n\t\t\tif ( features.bServerSide ) {\n\t\t\t\treturn paginate === false || len === -1 ?\n\t\t\t\t\tstart + records :\n\t\t\t\t\tMath.min( start+len, this._iRecordsDisplay );\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn ! paginate || calc>records || len===-1 ?\n\t\t\t\t\trecords :\n\t\t\t\t\tcalc;\n\t\t\t}\n\t\t},\n\t\n\t\t/**\n\t\t * The DataTables object for this table\n\t\t */\n\t\t\"oInstance\": null,\n\t\n\t\t/**\n\t\t * Unique identifier for each instance of the DataTables object. If there\n\t\t * is an ID on the table node, then it takes that value, otherwise an\n\t\t * incrementing internal counter is used.\n\t\t */\n\t\t\"sInstance\": null,\n\t\n\t\t/**\n\t\t * tabindex attribute value that is added to DataTables control elements, allowing\n\t\t * keyboard navigation of the table and its controls.\n\t\t */\n\t\t\"iTabIndex\": 0,\n\t\n\t\t/**\n\t\t * DIV container for the footer scrolling table if scrolling\n\t\t */\n\t\t\"nScrollHead\": null,\n\t\n\t\t/**\n\t\t * DIV container for the footer scrolling table if scrolling\n\t\t */\n\t\t\"nScrollFoot\": null,\n\t\n\t\t/**\n\t\t * Last applied sort\n\t\t */\n\t\t\"aLastSort\": [],\n\t\n\t\t/**\n\t\t * Stored plug-in instances\n\t\t */\n\t\t\"oPlugins\": {},\n\t\n\t\t/**\n\t\t * Function used to get a row's id from the row's data\n\t\t */\n\t\t\"rowIdFn\": null,\n\t\n\t\t/**\n\t\t * Data location where to store a row's id\n\t\t */\n\t\t\"rowId\": null,\n\t\n\t\tcaption: '',\n\t\n\t\tcaptionNode: null,\n\t\n\t\tcolgroup: null,\n\t\n\t\t/** Delay loading of data */\n\t\tdeferLoading: null,\n\t\n\t\t/** Allow auto type detection */\n\t\ttypeDetect: true,\n\t\n\t\t/** ResizeObserver for the container div */\n\t\tresizeObserver: null,\n\t\n\t\t/** Keep a record of the last size of the container, so we can skip duplicates */\n\t\tcontainerWidth: -1,\n\t\n\t\t/** Reverse the initial order of the data set on desc ordering */\n\t\torderDescReverse: null,\n\t\n\t\t/** Show / hide ordering indicators in headers */\n\t\torderIndicators: true,\n\t\n\t\t/** Default ordering listener */\n\t\torderHandler: true,\n\t\n\t\t/** Title row indicator */\n\t\ttitleRow: null,\n\t\n\t\t/** Title wrapper element type */\n\t\tcolumnTitleTag: 'span'\n\t};\n\t\n\t/**\n\t * Extension object for DataTables that is used to provide all extension\n\t * options.\n\t *\n\t * Note that the `DataTable.ext` object is available through\n\t * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is\n\t * also aliased to `jQuery.fn.dataTableExt` for historic reasons.\n\t *  @namespace\n\t *  @extends DataTable.models.ext\n\t */\n\t\n\t\n\tvar extPagination = DataTable.ext.pager;\n\t\n\t// Paging buttons configuration\n\t$.extend( extPagination, {\n\t\tsimple: function () {\n\t\t\treturn [ 'previous', 'next' ];\n\t\t},\n\t\n\t\tfull: function () {\n\t\t\treturn [ 'first', 'previous', 'next', 'last' ];\n\t\t},\n\t\n\t\tnumbers: function () {\n\t\t\treturn [ 'numbers' ];\n\t\t},\n\t\n\t\tsimple_numbers: function () {\n\t\t\treturn [ 'previous', 'numbers', 'next' ];\n\t\t},\n\t\n\t\tfull_numbers: function () {\n\t\t\treturn [ 'first', 'previous', 'numbers', 'next', 'last' ];\n\t\t},\n\t\n\t\tfirst_last: function () {\n\t\t\treturn ['first', 'last'];\n\t\t},\n\t\n\t\tfirst_last_numbers: function () {\n\t\t\treturn ['first', 'numbers', 'last'];\n\t\t},\n\t\n\t\t// For testing and plug-ins to use\n\t\t_numbers: _pagingNumbers,\n\t\n\t\t// Number of number buttons - legacy, use `numbers` option for paging feature\n\t\tnumbers_length: 7\n\t} );\n\t\n\t\n\t$.extend( true, DataTable.ext.renderer, {\n\t\tpagingButton: {\n\t\t\t_: function (settings, buttonType, content, active, disabled) {\n\t\t\t\tvar classes = settings.oClasses.paging;\n\t\t\t\tvar btnClasses = [classes.button];\n\t\t\t\tvar btn;\n\t\n\t\t\t\tif (active) {\n\t\t\t\t\tbtnClasses.push(classes.active);\n\t\t\t\t}\n\t\n\t\t\t\tif (disabled) {\n\t\t\t\t\tbtnClasses.push(classes.disabled)\n\t\t\t\t}\n\t\n\t\t\t\tif (buttonType === 'ellipsis') {\n\t\t\t\t\tbtn = $('<span class=\"ellipsis\"></span>').html(content)[0];\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tbtn = $('<button>', {\n\t\t\t\t\t\tclass: btnClasses.join(' '),\n\t\t\t\t\t\trole: 'link',\n\t\t\t\t\t\ttype: 'button'\n\t\t\t\t\t}).html(content);\n\t\t\t\t}\n\t\n\t\t\t\treturn {\n\t\t\t\t\tdisplay: btn,\n\t\t\t\t\tclicker: btn\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\n\t\tpagingContainer: {\n\t\t\t_: function (settings, buttons) {\n\t\t\t\t// No wrapping element - just append directly to the host\n\t\t\t\treturn buttons;\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t// Common function to remove new lines, strip HTML and diacritic control\n\tvar _filterString = function (stripHtml, normalize) {\n\t\treturn function (str) {\n\t\t\tif (_empty(str) || typeof str !== 'string') {\n\t\t\t\treturn str;\n\t\t\t}\n\t\n\t\t\tstr = str.replace( _re_new_lines, \" \" );\n\t\n\t\t\tif (stripHtml) {\n\t\t\t\tstr = _stripHtml(str);\n\t\t\t}\n\t\n\t\t\tif (normalize) {\n\t\t\t\tstr = _normalize(str, false);\n\t\t\t}\n\t\n\t\t\treturn str;\n\t\t};\n\t}\n\t\n\t/*\n\t * Public helper functions. These aren't used internally by DataTables, or\n\t * called by any of the options passed into DataTables, but they can be used\n\t * externally by developers working with DataTables. They are helper functions\n\t * to make working with DataTables a little bit easier.\n\t */\n\t\n\t/**\n\t * Common logic for moment, luxon or a date action.\n\t *\n\t * Happens after __mldObj, so don't need to call `resolveWindowsLibs` again\n\t */\n\tfunction __mld( dtLib, momentFn, luxonFn, dateFn, arg1 ) {\n\t\tif (__moment) {\n\t\t\treturn dtLib[momentFn]( arg1 );\n\t\t}\n\t\telse if (__luxon) {\n\t\t\treturn dtLib[luxonFn]( arg1 );\n\t\t}\n\t\t\n\t\treturn dateFn ? dtLib[dateFn]( arg1 ) : dtLib;\n\t}\n\t\n\t\n\tvar __mlWarning = false;\n\tvar __luxon; // Can be assigned in DateTable.use()\n\tvar __moment; // Can be assigned in DateTable.use()\n\t\n\t/**\n\t * \n\t */\n\tfunction resolveWindowLibs() {\n\t\tif (window.luxon && ! __luxon) {\n\t\t\t__luxon = window.luxon;\n\t\t}\n\t\t\n\t\tif (window.moment && ! __moment) {\n\t\t\t__moment = window.moment;\n\t\t}\n\t}\n\t\n\tfunction __mldObj (d, format, locale) {\n\t\tvar dt;\n\t\n\t\tresolveWindowLibs();\n\t\n\t\tif (__moment) {\n\t\t\tdt = __moment.utc( d, format, locale, true );\n\t\n\t\t\tif (! dt.isValid()) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\telse if (__luxon) {\n\t\t\tdt = format && typeof d === 'string'\n\t\t\t\t? __luxon.DateTime.fromFormat( d, format )\n\t\t\t\t: __luxon.DateTime.fromISO( d );\n\t\n\t\t\tif (! dt.isValid) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\n\t\t\tdt = dt.setLocale(locale);\n\t\t}\n\t\telse if (! format) {\n\t\t\t// No format given, must be ISO\n\t\t\tdt = new Date(d);\n\t\t}\n\t\telse {\n\t\t\tif (! __mlWarning) {\n\t\t\t\talert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17');\n\t\t\t}\n\t\n\t\t\t__mlWarning = true;\n\t\t}\n\t\n\t\treturn dt;\n\t}\n\t\n\t// Wrapper for date, datetime and time which all operate the same way with the exception of\n\t// the output string for auto locale support\n\tfunction __mlHelper (localeString) {\n\t\treturn function ( from, to, locale, def ) {\n\t\t\t// Luxon and Moment support\n\t\t\t// Argument shifting\n\t\t\tif ( arguments.length === 0 ) {\n\t\t\t\tlocale = 'en';\n\t\t\t\tto = null; // means toLocaleString\n\t\t\t\tfrom = null; // means iso8601\n\t\t\t}\n\t\t\telse if ( arguments.length === 1 ) {\n\t\t\t\tlocale = 'en';\n\t\t\t\tto = from;\n\t\t\t\tfrom = null;\n\t\t\t}\n\t\t\telse if ( arguments.length === 2 ) {\n\t\t\t\tlocale = to;\n\t\t\t\tto = from;\n\t\t\t\tfrom = null;\n\t\t\t}\n\t\n\t\t\tvar typeName = 'datetime' + (to ? '-' + to : '');\n\t\n\t\t\t// Add type detection and sorting specific to this date format - we need to be able to identify\n\t\t\t// date type columns as such, rather than as numbers in extensions. Hence the need for this.\n\t\t\tif (! DataTable.ext.type.order[typeName + '-pre']) {\n\t\t\t\tDataTable.type(typeName, {\n\t\t\t\t\tdetect: function (d) {\n\t\t\t\t\t\t// The renderer will give the value to type detect as the type!\n\t\t\t\t\t\treturn d === typeName ? typeName : false;\n\t\t\t\t\t},\n\t\t\t\t\torder: {\n\t\t\t\t\t\tpre: function (d) {\n\t\t\t\t\t\t\t// The renderer gives us Moment, Luxon or Date objects for the sorting, all of which have a\n\t\t\t\t\t\t\t// `valueOf` which gives milliseconds epoch\n\t\t\t\t\t\t\treturn d.valueOf();\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tclassName: 'dt-right'\n\t\t\t\t});\n\t\t\t}\n\t\t\n\t\t\treturn function ( d, type ) {\n\t\t\t\t// Allow for a default value\n\t\t\t\tif (d === null || d === undefined) {\n\t\t\t\t\tif (def === '--now') {\n\t\t\t\t\t\t// We treat everything as UTC further down, so no changes are\n\t\t\t\t\t\t// made, as such need to get the local date / time as if it were\n\t\t\t\t\t\t// UTC\n\t\t\t\t\t\tvar local = new Date();\n\t\t\t\t\t\td = new Date( Date.UTC(\n\t\t\t\t\t\t\tlocal.getFullYear(), local.getMonth(), local.getDate(),\n\t\t\t\t\t\t\tlocal.getHours(), local.getMinutes(), local.getSeconds()\n\t\t\t\t\t\t) );\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\td = '';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\n\t\t\t\tif (type === 'type') {\n\t\t\t\t\t// Typing uses the type name for fast matching\n\t\t\t\t\treturn typeName;\n\t\t\t\t}\n\t\n\t\t\t\tif (d === '') {\n\t\t\t\t\treturn type !== 'sort'\n\t\t\t\t\t\t? ''\n\t\t\t\t\t\t: __mldObj('0000-01-01 00:00:00', null, locale);\n\t\t\t\t}\n\t\n\t\t\t\t// Shortcut. If `from` and `to` are the same, we are using the renderer to\n\t\t\t\t// format for ordering, not display - its already in the display format.\n\t\t\t\tif ( to !== null && from === to && type !== 'sort' && type !== 'type' && ! (d instanceof Date) ) {\n\t\t\t\t\treturn d;\n\t\t\t\t}\n\t\n\t\t\t\tvar dt = __mldObj(d, from, locale);\n\t\n\t\t\t\tif (dt === null) {\n\t\t\t\t\treturn d;\n\t\t\t\t}\n\t\n\t\t\t\tif (type === 'sort') {\n\t\t\t\t\treturn dt;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tvar formatted = to === null\n\t\t\t\t\t? __mld(dt, 'toDate', 'toJSDate', '')[localeString](\n\t\t\t\t\t\tnavigator.language,\n\t\t\t\t\t\t{ timeZone: \"UTC\" }\n\t\t\t\t\t)\n\t\t\t\t\t: __mld(dt, 'format', 'toFormat', 'toISOString', to);\n\t\n\t\t\t\t// XSS protection\n\t\t\t\treturn type === 'display' ?\n\t\t\t\t\t_escapeHtml( formatted ) :\n\t\t\t\t\tformatted;\n\t\t\t};\n\t\t}\n\t}\n\t\n\t// Based on locale, determine standard number formatting\n\t// Fallback for legacy browsers is US English\n\tvar __thousands = ',';\n\tvar __decimal = '.';\n\t\n\tif (window.Intl !== undefined) {\n\t\ttry {\n\t\t\tvar num = new Intl.NumberFormat().formatToParts(100000.1);\n\t\t\n\t\t\tfor (var i=0 ; i<num.length ; i++) {\n\t\t\t\tif (num[i].type === 'group') {\n\t\t\t\t\t__thousands = num[i].value;\n\t\t\t\t}\n\t\t\t\telse if (num[i].type === 'decimal') {\n\t\t\t\t\t__decimal = num[i].value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcatch (e) {\n\t\t\t// noop\n\t\t}\n\t}\n\t\n\t// Formatted date time detection - use by declaring the formats you are going to use\n\tDataTable.datetime = function ( format, locale ) {\n\t\tvar typeName = 'datetime-' + format;\n\t\n\t\tif (! locale) {\n\t\t\tlocale = 'en';\n\t\t}\n\t\n\t\tif (! DataTable.ext.type.order[typeName]) {\n\t\t\tDataTable.type(typeName, {\n\t\t\t\tdetect: function (d) {\n\t\t\t\t\tvar dt = __mldObj(d, format, locale);\n\t\t\t\t\treturn d === '' || dt ? typeName : false;\n\t\t\t\t},\n\t\t\t\torder: {\n\t\t\t\t\tpre: function (d) {\n\t\t\t\t\t\treturn __mldObj(d, format, locale) || 0;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tclassName: 'dt-right'\n\t\t\t});\n\t\t}\n\t}\n\t\n\t/**\n\t * Helpers for `columns.render`.\n\t *\n\t * The options defined here can be used with the `columns.render` initialisation\n\t * option to provide a display renderer. The following functions are defined:\n\t *\n\t * * `moment` - Uses the MomentJS library to convert from a given format into another.\n\t * This renderer has three overloads:\n\t *   * 1 parameter:\n\t *     * `string` - Format to convert to (assumes input is ISO8601 and locale is `en`)\n\t *   * 2 parameters:\n\t *     * `string` - Format to convert from\n\t *     * `string` - Format to convert to. Assumes `en` locale\n\t *   * 3 parameters:\n\t *     * `string` - Format to convert from\n\t *     * `string` - Format to convert to\n\t *     * `string` - Locale\n\t * * `number` - Will format numeric data (defined by `columns.data`) for\n\t *   display, retaining the original unformatted data for sorting and filtering.\n\t *   It takes 5 parameters:\n\t *   * `string` - Thousands grouping separator\n\t *   * `string` - Decimal point indicator\n\t *   * `integer` - Number of decimal points to show\n\t *   * `string` (optional) - Prefix.\n\t *   * `string` (optional) - Postfix (/suffix).\n\t * * `text` - Escape HTML to help prevent XSS attacks. It has no optional\n\t *   parameters.\n\t *\n\t * @example\n\t *   // Column definition using the number renderer\n\t *   {\n\t *     data: \"salary\",\n\t *     render: $.fn.dataTable.render.number( '\\'', '.', 0, '$' )\n\t *   }\n\t *\n\t * @namespace\n\t */\n\tDataTable.render = {\n\t\tdate: __mlHelper('toLocaleDateString'),\n\t\tdatetime: __mlHelper('toLocaleString'),\n\t\ttime: __mlHelper('toLocaleTimeString'),\n\t\tnumber: function ( thousands, decimal, precision, prefix, postfix ) {\n\t\t\t// Auto locale detection\n\t\t\tif (thousands === null || thousands === undefined) {\n\t\t\t\tthousands = __thousands;\n\t\t\t}\n\t\n\t\t\tif (decimal === null || decimal === undefined) {\n\t\t\t\tdecimal = __decimal;\n\t\t\t}\n\t\n\t\t\treturn {\n\t\t\t\tdisplay: function ( d ) {\n\t\t\t\t\tif ( typeof d !== 'number' && typeof d !== 'string' ) {\n\t\t\t\t\t\treturn d;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif (d === '' || d === null) {\n\t\t\t\t\t\treturn d;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar negative = d < 0 ? '-' : '';\n\t\t\t\t\tvar flo = parseFloat( d );\n\t\t\t\t\tvar abs = Math.abs(flo);\n\t\n\t\t\t\t\t// Scientific notation for large and small numbers\n\t\t\t\t\tif (abs >= 100000000000 || (abs < 0.0001 && abs !== 0) ) {\n\t\t\t\t\t\tvar exp = flo.toExponential(precision).split(/e\\+?/);\n\t\t\t\t\t\treturn exp[0] + ' x 10<sup>' + exp[1] + '</sup>';\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// If NaN then there isn't much formatting that we can do - just\n\t\t\t\t\t// return immediately, escaping any HTML (this was supposed to\n\t\t\t\t\t// be a number after all)\n\t\t\t\t\tif ( isNaN( flo ) ) {\n\t\t\t\t\t\treturn _escapeHtml( d );\n\t\t\t\t\t}\n\t\n\t\t\t\t\tflo = flo.toFixed( precision );\n\t\t\t\t\td = Math.abs( flo );\n\t\n\t\t\t\t\tvar intPart = parseInt( d, 10 );\n\t\t\t\t\tvar floatPart = precision ?\n\t\t\t\t\t\tdecimal+(d - intPart).toFixed( precision ).substring( 2 ):\n\t\t\t\t\t\t'';\n\t\n\t\t\t\t\t// If zero, then can't have a negative prefix\n\t\t\t\t\tif (intPart === 0 && parseFloat(floatPart) === 0) {\n\t\t\t\t\t\tnegative = '';\n\t\t\t\t\t}\n\t\n\t\t\t\t\treturn negative + (prefix||'') +\n\t\t\t\t\t\tintPart.toString().replace(\n\t\t\t\t\t\t\t/\\B(?=(\\d{3})+(?!\\d))/g, thousands\n\t\t\t\t\t\t) +\n\t\t\t\t\t\tfloatPart +\n\t\t\t\t\t\t(postfix||'');\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\n\t\ttext: function () {\n\t\t\treturn {\n\t\t\t\tdisplay: _escapeHtml,\n\t\t\t\tfilter: _escapeHtml\n\t\t\t};\n\t\t}\n\t};\n\t\n\t\n\tvar _extTypes = DataTable.ext.type;\n\t\n\t// Get / set type\n\tDataTable.type = function (name, prop, val) {\n\t\tif (! prop) {\n\t\t\treturn {\n\t\t\t\tclassName: _extTypes.className[name],\n\t\t\t\tdetect: _extTypes.detect.find(function (fn) {\n\t\t\t\t\treturn fn._name === name;\n\t\t\t\t}),\n\t\t\t\torder: {\n\t\t\t\t\tpre: _extTypes.order[name + '-pre'],\n\t\t\t\t\tasc: _extTypes.order[name + '-asc'],\n\t\t\t\t\tdesc: _extTypes.order[name + '-desc']\n\t\t\t\t},\n\t\t\t\trender: _extTypes.render[name],\n\t\t\t\tsearch: _extTypes.search[name]\n\t\t\t};\n\t\t}\n\t\n\t\tvar setProp = function(prop, propVal) {\n\t\t\t_extTypes[prop][name] = propVal;\n\t\t};\n\t\tvar setDetect = function (detect) {\n\t\t\t// `detect` can be a function or an object - we set a name\n\t\t\t// property for either - that is used for the detection\n\t\t\tObject.defineProperty(detect, \"_name\", {value: name});\n\t\n\t\t\tvar idx = _extTypes.detect.findIndex(function (item) {\n\t\t\t\treturn item._name === name;\n\t\t\t});\n\t\n\t\t\tif (idx === -1) {\n\t\t\t\t_extTypes.detect.unshift(detect);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t_extTypes.detect.splice(idx, 1, detect);\n\t\t\t}\n\t\t};\n\t\tvar setOrder = function (obj) {\n\t\t\t_extTypes.order[name + '-pre'] = obj.pre; // can be undefined\n\t\t\t_extTypes.order[name + '-asc'] = obj.asc; // can be undefined\n\t\t\t_extTypes.order[name + '-desc'] = obj.desc; // can be undefined\n\t\t};\n\t\n\t\t// prop is optional\n\t\tif (val === undefined) {\n\t\t\tval = prop;\n\t\t\tprop = null;\n\t\t}\n\t\n\t\tif (prop === 'className') {\n\t\t\tsetProp('className', val);\n\t\t}\n\t\telse if (prop === 'detect') {\n\t\t\tsetDetect(val);\n\t\t}\n\t\telse if (prop === 'order') {\n\t\t\tsetOrder(val);\n\t\t}\n\t\telse if (prop === 'render') {\n\t\t\tsetProp('render', val);\n\t\t}\n\t\telse if (prop === 'search') {\n\t\t\tsetProp('search', val);\n\t\t}\n\t\telse if (! prop) {\n\t\t\tif (val.className) {\n\t\t\t\tsetProp('className', val.className);\n\t\t\t}\n\t\n\t\t\tif (val.detect !== undefined) {\n\t\t\t\tsetDetect(val.detect);\n\t\t\t}\n\t\n\t\t\tif (val.order) {\n\t\t\t\tsetOrder(val.order);\n\t\t\t}\n\t\n\t\t\tif (val.render !== undefined) {\n\t\t\t\tsetProp('render', val.render);\n\t\t\t}\n\t\n\t\t\tif (val.search !== undefined) {\n\t\t\t\tsetProp('search', val.search);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Get a list of types\n\tDataTable.types = function () {\n\t\treturn _extTypes.detect.map(function (fn) {\n\t\t\treturn fn._name;\n\t\t});\n\t};\n\t\n\tvar __diacriticSort = function (a, b) {\n\t\ta = a !== null && a !== undefined ? a.toString().toLowerCase() : '';\n\t\tb = b !== null && b !== undefined ? b.toString().toLowerCase() : '';\n\t\n\t\t// Checked for `navigator.languages` support in `oneOf` so this code can't execute in old\n\t\t// Safari and thus can disable this check\n\t\t// eslint-disable-next-line compat/compat\n\t\treturn a.localeCompare(b, navigator.languages[0] || navigator.language, {\n\t\t\tnumeric: true,\n\t\t\tignorePunctuation: true,\n\t\t});\n\t}\n\t\n\tvar __diacriticHtmlSort = function (a, b) {\n\t\ta = _stripHtml(a);\n\t\tb = _stripHtml(b);\n\t\n\t\treturn __diacriticSort(a, b);\n\t}\n\t\n\t//\n\t// Built in data types\n\t//\n\t\n\tDataTable.type('string', {\n\t\tdetect: function () {\n\t\t\treturn 'string';\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( a ) {\n\t\t\t\t// This is a little complex, but faster than always calling toString,\n\t\t\t\t// http://jsperf.com/tostring-v-check\n\t\t\t\treturn _empty(a) && typeof a !== 'boolean' ?\n\t\t\t\t\t'' :\n\t\t\t\t\ttypeof a === 'string' ?\n\t\t\t\t\t\ta.toLowerCase() :\n\t\t\t\t\t\t! a.toString ?\n\t\t\t\t\t\t\t'' :\n\t\t\t\t\t\t\ta.toString();\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(false, true)\n\t});\n\t\n\tDataTable.type('string-utf8', {\n\t\tdetect: {\n\t\t\tallOf: function ( d ) {\n\t\t\t\treturn true;\n\t\t\t},\n\t\t\toneOf: function ( d ) {\n\t\t\t\t// At least one data point must contain a non-ASCII character\n\t\t\t\t// This line will also check if navigator.languages is supported or not. If not (Safari 10.0-)\n\t\t\t\t// this data type won't be supported.\n\t\t\t\t// eslint-disable-next-line compat/compat\n\t\t\t\treturn ! _empty( d ) && navigator.languages && typeof d === 'string' && d.match(/[^\\x00-\\x7F]/);\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tasc: __diacriticSort,\n\t\t\tdesc: function (a, b) {\n\t\t\t\treturn __diacriticSort(a, b) * -1;\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(false, true)\n\t});\n\t\n\t\n\tDataTable.type('html', {\n\t\tdetect: {\n\t\t\tallOf: function ( d ) {\n\t\t\t\treturn _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1);\n\t\t\t},\n\t\t\toneOf: function ( d ) {\n\t\t\t\t// At least one data point must contain a `<`\n\t\t\t\treturn ! _empty( d ) && typeof d === 'string' && d.indexOf('<') !== -1;\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( a ) {\n\t\t\t\treturn _empty(a) ?\n\t\t\t\t\t'' :\n\t\t\t\t\ta.replace ?\n\t\t\t\t\t\t_stripHtml(a).trim().toLowerCase() :\n\t\t\t\t\t\ta+'';\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(true, true)\n\t});\n\t\n\t\n\tDataTable.type('html-utf8', {\n\t\tdetect: {\n\t\t\tallOf: function ( d ) {\n\t\t\t\treturn _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1);\n\t\t\t},\n\t\t\toneOf: function ( d ) {\n\t\t\t\t// At least one data point must contain a `<` and a non-ASCII character\n\t\t\t\t// eslint-disable-next-line compat/compat\n\t\t\t\treturn navigator.languages &&\n\t\t\t\t\t! _empty( d ) &&\n\t\t\t\t\ttypeof d === 'string' &&\n\t\t\t\t\td.indexOf('<') !== -1 &&\n\t\t\t\t\ttypeof d === 'string' && d.match(/[^\\x00-\\x7F]/);\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tasc: __diacriticHtmlSort,\n\t\t\tdesc: function (a, b) {\n\t\t\t\treturn __diacriticHtmlSort(a, b) * -1;\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(true, true)\n\t});\n\t\n\t\n\tDataTable.type('date', {\n\t\tclassName: 'dt-type-date',\n\t\tdetect: {\n\t\t\tallOf: function ( d ) {\n\t\t\t\t// V8 tries _very_ hard to make a string passed into `Date.parse()`\n\t\t\t\t// valid, so we need to use a regex to restrict date formats. Use a\n\t\t\t\t// plug-in for anything other than ISO8601 style strings\n\t\t\t\tif ( d && !(d instanceof Date) && ! _re_date.test(d) ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tvar parsed = Date.parse(d);\n\t\t\t\treturn (parsed !== null && !isNaN(parsed)) || _empty(d);\n\t\t\t},\n\t\t\toneOf: function ( d ) {\n\t\t\t\t// At least one entry must be a date or a string with a date\n\t\t\t\treturn (d instanceof Date) || (typeof d === 'string' && _re_date.test(d));\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( d ) {\n\t\t\t\tvar ts = Date.parse( d );\n\t\t\t\treturn isNaN(ts) ? -Infinity : ts;\n\t\t\t}\n\t\t}\n\t});\n\t\n\t\n\tDataTable.type('html-num-fmt', {\n\t\tclassName: 'dt-type-numeric',\n\t\tdetect: {\n\t\t\tallOf: function ( d, settings ) {\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _htmlNumeric( d, decimal, true, false );\n\t\t\t},\n\t\t\toneOf: function (d, settings) {\n\t\t\t\t// At least one data point must contain a numeric value\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _htmlNumeric( d, decimal, true, false );\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( d, s ) {\n\t\t\t\tvar dp = s.oLanguage.sDecimal;\n\t\t\t\treturn __numericReplace( d, dp, _re_html, _re_formatted_numeric );\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(true, true)\n\t});\n\t\n\t\n\tDataTable.type('html-num', {\n\t\tclassName: 'dt-type-numeric',\n\t\tdetect: {\n\t\t\tallOf: function ( d, settings ) {\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _htmlNumeric( d, decimal, false, true );\n\t\t\t},\n\t\t\toneOf: function (d, settings) {\n\t\t\t\t// At least one data point must contain a numeric value\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _htmlNumeric( d, decimal, false, false );\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( d, s ) {\n\t\t\t\tvar dp = s.oLanguage.sDecimal;\n\t\t\t\treturn __numericReplace( d, dp, _re_html );\n\t\t\t}\n\t\t},\n\t\tsearch: _filterString(true, true)\n\t});\n\t\n\t\n\tDataTable.type('num-fmt', {\n\t\tclassName: 'dt-type-numeric',\n\t\tdetect: {\n\t\t\tallOf: function ( d, settings ) {\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _isNumber( d, decimal, true, true );\n\t\t\t},\n\t\t\toneOf: function (d, settings) {\n\t\t\t\t// At least one data point must contain a numeric value\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _isNumber( d, decimal, true, false );\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function ( d, s ) {\n\t\t\t\tvar dp = s.oLanguage.sDecimal;\n\t\t\t\treturn __numericReplace( d, dp, _re_formatted_numeric );\n\t\t\t}\n\t\t}\n\t});\n\t\n\t\n\tDataTable.type('num', {\n\t\tclassName: 'dt-type-numeric',\n\t\tdetect: {\n\t\t\tallOf: function ( d, settings ) {\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _isNumber( d, decimal, false, true );\n\t\t\t},\n\t\t\toneOf: function (d, settings) {\n\t\t\t\t// At least one data point must contain a numeric value\n\t\t\t\tvar decimal = settings.oLanguage.sDecimal;\n\t\t\t\treturn _isNumber( d, decimal, false, false );\n\t\t\t}\n\t\t},\n\t\torder: {\n\t\t\tpre: function (d, s) {\n\t\t\t\tvar dp = s.oLanguage.sDecimal;\n\t\t\t\treturn __numericReplace( d, dp );\n\t\t\t}\n\t\t}\n\t});\n\t\n\t\n\t\n\t\n\tvar __numericReplace = function ( d, decimalPlace, re1, re2 ) {\n\t\tif ( d !== 0 && (!d || d === '-') ) {\n\t\t\treturn -Infinity;\n\t\t}\n\t\t\n\t\tvar type = typeof d;\n\t\n\t\tif (type === 'number' || type === 'bigint') {\n\t\t\treturn d;\n\t\t}\n\t\n\t\t// If a decimal place other than `.` is used, it needs to be given to the\n\t\t// function so we can detect it and replace with a `.` which is the only\n\t\t// decimal place JavaScript recognises - it is not locale aware.\n\t\tif ( decimalPlace ) {\n\t\t\td = _numToDecimal( d, decimalPlace );\n\t\t}\n\t\n\t\tif ( d.replace ) {\n\t\t\tif ( re1 ) {\n\t\t\t\td = d.replace( re1, '' );\n\t\t\t}\n\t\n\t\t\tif ( re2 ) {\n\t\t\t\td = d.replace( re2, '' );\n\t\t\t}\n\t\t}\n\t\n\t\treturn d * 1;\n\t};\n\t\n\t\n\t$.extend( true, DataTable.ext.renderer, {\n\t\tfooter: {\n\t\t\t_: function ( settings, cell, classes ) {\n\t\t\t\tcell.addClass(classes.tfoot.cell);\n\t\t\t}\n\t\t},\n\t\n\t\theader: {\n\t\t\t_: function ( settings, cell, classes ) {\n\t\t\t\tcell.addClass(classes.thead.cell);\n\t\n\t\t\t\tif (! settings.oFeatures.bSort) {\n\t\t\t\t\tcell.addClass(classes.order.none);\n\t\t\t\t}\n\t\n\t\t\t\tvar titleRow = settings.titleRow;\n\t\t\t\tvar headerRows = cell.closest('thead').find('tr');\n\t\t\t\tvar rowIdx = cell.parent().index();\n\t\n\t\t\t\t// Conditions to not apply the ordering icons\n\t\t\t\tif (\n\t\t\t\t\t// Cells and rows which have the attribute to disable the icons\n\t\t\t\t\tcell.attr('data-dt-order') === 'disable' ||\n\t\t\t\t\tcell.parent().attr('data-dt-order') === 'disable' ||\n\t\n\t\t\t\t\t// titleRow support, for defining a specific row in the header\n\t\t\t\t\t(titleRow === true && rowIdx !== 0) ||\n\t\t\t\t\t(titleRow === false && rowIdx !== headerRows.length - 1) ||\n\t\t\t\t\t(typeof titleRow === 'number' && rowIdx !== titleRow)\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\n\t\t\t\t// No additional mark-up required\n\t\t\t\t// Attach a sort listener to update on sort - note that using the\n\t\t\t\t// `DT` namespace will allow the event to be removed automatically\n\t\t\t\t// on destroy, while the `dt` namespaced event is the one we are\n\t\t\t\t// listening for\n\t\t\t\t$(settings.nTable).on( 'order.dt.DT column-visibility.dt.DT', function ( e, ctx, column ) {\n\t\t\t\t\tif ( settings !== ctx ) { // need to check if this is the host\n\t\t\t\t\t\treturn;               // table, not a nested one\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar sorting = ctx.sortDetails;\n\t\n\t\t\t\t\tif (! sorting) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar orderedColumns = _pluck(sorting, 'col');\n\t\n\t\t\t\t\t// This handler is only needed on column visibility if the column is part of the\n\t\t\t\t\t// ordering. If it isn't, then we can bail out to save performance. It could be a\n\t\t\t\t\t// separate event handler, but this is a balance between code reuse / size and performance\n\t\t\t\t\t// console.log(e, e.name, column, orderedColumns, orderedColumns.includes(column))\n\t\t\t\t\tif (e.type === 'column-visibility' && ! orderedColumns.includes(column)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar i;\n\t\t\t\t\tvar orderClasses = classes.order;\n\t\t\t\t\tvar columns = ctx.api.columns( cell );\n\t\t\t\t\tvar col = settings.aoColumns[columns.flatten()[0]];\n\t\t\t\t\tvar orderable = columns.orderable().includes(true);\n\t\t\t\t\tvar ariaType = '';\n\t\t\t\t\tvar indexes = columns.indexes();\n\t\t\t\t\tvar sortDirs = columns.orderable(true).flatten();\n\t\t\t\t\tvar tabIndex = settings.iTabIndex;\n\t\t\t\t\tvar canOrder = ctx.orderHandler && orderable;\n\t\n\t\t\t\t\tcell\n\t\t\t\t\t\t.removeClass(\n\t\t\t\t\t\t\torderClasses.isAsc +' '+\n\t\t\t\t\t\t\torderClasses.isDesc\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.toggleClass( orderClasses.none, ! orderable )\n\t\t\t\t\t\t.toggleClass( orderClasses.canAsc, canOrder && sortDirs.includes('asc') )\n\t\t\t\t\t\t.toggleClass( orderClasses.canDesc, canOrder && sortDirs.includes('desc') );\n\t\n\t\t\t\t\t// Determine if all of the columns that this cell covers are included in the\n\t\t\t\t\t// current ordering\n\t\t\t\t\tvar isOrdering = true;\n\t\t\t\t\t\n\t\t\t\t\tfor (i=0; i<indexes.length; i++) {\n\t\t\t\t\t\tif (! orderedColumns.includes(indexes[i])) {\n\t\t\t\t\t\t\tisOrdering = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif ( isOrdering ) {\n\t\t\t\t\t\t// Get the ordering direction for the columns under this cell\n\t\t\t\t\t\t// Note that it is possible for a cell to be asc and desc sorting\n\t\t\t\t\t\t// (column spanning cells)\n\t\t\t\t\t\tvar orderDirs = columns.order();\n\t\n\t\t\t\t\t\tcell.addClass(\n\t\t\t\t\t\t\torderDirs.includes('asc') ? orderClasses.isAsc : '' +\n\t\t\t\t\t\t\torderDirs.includes('desc') ? orderClasses.isDesc : ''\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Find the first visible column that has ordering applied to it - it get's\n\t\t\t\t\t// the aria information, as the ARIA spec says that only one column should\n\t\t\t\t\t// be marked with aria-sort\n\t\t\t\t\tvar firstVis = -1; // column index\n\t\n\t\t\t\t\tfor (i=0; i<orderedColumns.length; i++) {\n\t\t\t\t\t\tif (settings.aoColumns[orderedColumns[i]].bVisible) {\n\t\t\t\t\t\t\tfirstVis = orderedColumns[i];\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif (indexes[0] == firstVis) {\n\t\t\t\t\t\tvar firstSort = sorting[0];\n\t\t\t\t\t\tvar sortOrder = col.asSorting;\n\t\n\t\t\t\t\t\tcell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending');\n\t\n\t\t\t\t\t\t// Determine if the next click will remove sorting or change the sort\n\t\t\t\t\t\tariaType = ! sortOrder[firstSort.index + 1] ? 'Remove' : 'Reverse';\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tcell.removeAttr('aria-sort');\n\t\t\t\t\t}\n\t\n\t\t\t\t\t// Make the headers tab-able for keyboard navigation\n\t\t\t\t\tif (orderable) {\n\t\t\t\t\t\tvar orderSpan = cell.find('.dt-column-order');\n\t\t\t\t\t\t\n\t\t\t\t\t\torderSpan\n\t\t\t\t\t\t\t.attr('role', 'button')\n\t\t\t\t\t\t\t.attr('aria-label', orderable\n\t\t\t\t\t\t\t\t? col.ariaTitle + ctx.api.i18n('oAria.orderable' + ariaType)\n\t\t\t\t\t\t\t\t: col.ariaTitle\n\t\t\t\t\t\t\t);\n\t\n\t\t\t\t\t\tif (tabIndex !== -1) {\n\t\t\t\t\t\t\torderSpan.attr('tabindex', tabIndex);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t},\n\t\n\t\tlayout: {\n\t\t\t_: function ( settings, container, items ) {\n\t\t\t\tvar classes = settings.oClasses.layout;\n\t\t\t\tvar row = $('<div/>')\n\t\t\t\t\t.attr('id', items.id || null)\n\t\t\t\t\t.addClass(items.className || classes.row)\n\t\t\t\t\t.appendTo( container );\n\t\n\t\t\t\tDataTable.ext.renderer.layout._forLayoutRow(items, function (key, val) {\n\t\t\t\t\tif (key === 'id' || key === 'className') {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\n\t\t\t\t\tvar klass = '';\n\t\n\t\t\t\t\tif (val.table) {\n\t\t\t\t\t\trow.addClass(classes.tableRow);\n\t\t\t\t\t\tklass += classes.tableCell + ' ';\n\t\t\t\t\t}\n\t\n\t\t\t\t\tif (key === 'start') {\n\t\t\t\t\t\tklass += classes.start;\n\t\t\t\t\t}\n\t\t\t\t\telse if (key === 'end') {\n\t\t\t\t\t\tklass += classes.end;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tklass += classes.full;\n\t\t\t\t\t}\n\t\n\t\t\t\t\t$('<div/>')\n\t\t\t\t\t\t.attr({\n\t\t\t\t\t\t\tid: val.id || null,\n\t\t\t\t\t\t\t\"class\": val.className\n\t\t\t\t\t\t\t\t? val.className\n\t\t\t\t\t\t\t\t: classes.cell + ' ' + klass\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.append( val.contents )\n\t\t\t\t\t\t.appendTo( row );\n\t\t\t\t});\n\t\t\t},\n\t\n\t\t\t// Shared for use by the styling frameworks\n\t\t\t_forLayoutRow: function (items, fn) {\n\t\t\t\t// As we are inserting dom elements, we need start / end in a\n\t\t\t\t// specific order, this function is used for sorting the layout\n\t\t\t\t// keys.\n\t\t\t\tvar layoutEnum = function (x) {\n\t\t\t\t\tswitch (x) {\n\t\t\t\t\t\tcase '': return 0;\n\t\t\t\t\t\tcase 'start': return 1;\n\t\t\t\t\t\tcase 'end': return 2;\n\t\t\t\t\t\tdefault: return 3;\n\t\t\t\t\t}\n\t\t\t\t};\n\t\n\t\t\t\tObject\n\t\t\t\t\t.keys(items)\n\t\t\t\t\t.sort(function (a, b) {\n\t\t\t\t\t\treturn layoutEnum(a) - layoutEnum(b);\n\t\t\t\t\t})\n\t\t\t\t\t.forEach(function (key) {\n\t\t\t\t\t\tfn(key, items[key]);\n\t\t\t\t\t});\n\t\t\t}\n\t\t}\n\t} );\n\t\n\t\n\tDataTable.feature = {};\n\t\n\t// Third parameter is internal only!\n\tDataTable.feature.register = function ( name, cb, legacy ) {\n\t\tDataTable.ext.features[ name ] = cb;\n\t\n\t\tif (legacy) {\n\t\t\t_ext.feature.push({\n\t\t\t\tcFeature: legacy,\n\t\t\t\tfnInit: cb\n\t\t\t});\n\t\t}\n\t};\n\t\n\tfunction _divProp(el, prop, val) {\n\t\tif (val) {\n\t\t\tel[prop] = val;\n\t\t}\n\t}\n\t\n\tDataTable.feature.register( 'div', function ( settings, opts ) {\n\t\tvar n = $('<div>')[0];\n\t\n\t\tif (opts) {\n\t\t\t_divProp(n, 'className', opts.className);\n\t\t\t_divProp(n, 'id', opts.id);\n\t\t\t_divProp(n, 'innerHTML', opts.html);\n\t\t\t_divProp(n, 'textContent', opts.text);\n\t\t}\n\t\n\t\treturn n;\n\t} );\n\t\n\tDataTable.feature.register( 'info', function ( settings, opts ) {\n\t\t// For compatibility with the legacy `info` top level option\n\t\tif (! settings.oFeatures.bInfo) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\tvar\n\t\t\tlang  = settings.oLanguage,\n\t\t\ttid = settings.sTableId,\n\t\t\tn = $('<div/>', {\n\t\t\t\t'class': settings.oClasses.info.container,\n\t\t\t} );\n\t\n\t\topts = $.extend({\n\t\t\tcallback: lang.fnInfoCallback,\n\t\t\tempty: lang.sInfoEmpty,\n\t\t\tpostfix: lang.sInfoPostFix,\n\t\t\tsearch: lang.sInfoFiltered,\n\t\t\ttext: lang.sInfo,\n\t\t}, opts);\n\t\n\t\n\t\t// Update display on each draw\n\t\tsettings.aoDrawCallback.push(function (s) {\n\t\t\t_fnUpdateInfo(s, opts, n);\n\t\t});\n\t\n\t\t// For the first info display in the table, we add a callback and aria information.\n\t\tif (! settings._infoEl) {\n\t\t\tn.attr({\n\t\t\t\t'aria-live': 'polite',\n\t\t\t\tid: tid+'_info',\n\t\t\t\trole: 'status'\n\t\t\t});\n\t\n\t\t\t// Table is described by our info div\n\t\t\t$(settings.nTable).attr( 'aria-describedby', tid+'_info' );\n\t\n\t\t\tsettings._infoEl = n;\n\t\t}\n\t\n\t\treturn n;\n\t}, 'i' );\n\t\n\t/**\n\t * Update the information elements in the display\n\t *  @param {object} settings dataTables settings object\n\t *  @memberof DataTable#oApi\n\t */\n\tfunction _fnUpdateInfo ( settings, opts, node )\n\t{\n\t\tvar\n\t\t\tstart = settings._iDisplayStart+1,\n\t\t\tend   = settings.fnDisplayEnd(),\n\t\t\tmax   = settings.fnRecordsTotal(),\n\t\t\ttotal = settings.fnRecordsDisplay(),\n\t\t\tout   = total\n\t\t\t\t? opts.text\n\t\t\t\t: opts.empty;\n\t\n\t\tif ( total !== max ) {\n\t\t\t// Record set after filtering\n\t\t\tout += ' ' + opts.search;\n\t\t}\n\t\n\t\t// Convert the macros\n\t\tout += opts.postfix;\n\t\tout = _fnMacros( settings, out );\n\t\n\t\tif ( opts.callback ) {\n\t\t\tout = opts.callback.call( settings.oInstance,\n\t\t\t\tsettings, start, end, max, total, out\n\t\t\t);\n\t\t}\n\t\n\t\tnode.html( out );\n\t\n\t\t_fnCallbackFire(settings, null, 'info', [settings, node[0], out]);\n\t}\n\t\n\tvar __searchCounter = 0;\n\t\n\t// opts\n\t// - text\n\t// - placeholder\n\tDataTable.feature.register( 'search', function ( settings, opts ) {\n\t\t// Don't show the input if filtering isn't available on the table\n\t\tif (! settings.oFeatures.bFilter) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\tvar classes = settings.oClasses.search;\n\t\tvar tableId = settings.sTableId;\n\t\tvar language = settings.oLanguage;\n\t\tvar previousSearch = settings.oPreviousSearch;\n\t\tvar input = '<input type=\"search\" class=\"'+classes.input+'\"/>';\n\t\n\t\topts = $.extend({\n\t\t\tplaceholder: language.sSearchPlaceholder,\n\t\t\tprocessing: false,\n\t\t\ttext: language.sSearch\n\t\t}, opts);\n\t\n\t\t// The _INPUT_ is optional - is appended if not present\n\t\tif (opts.text.indexOf('_INPUT_') === -1) {\n\t\t\topts.text += '_INPUT_';\n\t\t}\n\t\n\t\topts.text = _fnMacros(settings, opts.text);\n\t\n\t\t// We can put the <input> outside of the label if it is at the start or end\n\t\t// which helps improve accessability (not all screen readers like implicit\n\t\t// for elements).\n\t\tvar end = opts.text.match(/_INPUT_$/);\n\t\tvar start = opts.text.match(/^_INPUT_/);\n\t\tvar removed = opts.text.replace(/_INPUT_/, '');\n\t\tvar str = '<label>' + opts.text + '</label>';\n\t\n\t\tif (start) {\n\t\t\tstr = '_INPUT_<label>' + removed + '</label>';\n\t\t}\n\t\telse if (end) {\n\t\t\tstr = '<label>' + removed + '</label>_INPUT_';\n\t\t}\n\t\n\t\tvar filter = $('<div>')\n\t\t\t.addClass(classes.container)\n\t\t\t.append(str.replace(/_INPUT_/, input));\n\t\n\t\t// add for and id to label and input\n\t\tfilter.find('label').attr('for', 'dt-search-' + __searchCounter);\n\t\tfilter.find('input').attr('id', 'dt-search-' + __searchCounter);\n\t\t__searchCounter++;\n\t\n\t\tvar searchFn = function(event) {\n\t\t\tvar val = this.value;\n\t\n\t\t\tif(previousSearch.return && event.key !== \"Enter\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\n\t\t\t/* Now do the filter */\n\t\t\tif ( val != previousSearch.search ) {\n\t\t\t\t_fnProcessingRun(settings, opts.processing, function () {\n\t\t\t\t\tpreviousSearch.search = val;\n\t\t\t\n\t\t\t\t\t_fnFilterComplete( settings, previousSearch );\n\t\t\t\n\t\t\t\t\t// Need to redraw, without resorting\n\t\t\t\t\tsettings._iDisplayStart = 0;\n\t\t\t\t\t_fnDraw( settings );\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\n\t\tvar searchDelay = settings.searchDelay !== null ?\n\t\t\tsettings.searchDelay :\n\t\t\t0;\n\t\n\t\tvar jqFilter = $('input', filter)\n\t\t\t.val( previousSearch.search )\n\t\t\t.attr( 'placeholder', opts.placeholder )\n\t\t\t.on(\n\t\t\t\t'keyup.DT search.DT input.DT paste.DT cut.DT',\n\t\t\t\tsearchDelay ?\n\t\t\t\t\tDataTable.util.debounce( searchFn, searchDelay ) :\n\t\t\t\t\tsearchFn\n\t\t\t)\n\t\t\t.on( 'mouseup.DT', function(e) {\n\t\t\t\t// Edge fix! Edge 17 does not trigger anything other than mouse events when clicking\n\t\t\t\t// on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn`\n\t\t\t\t// checks the value to see if it has changed. In other browsers it won't have.\n\t\t\t\tsetTimeout( function () {\n\t\t\t\t\tsearchFn.call(jqFilter[0], e);\n\t\t\t\t}, 10);\n\t\t\t} )\n\t\t\t.on( 'keypress.DT', function(e) {\n\t\t\t\t/* Prevent form submission */\n\t\t\t\tif ( e.keyCode == 13 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.attr('aria-controls', tableId);\n\t\n\t\t// Update the input elements whenever the table is filtered\n\t\t$(settings.nTable).on( 'search.dt.DT', function ( ev, s ) {\n\t\t\tif ( settings === s && jqFilter[0] !== document.activeElement ) {\n\t\t\t\tjqFilter.val( typeof previousSearch.search !== 'function'\n\t\t\t\t\t? previousSearch.search\n\t\t\t\t\t: ''\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\t\n\t\treturn filter;\n\t}, 'f' );\n\t\n\t// opts\n\t// - type - button configuration\n\t// - buttons - number of buttons to show - must be odd\n\tDataTable.feature.register( 'paging', function ( settings, opts ) {\n\t\t// Don't show the paging input if the table doesn't have paging enabled\n\t\tif (! settings.oFeatures.bPaginate) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\topts = $.extend({\n\t\t\tbuttons: DataTable.ext.pager.numbers_length,\n\t\t\ttype: settings.sPaginationType,\n\t\t\tboundaryNumbers: true,\n\t\t\tfirstLast: true,\n\t\t\tpreviousNext: true,\n\t\t\tnumbers: true\n\t\t}, opts);\n\t\n\t\tvar host = $('<div/>')\n\t\t\t.addClass(settings.oClasses.paging.container + (opts.type ? ' paging_' + opts.type : ''))\n\t\t\t.append(\n\t\t\t\t$('<nav>')\n\t\t\t\t\t.attr('aria-label', 'pagination')\n\t\t\t\t\t.addClass(settings.oClasses.paging.nav)\n\t\t\t);\n\t\tvar draw = function () {\n\t\t\t_pagingDraw(settings, host.children(), opts);\n\t\t};\n\t\n\t\tsettings.aoDrawCallback.push(draw);\n\t\n\t\t// Responsive redraw of paging control\n\t\t$(settings.nTable).on('column-sizing.dt.DT', draw);\n\t\n\t\treturn host;\n\t}, 'p' );\n\t\n\t/**\n\t * Dynamically create the button type array based on the configuration options.\n\t * This will only happen if the paging type is not defined.\n\t */\n\tfunction _pagingDynamic(opts) {\n\t\tvar out = [];\n\t\n\t\tif (opts.numbers) {\n\t\t\tout.push('numbers');\n\t\t}\n\t\n\t\tif (opts.previousNext) {\n\t\t\tout.unshift('previous');\n\t\t\tout.push('next');\n\t\t}\n\t\n\t\tif (opts.firstLast) {\n\t\t\tout.unshift('first');\n\t\t\tout.push('last');\n\t\t}\n\t\n\t\treturn out;\n\t}\n\t\n\tfunction _pagingDraw(settings, host, opts) {\n\t\tif (! settings._bInitComplete) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tvar\n\t\t\tplugin = opts.type\n\t\t\t\t? DataTable.ext.pager[ opts.type ]\n\t\t\t\t: _pagingDynamic,\n\t\t\taria = settings.oLanguage.oAria.paginate || {},\n\t\t\tstart      = settings._iDisplayStart,\n\t\t\tlen        = settings._iDisplayLength,\n\t\t\tvisRecords = settings.fnRecordsDisplay(),\n\t\t\tall        = len === -1,\n\t\t\tpage = all ? 0 : Math.ceil( start / len ),\n\t\t\tpages = all ? 1 : Math.ceil( visRecords / len ),\n\t\t\tbuttons = [],\n\t\t\tbuttonEls = [],\n\t\t\tbuttonsNested = plugin(opts)\n\t\t\t\t.map(function (val) {\n\t\t\t\t\treturn val === 'numbers'\n\t\t\t\t\t\t? _pagingNumbers(page, pages, opts.buttons, opts.boundaryNumbers)\n\t\t\t\t\t\t: val;\n\t\t\t\t});\n\t\n\t\t// .flat() would be better, but not supported in old Safari\n\t\tbuttons = buttons.concat.apply(buttons, buttonsNested);\n\t\n\t\tfor (var i=0 ; i<buttons.length ; i++) {\n\t\t\tvar button = buttons[i];\n\t\n\t\t\tvar btnInfo = _pagingButtonInfo(settings, button, page, pages);\n\t\t\tvar btn = _fnRenderer( settings, 'pagingButton' )(\n\t\t\t\tsettings,\n\t\t\t\tbutton,\n\t\t\t\tbtnInfo.display,\n\t\t\t\tbtnInfo.active,\n\t\t\t\tbtnInfo.disabled\n\t\t\t);\n\t\n\t\t\tvar ariaLabel = typeof button === 'string'\n\t\t\t\t? aria[ button ]\n\t\t\t\t: aria.number\n\t\t\t\t\t? aria.number + (button+1)\n\t\t\t\t\t: null;\n\t\n\t\t\t// Common attributes\n\t\t\t$(btn.clicker).attr({\n\t\t\t\t'aria-controls': settings.sTableId,\n\t\t\t\t'aria-disabled': btnInfo.disabled ? 'true' : null,\n\t\t\t\t'aria-current': btnInfo.active ? 'page' : null,\n\t\t\t\t'aria-label': ariaLabel,\n\t\t\t\t'data-dt-idx': button,\n\t\t\t\t'tabIndex': btnInfo.disabled\n\t\t\t\t\t? -1\n\t\t\t\t\t: settings.iTabIndex && btn.clicker[0].nodeName.toLowerCase() !== 'span'\n\t\t\t\t\t\t? settings.iTabIndex\n\t\t\t\t\t\t: null, // `0` doesn't need a tabIndex since it is the default\n\t\t\t});\n\t\n\t\t\tif (typeof button !== 'number') {\n\t\t\t\t$(btn.clicker).addClass(button);\n\t\t\t}\n\t\n\t\t\t_fnBindAction(\n\t\t\t\tbtn.clicker, {action: button}, function(e) {\n\t\t\t\t\te.preventDefault();\n\t\n\t\t\t\t\t_fnPageChange( settings, e.data.action, true );\n\t\t\t\t}\n\t\t\t);\n\t\n\t\t\tbuttonEls.push(btn.display);\n\t\t}\n\t\n\t\tvar wrapped = _fnRenderer(settings, 'pagingContainer')(\n\t\t\tsettings, buttonEls\n\t\t);\n\t\n\t\tvar activeEl = host.find(document.activeElement).data('dt-idx');\n\t\n\t\thost.empty().append(wrapped);\n\t\n\t\tif ( activeEl !== undefined ) {\n\t\t\thost.find( '[data-dt-idx='+activeEl+']' ).trigger('focus');\n\t\t}\n\t\n\t\t// Responsive - check if the buttons are over two lines based on the\n\t\t// height of the buttons and the container.\n\t\tif (buttonEls.length) {\n\t\t\tvar outerHeight = $(buttonEls[0]).outerHeight();\n\t\t\n\t\t\tif (\n\t\t\t\topts.buttons > 1 && // prevent infinite\n\t\t\t\touterHeight > 0 && // will be 0 if hidden\n\t\t\t\t$(host).height() >= (outerHeight * 2) - 10\n\t\t\t) {\n\t\t\t\t_pagingDraw(settings, host, $.extend({}, opts, { buttons: opts.buttons - 2 }));\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * Get properties for a button based on the current paging state of the table\n\t *\n\t * @param {*} settings DT settings object\n\t * @param {*} button The button type in question\n\t * @param {*} page Table's current page\n\t * @param {*} pages Number of pages\n\t * @returns Info object\n\t */\n\tfunction _pagingButtonInfo(settings, button, page, pages) {\n\t\tvar lang = settings.oLanguage.oPaginate;\n\t\tvar o = {\n\t\t\tdisplay: '',\n\t\t\tactive: false,\n\t\t\tdisabled: false\n\t\t};\n\t\n\t\tswitch ( button ) {\n\t\t\tcase 'ellipsis':\n\t\t\t\to.display = '&#x2026;';\n\t\t\t\tbreak;\n\t\n\t\t\tcase 'first':\n\t\t\t\to.display = lang.sFirst;\n\t\n\t\t\t\tif (page === 0) {\n\t\t\t\t\to.disabled = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\n\t\t\tcase 'previous':\n\t\t\t\to.display = lang.sPrevious;\n\t\n\t\t\t\tif ( page === 0 ) {\n\t\t\t\t\to.disabled = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\n\t\t\tcase 'next':\n\t\t\t\to.display = lang.sNext;\n\t\n\t\t\t\tif ( pages === 0 || page === pages-1 ) {\n\t\t\t\t\to.disabled = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\n\t\t\tcase 'last':\n\t\t\t\to.display = lang.sLast;\n\t\n\t\t\t\tif ( pages === 0 || page === pages-1 ) {\n\t\t\t\t\to.disabled = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\n\t\t\tdefault:\n\t\t\t\tif ( typeof button === 'number' ) {\n\t\t\t\t\to.display = settings.fnFormatNumber( button + 1 );\n\t\t\t\t\t\n\t\t\t\t\tif (page === button) {\n\t\t\t\t\t\to.active = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t\n\t\treturn o;\n\t}\n\t\n\t/**\n\t * Compute what number buttons to show in the paging control\n\t *\n\t * @param {*} page Current page\n\t * @param {*} pages Total number of pages\n\t * @param {*} buttons Target number of number buttons\n\t * @param {boolean} addFirstLast Indicate if page 1 and end should be included\n\t * @returns Buttons to show\n\t */\n\tfunction _pagingNumbers ( page, pages, buttons, addFirstLast ) {\n\t\tvar\n\t\t\tnumbers = [],\n\t\t\thalf = Math.floor(buttons / 2),\n\t\t\tbefore = addFirstLast ? 2 : 1,\n\t\t\tafter = addFirstLast ? 1 : 0;\n\t\n\t\tif ( pages <= buttons ) {\n\t\t\tnumbers = _range(0, pages);\n\t\t}\n\t\telse if (buttons === 1) {\n\t\t\t// Single button - current page only\n\t\t\tnumbers = [page];\n\t\t}\n\t\telse if (buttons === 3) {\n\t\t\t// Special logic for just three buttons\n\t\t\tif (page <= 1) {\n\t\t\t\tnumbers = [0, 1, 'ellipsis'];\n\t\t\t}\n\t\t\telse if (page >= pages - 2) {\n\t\t\t\tnumbers = _range(pages-2, pages);\n\t\t\t\tnumbers.unshift('ellipsis');\n\t\t\t}\n\t\t\telse {\n\t\t\t\tnumbers = ['ellipsis', page, 'ellipsis'];\n\t\t\t}\n\t\t}\n\t\telse if ( page <= half ) {\n\t\t\tnumbers = _range(0, buttons-before);\n\t\t\tnumbers.push('ellipsis');\n\t\n\t\t\tif (addFirstLast) {\n\t\t\t\tnumbers.push(pages-1);\n\t\t\t}\n\t\t}\n\t\telse if ( page >= pages - 1 - half ) {\n\t\t\tnumbers = _range(pages-(buttons-before), pages);\n\t\t\tnumbers.unshift('ellipsis');\n\t\n\t\t\tif (addFirstLast) {\n\t\t\t\tnumbers.unshift(0);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tnumbers = _range(page-half+before, page+half-after);\n\t\t\tnumbers.push('ellipsis');\n\t\t\tnumbers.unshift('ellipsis');\n\t\n\t\t\tif (addFirstLast) {\n\t\t\t\tnumbers.push(pages-1);\n\t\t\t\tnumbers.unshift(0);\n\t\t\t}\n\t\t}\n\t\n\t\treturn numbers;\n\t}\n\t\n\tvar __lengthCounter = 0;\n\t\n\t// opts\n\t// - menu\n\t// - text\n\tDataTable.feature.register( 'pageLength', function ( settings, opts ) {\n\t\tvar features = settings.oFeatures;\n\t\n\t\t// For compatibility with the legacy `pageLength` top level option\n\t\tif (! features.bPaginate || ! features.bLengthChange) {\n\t\t\treturn null;\n\t\t}\n\t\n\t\topts = $.extend({\n\t\t\tmenu: settings.aLengthMenu,\n\t\t\ttext: settings.oLanguage.sLengthMenu\n\t\t}, opts);\n\t\n\t\tvar\n\t\t\tclasses  = settings.oClasses.length,\n\t\t\ttableId  = settings.sTableId,\n\t\t\tmenu     = opts.menu,\n\t\t\tlengths  = [],\n\t\t\tlanguage = [],\n\t\t\ti;\n\t\n\t\t// Options can be given in a number of ways\n\t\tif (Array.isArray( menu[0] )) {\n\t\t\t// Old 1.x style - 2D array\n\t\t\tlengths = menu[0];\n\t\t\tlanguage = menu[1];\n\t\t}\n\t\telse {\n\t\t\tfor ( i=0 ; i<menu.length ; i++ ) {\n\t\t\t\t// An object with different label and value\n\t\t\t\tif ($.isPlainObject(menu[i])) {\n\t\t\t\t\tlengths.push(menu[i].value);\n\t\t\t\t\tlanguage.push(menu[i].label);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Or just a number to display and use\n\t\t\t\t\tlengths.push(menu[i]);\n\t\t\t\t\tlanguage.push(menu[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// We can put the <select> outside of the label if it is at the start or\n\t\t// end which helps improve accessability (not all screen readers like\n\t\t// implicit for elements).\n\t\tvar end = opts.text.match(/_MENU_$/);\n\t\tvar start = opts.text.match(/^_MENU_/);\n\t\tvar removed = opts.text.replace(/_MENU_/, '');\n\t\tvar str = '<label>' + opts.text + '</label>';\n\t\n\t\tif (start) {\n\t\t\tstr = '_MENU_<label>' + removed + '</label>';\n\t\t}\n\t\telse if (end) {\n\t\t\tstr = '<label>' + removed + '</label>_MENU_';\n\t\t}\n\t\n\t\t// Wrapper element - use a span as a holder for where the select will go\n\t\tvar tmpId = 'tmp-' + (+new Date())\n\t\tvar div = $('<div/>')\n\t\t\t.addClass( classes.container )\n\t\t\t.append(\n\t\t\t\tstr.replace( '_MENU_', '<span id=\"'+tmpId+'\"></span>' )\n\t\t\t);\n\t\n\t\t// Save text node content for macro updating\n\t\tvar textNodes = [];\n\t\tArray.prototype.slice.call(div.find('label')[0].childNodes).forEach(function (el) {\n\t\t\tif (el.nodeType === Node.TEXT_NODE) {\n\t\t\t\ttextNodes.push({\n\t\t\t\t\tel: el,\n\t\t\t\t\ttext: el.textContent\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\n\t\t// Update the label text in case it has an entries value\n\t\tvar updateEntries = function (len) {\n\t\t\ttextNodes.forEach(function (node) {\n\t\t\t\tnode.el.textContent = _fnMacros(settings, node.text, len);\n\t\t\t});\n\t\t}\n\t\n\t\t// Next, the select itself, along with the options\n\t\tvar select = $('<select/>', {\n\t\t\t'aria-controls': tableId,\n\t\t\t'class':         classes.select\n\t\t} );\n\t\n\t\tfor ( i=0 ; i<lengths.length ; i++ ) {\n\t\t\t// Attempt to look up the length from the i18n options\n\t\t\tvar label = settings.api.i18n('lengthLabels.' + lengths[i], null);\n\t\n\t\t\tif (label === null) {\n\t\t\t\t// If not present, fallback to old style\n\t\t\t\tlabel = typeof language[i] === 'number' ?\n\t\t\t\t\tsettings.fnFormatNumber( language[i] ) :\n\t\t\t\t\tlanguage[i];\n\t\t\t}\n\t\n\t\t\tselect[0][ i ] = new Option(label, lengths[i]);\n\t\t}\n\t\n\t\t// add for and id to label and input\n\t\tdiv.find('label').attr('for', 'dt-length-' + __lengthCounter);\n\t\tselect.attr('id', 'dt-length-' + __lengthCounter);\n\t\t__lengthCounter++;\n\t\n\t\t// Swap in the select list\n\t\tdiv.find('#' + tmpId).replaceWith(select);\n\t\n\t\t// Can't use `select` variable as user might provide their own and the\n\t\t// reference is broken by the use of outerHTML\n\t\t$('select', div)\n\t\t\t.val( settings._iDisplayLength )\n\t\t\t.on( 'change.DT', function() {\n\t\t\t\t_fnLengthChange( settings, $(this).val() );\n\t\t\t\t_fnDraw( settings );\n\t\t\t} );\n\t\n\t\t// Update node value whenever anything changes the table's length\n\t\t$(settings.nTable).on( 'length.dt.DT', function (e, s, len) {\n\t\t\tif ( settings === s ) {\n\t\t\t\t$('select', div).val( len );\n\t\n\t\t\t\t// Resolve plurals in the text for the new length\n\t\t\t\tupdateEntries(len);\n\t\t\t}\n\t\t} );\n\t\n\t\tupdateEntries(settings._iDisplayLength);\n\t\n\t\treturn div;\n\t}, 'l' );\n\t\n\t// jQuery access\n\t$.fn.dataTable = DataTable;\n\t\n\t// Provide access to the host jQuery object (circular reference)\n\tDataTable.$ = $;\n\t\n\t// Legacy aliases\n\t$.fn.dataTableSettings = DataTable.settings;\n\t$.fn.dataTableExt = DataTable.ext;\n\t\n\t// With a capital `D` we return a DataTables API instance rather than a\n\t// jQuery object\n\t$.fn.DataTable = function ( opts ) {\n\t\treturn $(this).dataTable( opts ).api();\n\t};\n\t\n\t// All properties that are available to $.fn.dataTable should also be\n\t// available on $.fn.DataTable\n\t$.each( DataTable, function ( prop, val ) {\n\t\t$.fn.DataTable[ prop ] = val;\n\t} );\n\n\treturn DataTable;\n}));\n\n\n/*! DataTables Bootstrap 5 integration\n * © SpryMedia Ltd - datatables.net/license\n */\n\n(function( factory ){\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD\n\t\tdefine( ['jquery', 'datatables.net'], function ( $ ) {\n\t\t\treturn factory( $, window, document );\n\t\t} );\n\t}\n\telse if ( typeof exports === 'object' ) {\n\t\t// CommonJS\n\t\tvar jq = require('jquery');\n\t\tvar cjsRequires = function (root, $) {\n\t\t\tif ( ! $.fn.dataTable ) {\n\t\t\t\trequire('datatables.net')(root, $);\n\t\t\t}\n\t\t};\n\n\t\tif (typeof window === 'undefined') {\n\t\t\tmodule.exports = function (root, $) {\n\t\t\t\tif ( ! root ) {\n\t\t\t\t\t// CommonJS environments without a window global must pass a\n\t\t\t\t\t// root. This will give an error otherwise\n\t\t\t\t\troot = window;\n\t\t\t\t}\n\n\t\t\t\tif ( ! $ ) {\n\t\t\t\t\t$ = jq( root );\n\t\t\t\t}\n\n\t\t\t\tcjsRequires( root, $ );\n\t\t\t\treturn factory( $, root, root.document );\n\t\t\t};\n\t\t}\n\t\telse {\n\t\t\tcjsRequires( window, jq );\n\t\t\tmodule.exports = factory( jq, window, window.document );\n\t\t}\n\t}\n\telse {\n\t\t// Browser\n\t\tfactory( jQuery, window, document );\n\t}\n}(function( $, window, document ) {\n'use strict';\nvar DataTable = $.fn.dataTable;\n\n\n\n/**\n * DataTables integration for Bootstrap 5.\n *\n * This file sets the defaults and adds options to DataTables to style its\n * controls using Bootstrap. See https://datatables.net/manual/styling/bootstrap\n * for further information.\n */\n\n/* Set the defaults for DataTables initialisation */\n$.extend( true, DataTable.defaults, {\n\trenderer: 'bootstrap'\n} );\n\n\n/* Default class modification */\n$.extend( true, DataTable.ext.classes, {\n\tcontainer: \"dt-container dt-bootstrap5\",\n\tsearch: {\n\t\tinput: \"form-control form-control-sm\"\n\t},\n\tlength: {\n\t\tselect: \"form-select form-select-sm\"\n\t},\n\tprocessing: {\n\t\tcontainer: \"dt-processing card\"\n\t},\n\tlayout: {\n\t\trow: 'row mt-2 justify-content-between',\n\t\tcell: 'd-md-flex justify-content-between align-items-center',\n\t\ttableCell: 'col-12',\n\t\tstart: 'dt-layout-start col-md-auto me-auto',\n\t\tend: 'dt-layout-end col-md-auto ms-auto',\n\t\tfull: 'dt-layout-full col-md'\n\t}\n} );\n\n\n/* Bootstrap paging button renderer */\nDataTable.ext.renderer.pagingButton.bootstrap = function (settings, buttonType, content, active, disabled) {\n\tvar btnClasses = ['dt-paging-button', 'page-item'];\n\n\tif (active) {\n\t\tbtnClasses.push('active');\n\t}\n\n\tif (disabled) {\n\t\tbtnClasses.push('disabled')\n\t}\n\n\tvar li = $('<li>').addClass(btnClasses.join(' '));\n\tvar a = $('<button>', {\n\t\t'class': 'page-link',\n\t\trole: 'link',\n\t\ttype: 'button'\n\t})\n\t\t.html(content)\n\t\t.appendTo(li);\n\n\treturn {\n\t\tdisplay: li,\n\t\tclicker: a\n\t};\n};\n\nDataTable.ext.renderer.pagingContainer.bootstrap = function (settings, buttonEls) {\n\treturn $('<ul/>').addClass('pagination').append(buttonEls);\n};\n\n\nreturn DataTable;\n}));\n\n\n"
  },
  {
    "path": "src/static/scripts/jdenticon-3.3.0.js",
    "content": "/**\n * Jdenticon 3.3.0\n * http://jdenticon.com\n *\n * Built: 2024-05-10T09:48:41.921Z\n *\n * MIT License\n *\n * Copyright (c) 2014-2024 Daniel Mester Pirttijärvi\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n(function (umdGlobal, factory) {\n    var jdenticon = factory(umdGlobal);\n\n    // Node.js\n    if (typeof module !== \"undefined\" && \"exports\" in module) {\n        module[\"exports\"] = jdenticon;\n    }\n    // RequireJS\n    else if (typeof define === \"function\" && define[\"amd\"]) {\n        define([], function () { return jdenticon; });\n    }\n    // No module loader\n    else {\n        umdGlobal[\"jdenticon\"] = jdenticon;\n    }\n})(typeof self !== \"undefined\" ? self : this, function (umdGlobal) {\n'use strict';\n\n/**\n * Parses a substring of the hash as a number.\n * @param {number} startPosition\n * @param {number=} octets\n */\nfunction parseHex(hash, startPosition, octets) {\n    return parseInt(hash.substr(startPosition, octets), 16);\n}\n\nfunction decToHex(v) {\n    v |= 0; // Ensure integer value\n    return v < 0 ? \"00\" :\n        v < 16 ? \"0\" + v.toString(16) :\n        v < 256 ? v.toString(16) :\n        \"ff\";\n}\n\nfunction hueToRgb(m1, m2, h) {\n    h = h < 0 ? h + 6 : h > 6 ? h - 6 : h;\n    return decToHex(255 * (\n        h < 1 ? m1 + (m2 - m1) * h :\n        h < 3 ? m2 :\n        h < 4 ? m1 + (m2 - m1) * (4 - h) :\n        m1));\n}\n\n/**\n * @param {string} color  Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported.\n * @returns {string}\n */\nfunction parseColor(color) {\n    if (/^#[0-9a-f]{3,8}$/i.test(color)) {\n        var result;\n        var colorLength = color.length;\n\n        if (colorLength < 6) {\n            var r = color[1],\n                  g = color[2],\n                  b = color[3],\n                  a = color[4] || \"\";\n            result = \"#\" + r + r + g + g + b + b + a + a;\n        }\n        if (colorLength == 7 || colorLength > 8) {\n            result = color;\n        }\n\n        return result;\n    }\n}\n\n/**\n * Converts a hexadecimal color to a CSS3 compatible color.\n * @param {string} hexColor  Color on the format \"#RRGGBB\" or \"#RRGGBBAA\"\n * @returns {string}\n */\nfunction toCss3Color(hexColor) {\n    var a = parseHex(hexColor, 7, 2);\n    var result;\n\n    if (isNaN(a)) {\n        result = hexColor;\n    } else {\n        var r = parseHex(hexColor, 1, 2),\n            g = parseHex(hexColor, 3, 2),\n            b = parseHex(hexColor, 5, 2);\n        result = \"rgba(\" + r + \",\" + g + \",\" + b + \",\" + (a / 255).toFixed(2) + \")\";\n    }\n\n    return result;\n}\n\n/**\n * Converts an HSL color to a hexadecimal RGB color.\n * @param {number} hue  Hue in range [0, 1]\n * @param {number} saturation  Saturation in range [0, 1]\n * @param {number} lightness  Lightness in range [0, 1]\n * @returns {string}\n */\nfunction hsl(hue, saturation, lightness) {\n    // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color\n    var result;\n\n    if (saturation == 0) {\n        var partialHex = decToHex(lightness * 255);\n        result = partialHex + partialHex + partialHex;\n    }\n    else {\n        var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation,\n              m1 = lightness * 2 - m2;\n        result =\n            hueToRgb(m1, m2, hue * 6 + 2) +\n            hueToRgb(m1, m2, hue * 6) +\n            hueToRgb(m1, m2, hue * 6 - 2);\n    }\n\n    return \"#\" + result;\n}\n\n/**\n * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the \"dark\" hues\n * @param {number} hue  Hue in range [0, 1]\n * @param {number} saturation  Saturation in range [0, 1]\n * @param {number} lightness  Lightness in range [0, 1]\n * @returns {string}\n */\nfunction correctedHsl(hue, saturation, lightness) {\n    // The corrector specifies the perceived middle lightness for each hue\n    var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ],\n          corrector = correctors[(hue * 6 + 0.5) | 0];\n\n    // Adjust the input lightness relative to the corrector\n    lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2;\n\n    return hsl(hue, saturation, lightness);\n}\n\n/* global umdGlobal */\n\n// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for\n// backward compatibility.\nvar GLOBAL = umdGlobal;\n\n/**\n * @typedef {Object} ParsedConfiguration\n * @property {number} colorSaturation\n * @property {number} grayscaleSaturation\n * @property {string} backColor\n * @property {number} iconPadding\n * @property {function(number):number} hue\n * @property {function(number):number} colorLightness\n * @property {function(number):number} grayscaleLightness\n */\n\nvar CONFIG_PROPERTIES = {\n    G/*GLOBAL*/: \"jdenticon_config\",\n    n/*MODULE*/: \"config\",\n};\n\nvar rootConfigurationHolder = {};\n\n/**\n * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console\n * when it is being used.\n * @param {!Object} rootObject\n */\nfunction defineConfigProperty(rootObject) {\n    rootConfigurationHolder = rootObject;\n}\n\n/**\n * Sets a new icon style configuration. The new configuration is not merged with the previous one. *\n * @param {Object} newConfiguration - New configuration object.\n */\nfunction configure(newConfiguration) {\n    if (arguments.length) {\n        rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration;\n    }\n    return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/];\n}\n\n/**\n * Gets the normalized current Jdenticon color configuration. Missing fields have default values.\n * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A\n *    local configuration overrides the global configuration in it entirety. This parameter can for backward\n *    compatibility also contain a padding value. A padding value only overrides the global padding, not the\n *    entire global configuration.\n * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor\n *    explicitly to the API method.\n * @returns {ParsedConfiguration}\n */\nfunction getConfiguration(paddingOrLocalConfig, defaultPadding) {\n    var configObject =\n            typeof paddingOrLocalConfig == \"object\" && paddingOrLocalConfig ||\n            rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] ||\n            GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] ||\n            { },\n\n        lightnessConfig = configObject[\"lightness\"] || { },\n\n        // In versions < 2.1.0 there was no grayscale saturation -\n        // saturation was the color saturation.\n        saturation = configObject[\"saturation\"] || { },\n        colorSaturation = \"color\" in saturation ? saturation[\"color\"] : saturation,\n        grayscaleSaturation = saturation[\"grayscale\"],\n\n        backColor = configObject[\"backColor\"],\n        padding = configObject[\"padding\"];\n\n    /**\n     * Creates a lightness range.\n     */\n    function lightness(configName, defaultRange) {\n        var range = lightnessConfig[configName];\n\n        // Check if the lightness range is an array-like object. This way we ensure the\n        // array contain two values at the same time.\n        if (!(range && range.length > 1)) {\n            range = defaultRange;\n        }\n\n        /**\n         * Gets a lightness relative the specified value in the specified lightness range.\n         */\n        return function (value) {\n            value = range[0] + value * (range[1] - range[0]);\n            return value < 0 ? 0 : value > 1 ? 1 : value;\n        };\n    }\n\n    /**\n     * Gets a hue allowed by the configured hue restriction,\n     * provided the originally computed hue.\n     */\n    function hueFunction(originalHue) {\n        var hueConfig = configObject[\"hues\"];\n        var hue;\n\n        // Check if 'hues' is an array-like object. This way we also ensure that\n        // the array is not empty, which would mean no hue restriction.\n        if (hueConfig && hueConfig.length > 0) {\n            // originalHue is in the range [0, 1]\n            // Multiply with 0.999 to change the range to [0, 1) and then truncate the index.\n            hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)];\n        }\n\n        return typeof hue == \"number\" ?\n\n            // A hue was specified. We need to convert the hue from\n            // degrees on any turn - e.g. 746° is a perfectly valid hue -\n            // to turns in the range [0, 1).\n            ((((hue / 360) % 1) + 1) % 1) :\n\n            // No hue configured => use original hue\n            originalHue;\n    }\n\n    return {\n        X/*hue*/: hueFunction,\n        p/*colorSaturation*/: typeof colorSaturation == \"number\" ? colorSaturation : 0.5,\n        H/*grayscaleSaturation*/: typeof grayscaleSaturation == \"number\" ? grayscaleSaturation : 0,\n        q/*colorLightness*/: lightness(\"color\", [0.4, 0.8]),\n        I/*grayscaleLightness*/: lightness(\"grayscale\", [0.3, 0.9]),\n        J/*backColor*/: parseColor(backColor),\n        Y/*iconPadding*/:\n            typeof paddingOrLocalConfig == \"number\" ? paddingOrLocalConfig :\n            typeof padding == \"number\" ? padding :\n            defaultPadding\n    }\n}\n\nvar ICON_TYPE_SVG = 1;\n\nvar ICON_TYPE_CANVAS = 2;\n\nvar ATTRIBUTES = {\n    t/*HASH*/: \"data-jdenticon-hash\",\n    o/*VALUE*/: \"data-jdenticon-value\"\n};\n\nvar IS_RENDERED_PROPERTY = \"jdenticonRendered\";\n\nvar ICON_SELECTOR = \"[\" + ATTRIBUTES.t/*HASH*/ +\"],[\" + ATTRIBUTES.o/*VALUE*/ +\"]\";\n\nvar documentQuerySelectorAll = /** @type {!Function} */ (\n    typeof document !== \"undefined\" && document.querySelectorAll.bind(document));\n\nfunction getIdenticonType(el) {\n    if (el) {\n        var tagName = el[\"tagName\"];\n\n        if (/^svg$/i.test(tagName)) {\n            return ICON_TYPE_SVG;\n        }\n\n        if (/^canvas$/i.test(tagName) && \"getContext\" in el) {\n            return ICON_TYPE_CANVAS;\n        }\n    }\n}\n\nfunction whenDocumentIsReady(/** @type {Function} */ callback) {\n    function loadedHandler() {\n        document.removeEventListener(\"DOMContentLoaded\", loadedHandler);\n        window.removeEventListener(\"load\", loadedHandler);\n        setTimeout(callback, 0); // Give scripts a chance to run\n    }\n\n    if (typeof document !== \"undefined\" &&\n        typeof window !== \"undefined\" &&\n        typeof setTimeout !== \"undefined\"\n    ) {\n        if (document.readyState === \"loading\") {\n            document.addEventListener(\"DOMContentLoaded\", loadedHandler);\n            window.addEventListener(\"load\", loadedHandler);\n        } else {\n            // Document already loaded. The load events above likely won't be raised\n            setTimeout(callback, 0);\n        }\n    }\n}\n\nfunction observer(updateCallback) {\n    if (typeof MutationObserver != \"undefined\") {\n        var mutationObserver = new MutationObserver(function onmutation(mutations) {\n            for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) {\n                var mutation = mutations[mutationIndex];\n                var addedNodes = mutation.addedNodes;\n\n                for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) {\n                    var addedNode = addedNodes[addedNodeIndex];\n\n                    // Skip other types of nodes than element nodes, since they might not support\n                    // the querySelectorAll method => runtime error.\n                    if (addedNode.nodeType == 1) {\n                        if (getIdenticonType(addedNode)) {\n                            updateCallback(addedNode);\n                        }\n                        else {\n                            var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR);\n                            for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) {\n                                updateCallback(icons[iconIndex]);\n                            }\n                        }\n                    }\n                }\n\n                if (mutation.type == \"attributes\" && getIdenticonType(mutation.target)) {\n                    updateCallback(mutation.target);\n                }\n            }\n        });\n\n        mutationObserver.observe(document.body, {\n            \"childList\": true,\n            \"attributes\": true,\n            \"attributeFilter\": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, \"width\", \"height\"],\n            \"subtree\": true,\n        });\n    }\n}\n\n/**\n * Represents a point.\n */\nfunction Point(x, y) {\n    this.x = x;\n    this.y = y;\n}\n\n/**\n * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself,\n * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly.\n */\nfunction Transform(x, y, size, rotation) {\n    this.u/*_x*/ = x;\n    this.v/*_y*/ = y;\n    this.K/*_size*/ = size;\n    this.Z/*_rotation*/ = rotation;\n}\n\n/**\n * Transforms the specified point based on the translation and rotation specification for this Transform.\n * @param {number} x x-coordinate\n * @param {number} y y-coordinate\n * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.\n * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.\n */\nTransform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) {\n    var right = this.u/*_x*/ + this.K/*_size*/,\n          bottom = this.v/*_y*/ + this.K/*_size*/,\n          rotation = this.Z/*_rotation*/;\n    return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) :\n           rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) :\n           rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) :\n           new Point(this.u/*_x*/ + x, this.v/*_y*/ + y);\n};\n\nvar NO_TRANSFORM = new Transform(0, 0, 0, 0);\n\n\n\n/**\n * Provides helper functions for rendering common basic shapes.\n */\nfunction Graphics(renderer) {\n    /**\n     * @type {Renderer}\n     * @private\n     */\n    this.M/*_renderer*/ = renderer;\n\n    /**\n     * @type {Transform}\n     */\n    this.A/*currentTransform*/ = NO_TRANSFORM;\n}\nvar Graphics__prototype = Graphics.prototype;\n\n/**\n * Adds a polygon to the underlying renderer.\n * @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ]\n * @param {boolean=} invert Specifies if the polygon will be inverted.\n */\nGraphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) {\n        var this$1 = this;\n\n    var di = invert ? -2 : 2,\n          transformedPoints = [];\n\n    for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) {\n        transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1]));\n    }\n\n    this.M/*_renderer*/.g/*addPolygon*/(transformedPoints);\n};\n\n/**\n * Adds a polygon to the underlying renderer.\n * Source: http://stackoverflow.com/a/2173084\n * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse.\n * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse.\n * @param {number} size The size of the ellipse.\n * @param {boolean=} invert Specifies if the ellipse will be inverted.\n */\nGraphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) {\n    var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size);\n    this.M/*_renderer*/.h/*addCircle*/(p, size, invert);\n};\n\n/**\n * Adds a rectangle to the underlying renderer.\n * @param {number} x The x-coordinate of the upper left corner of the rectangle.\n * @param {number} y The y-coordinate of the upper left corner of the rectangle.\n * @param {number} w The width of the rectangle.\n * @param {number} h The height of the rectangle.\n * @param {boolean=} invert Specifies if the rectangle will be inverted.\n */\nGraphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) {\n    this.g/*addPolygon*/([\n        x, y,\n        x + w, y,\n        x + w, y + h,\n        x, y + h\n    ], invert);\n};\n\n/**\n * Adds a right triangle to the underlying renderer.\n * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle.\n * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle.\n * @param {number} w The width of the triangle.\n * @param {number} h The height of the triangle.\n * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle.\n * @param {boolean=} invert Specifies if the triangle will be inverted.\n */\nGraphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) {\n    var points = [\n        x + w, y,\n        x + w, y + h,\n        x, y + h,\n        x, y\n    ];\n    points.splice(((r || 0) % 4) * 2, 2);\n    this.g/*addPolygon*/(points, invert);\n};\n\n/**\n * Adds a rhombus to the underlying renderer.\n * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus.\n * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus.\n * @param {number} w The width of the rhombus.\n * @param {number} h The height of the rhombus.\n * @param {boolean=} invert Specifies if the rhombus will be inverted.\n */\nGraphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) {\n    this.g/*addPolygon*/([\n        x + w / 2, y,\n        x + w, y + h / 2,\n        x + w / 2, y + h,\n        x, y + h / 2\n    ], invert);\n};\n\n/**\n * @param {number} index\n * @param {Graphics} g\n * @param {number} cell\n * @param {number} positionIndex\n */\nfunction centerShape(index, g, cell, positionIndex) {\n    index = index % 14;\n\n    var k, m, w, h, inner, outer;\n\n    !index ? (\n        k = cell * 0.42,\n        g.g/*addPolygon*/([\n            0, 0,\n            cell, 0,\n            cell, cell - k * 2,\n            cell - k, cell,\n            0, cell\n        ])) :\n\n    index == 1 ? (\n        w = 0 | (cell * 0.5),\n        h = 0 | (cell * 0.8),\n\n        g.j/*addTriangle*/(cell - w, 0, w, h, 2)) :\n\n    index == 2 ? (\n        w = 0 | (cell / 3),\n        g.i/*addRectangle*/(w, w, cell - w, cell - w)) :\n\n    index == 3 ? (\n        inner = cell * 0.1,\n        // Use fixed outer border widths in small icons to ensure the border is drawn\n        outer =\n            cell < 6 ? 1 :\n            cell < 8 ? 2 :\n            (0 | (cell * 0.25)),\n\n        inner =\n            inner > 1 ? (0 | inner) : // large icon => truncate decimals\n            inner > 0.5 ? 1 :         // medium size icon => fixed width\n            inner,                    // small icon => anti-aliased border\n\n        g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) :\n\n    index == 4 ? (\n        m = 0 | (cell * 0.15),\n        w = 0 | (cell * 0.5),\n        g.h/*addCircle*/(cell - w - m, cell - w - m, w)) :\n\n    index == 5 ? (\n        inner = cell * 0.1,\n        outer = inner * 4,\n\n        // Align edge to nearest pixel in large icons\n        outer > 3 && (outer = 0 | outer),\n\n        g.i/*addRectangle*/(0, 0, cell, cell),\n        g.g/*addPolygon*/([\n            outer, outer,\n            cell - inner, outer,\n            outer + (cell - outer - inner) / 2, cell - inner\n        ], true)) :\n\n    index == 6 ?\n        g.g/*addPolygon*/([\n            0, 0,\n            cell, 0,\n            cell, cell * 0.7,\n            cell * 0.4, cell * 0.4,\n            cell * 0.7, cell,\n            0, cell\n        ]) :\n\n    index == 7 ?\n        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :\n\n    index == 8 ? (\n        g.i/*addRectangle*/(0, 0, cell, cell / 2),\n        g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2),\n        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) :\n\n    index == 9 ? (\n        inner = cell * 0.14,\n        // Use fixed outer border widths in small icons to ensure the border is drawn\n        outer =\n            cell < 4 ? 1 :\n            cell < 6 ? 2 :\n            (0 | (cell * 0.35)),\n\n        inner =\n            cell < 8 ? inner : // small icon => anti-aliased border\n            (0 | inner),       // large icon => truncate decimals\n\n        g.i/*addRectangle*/(0, 0, cell, cell),\n        g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) :\n\n    index == 10 ? (\n        inner = cell * 0.12,\n        outer = inner * 3,\n\n        g.i/*addRectangle*/(0, 0, cell, cell),\n        g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) :\n\n    index == 11 ?\n        g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :\n\n    index == 12 ? (\n        m = cell * 0.25,\n        g.i/*addRectangle*/(0, 0, cell, cell),\n        g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) :\n\n    // 13\n    (\n        !positionIndex && (\n            m = cell * 0.4, w = cell * 1.2,\n            g.h/*addCircle*/(m, m, w)\n        )\n    );\n}\n\n/**\n * @param {number} index\n * @param {Graphics} g\n * @param {number} cell\n */\nfunction outerShape(index, g, cell) {\n    index = index % 4;\n\n    var m;\n\n    !index ?\n        g.j/*addTriangle*/(0, 0, cell, cell, 0) :\n\n    index == 1 ?\n        g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) :\n\n    index == 2 ?\n        g.N/*addRhombus*/(0, 0, cell, cell) :\n\n    // 3\n    (\n        m = cell / 6,\n        g.h/*addCircle*/(m, m, cell - 2 * m)\n    );\n}\n\n/**\n * Gets a set of identicon color candidates for a specified hue and config.\n * @param {number} hue\n * @param {ParsedConfiguration} config\n */\nfunction colorTheme(hue, config) {\n    hue = config.X/*hue*/(hue);\n    return [\n        // Dark gray\n        correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)),\n        // Mid color\n        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)),\n        // Light gray\n        correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)),\n        // Light color\n        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)),\n        // Dark color\n        correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0))\n    ];\n}\n\n/**\n * Draws an identicon to a specified renderer.\n * @param {Renderer} renderer\n * @param {string} hash\n * @param {Object|number=} config\n */\nfunction iconGenerator(renderer, hash, config) {\n    var parsedConfig = getConfiguration(config, 0.08);\n\n    // Set background color\n    if (parsedConfig.J/*backColor*/) {\n        renderer.m/*setBackground*/(parsedConfig.J/*backColor*/);\n    }\n\n    // Calculate padding and round to nearest integer\n    var size = renderer.k/*iconSize*/;\n    var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0;\n    size -= padding * 2;\n\n    var graphics = new Graphics(renderer);\n\n    // Calculate cell size and ensure it is an integer\n    var cell = 0 | (size / 4);\n\n    // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon\n    var x = 0 | (padding + size / 2 - cell * 2);\n    var y = 0 | (padding + size / 2 - cell * 2);\n\n    function renderShape(colorIndex, shapes, index, rotationIndex, positions) {\n        var shapeIndex = parseHex(hash, index, 1);\n        var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0;\n\n        renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]);\n\n        for (var i = 0; i < positions.length; i++) {\n            graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4);\n            shapes(shapeIndex, graphics, cell, i);\n        }\n\n        renderer.P/*endShape*/();\n    }\n\n    // AVAILABLE COLORS\n    var hue = parseHex(hash, -7) / 0xfffffff,\n\n          // Available colors for this icon\n          availableColors = colorTheme(hue, parsedConfig),\n\n          // The index of the selected colors\n          selectedColorIndexes = [];\n\n    var index;\n\n    function isDuplicate(values) {\n        if (values.indexOf(index) >= 0) {\n            for (var i = 0; i < values.length; i++) {\n                if (selectedColorIndexes.indexOf(values[i]) >= 0) {\n                    return true;\n                }\n            }\n        }\n    }\n\n    for (var i = 0; i < 3; i++) {\n        index = parseHex(hash, 8 + i, 1) % availableColors.length;\n        if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo\n            isDuplicate([2, 3])) { // Disallow light gray and light color combo\n            index = 1;\n        }\n        selectedColorIndexes.push(index);\n    }\n\n    // ACTUAL RENDERING\n    // Sides\n    renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);\n    // Corners\n    renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);\n    // Center\n    renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);\n\n    renderer.finish();\n}\n\n/**\n * Computes a SHA1 hash for any value and returns it as a hexadecimal string.\n *\n * This function is optimized for minimal code size and rather short messages.\n *\n * @param {string} message\n */\nfunction sha1(message) {\n    var HASH_SIZE_HALF_BYTES = 40;\n    var BLOCK_SIZE_WORDS = 16;\n\n    // Variables\n    // `var` is used to be able to minimize the number of `var` keywords.\n    var i = 0,\n        f = 0,\n\n        // Use `encodeURI` to UTF8 encode the message without any additional libraries\n        // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky\n        // since `unescape` is deprecated.\n        urlEncodedMessage = encodeURI(message) + \"%80\", // trailing '1' bit padding\n\n        // This can be changed to a preallocated Uint32Array array for greater performance and larger code size\n        data = [],\n        dataSize,\n\n        hashBuffer = [],\n\n        a = 0x67452301,\n        b = 0xefcdab89,\n        c = ~a,\n        d = ~b,\n        e = 0xc3d2e1f0,\n        hash = [a, b, c, d, e],\n\n        blockStartIndex = 0,\n        hexHash = \"\";\n\n    /**\n     * Rotates the value a specified number of bits to the left.\n     * @param {number} value  Value to rotate\n     * @param {number} shift  Bit count to shift.\n     */\n    function rotl(value, shift) {\n        return (value << shift) | (value >>> (32 - shift));\n    }\n\n    // Message data\n    for ( ; i < urlEncodedMessage.length; f++) {\n        data[f >> 2] = data[f >> 2] |\n            (\n                (\n                    urlEncodedMessage[i] == \"%\"\n                        // Percent encoded byte\n                        ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16)\n                        // Unencoded byte\n                        : urlEncodedMessage.charCodeAt(i++)\n                )\n\n                // Read bytes in reverse order (big endian words)\n                << ((3 - (f & 3)) * 8)\n            );\n    }\n\n    // f is now the length of the utf8 encoded message\n    // 7 = 8 bytes (64 bit) for message size, -1 to round down\n    // >> 6 = integer division with block size\n    dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS;\n\n    // Message size in bits.\n    // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least\n    // significant 32 bits are set. -8 is for the '1' bit padding byte.\n    data[dataSize - 1] = f * 8 - 8;\n\n    // Compute hash\n    for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) {\n        for (i = 0; i < 80; i++) {\n            f = rotl(a, 5) + e + (\n                    // Ch\n                    i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 :\n\n                    // Parity\n                    i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 :\n\n                    // Maj\n                    i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc :\n\n                    // Parity\n                    (b ^ c ^ d) + 0xca62c1d6\n                ) + (\n                    hashBuffer[i] = i < BLOCK_SIZE_WORDS\n                        // Bitwise OR is used to coerse `undefined` to 0\n                        ? (data[blockStartIndex + i] | 0)\n                        : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1)\n                );\n\n            e = d;\n            d = c;\n            c = rotl(b, 30);\n            b = a;\n            a = f;\n        }\n\n        hash[0] = a = ((hash[0] + a) | 0);\n        hash[1] = b = ((hash[1] + b) | 0);\n        hash[2] = c = ((hash[2] + c) | 0);\n        hash[3] = d = ((hash[3] + d) | 0);\n        hash[4] = e = ((hash[4] + e) | 0);\n    }\n\n    // Format hex hash\n    for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) {\n        hexHash += (\n            (\n                // Get word (2^3 half-bytes per word)\n                hash[i >> 3] >>>\n\n                // Append half-bytes in reverse order\n                ((7 - (i & 7)) * 4)\n            )\n            // Clamp to half-byte\n            & 0xf\n        ).toString(16);\n    }\n\n    return hexHash;\n}\n\n/**\n * Inputs a value that might be a valid hash string for Jdenticon and returns it\n * if it is determined valid, otherwise a falsy value is returned.\n */\nfunction isValidHash(hashCandidate) {\n    return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate;\n}\n\n/**\n * Computes a hash for the specified value. Currently SHA1 is used. This function\n * always returns a valid hash.\n */\nfunction computeHash(value) {\n    return sha1(value == null ? \"\" : \"\" + value);\n}\n\n\n\n/**\n * Renderer redirecting drawing commands to a canvas context.\n * @implements {Renderer}\n */\nfunction CanvasRenderer(ctx, iconSize) {\n    var canvas = ctx.canvas;\n    var width = canvas.width;\n    var height = canvas.height;\n\n    ctx.save();\n\n    if (!iconSize) {\n        iconSize = Math.min(width, height);\n\n        ctx.translate(\n            ((width - iconSize) / 2) | 0,\n            ((height - iconSize) / 2) | 0);\n    }\n\n    /**\n     * @private\n     */\n    this.l/*_ctx*/ = ctx;\n    this.k/*iconSize*/ = iconSize;\n\n    ctx.clearRect(0, 0, iconSize, iconSize);\n}\nvar CanvasRenderer__prototype = CanvasRenderer.prototype;\n\n/**\n * Fills the background with the specified color.\n * @param {string} fillColor  Fill color on the format #rrggbb[aa].\n */\nCanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) {\n    var ctx = this.l/*_ctx*/;\n    var iconSize = this.k/*iconSize*/;\n\n    ctx.fillStyle = toCss3Color(fillColor);\n    ctx.fillRect(0, 0, iconSize, iconSize);\n};\n\n/**\n * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.\n * @param {string} fillColor Fill color on format #rrggbb[aa].\n */\nCanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) {\n    var ctx = this.l/*_ctx*/;\n    ctx.fillStyle = toCss3Color(fillColor);\n    ctx.beginPath();\n};\n\n/**\n * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas.\n */\nCanvasRenderer__prototype.P/*endShape*/ = function endShape () {\n    this.l/*_ctx*/.fill();\n};\n\n/**\n * Adds a polygon to the rendering queue.\n * @param points An array of Point objects.\n */\nCanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) {\n    var ctx = this.l/*_ctx*/;\n    ctx.moveTo(points[0].x, points[0].y);\n    for (var i = 1; i < points.length; i++) {\n        ctx.lineTo(points[i].x, points[i].y);\n    }\n    ctx.closePath();\n};\n\n/**\n * Adds a circle to the rendering queue.\n * @param {Point} point The upper left corner of the circle bounding box.\n * @param {number} diameter The diameter of the circle.\n * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).\n */\nCanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {\n    var ctx = this.l/*_ctx*/,\n          radius = diameter / 2;\n    ctx.moveTo(point.x + radius, point.y + radius);\n    ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise);\n    ctx.closePath();\n};\n\n/**\n * Called when the icon has been completely drawn.\n */\nCanvasRenderer__prototype.finish = function finish () {\n    this.l/*_ctx*/.restore();\n};\n\n/**\n * Draws an identicon to a context.\n * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0).\n * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.\n * @param {number} size - Icon size in pixels.\n * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any\n *    global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be\n *    specified in place of a configuration object.\n */\nfunction drawIcon(ctx, hashOrValue, size, config) {\n    if (!ctx) {\n        throw new Error(\"No canvas specified.\");\n    }\n\n    iconGenerator(new CanvasRenderer(ctx, size),\n        isValidHash(hashOrValue) || computeHash(hashOrValue),\n        config);\n\n    var canvas = ctx.canvas;\n    if (canvas) {\n        canvas[IS_RENDERED_PROPERTY] = true;\n    }\n}\n\n/**\n * Prepares a measure to be used as a measure in an SVG path, by\n * rounding the measure to a single decimal. This reduces the file\n * size of the generated SVG with more than 50% in some cases.\n */\nfunction svgValue(value) {\n    return ((value * 10 + 0.5) | 0) / 10;\n}\n\n/**\n * Represents an SVG path element.\n */\nfunction SvgPath() {\n    /**\n     * This property holds the data string (path.d) of the SVG path.\n     * @type {string}\n     */\n    this.B/*dataString*/ = \"\";\n}\nvar SvgPath__prototype = SvgPath.prototype;\n\n/**\n * Adds a polygon with the current fill color to the SVG path.\n * @param points An array of Point objects.\n */\nSvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) {\n    var dataString = \"\";\n    for (var i = 0; i < points.length; i++) {\n        dataString += (i ? \"L\" : \"M\") + svgValue(points[i].x) + \" \" + svgValue(points[i].y);\n    }\n    this.B/*dataString*/ += dataString + \"Z\";\n};\n\n/**\n * Adds a circle with the current fill color to the SVG path.\n * @param {Point} point The upper left corner of the circle bounding box.\n * @param {number} diameter The diameter of the circle.\n * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).\n */\nSvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {\n    var sweepFlag = counterClockwise ? 0 : 1,\n          svgRadius = svgValue(diameter / 2),\n          svgDiameter = svgValue(diameter),\n          svgArc = \"a\" + svgRadius + \",\" + svgRadius + \" 0 1,\" + sweepFlag + \" \";\n\n    this.B/*dataString*/ +=\n        \"M\" + svgValue(point.x) + \" \" + svgValue(point.y + diameter / 2) +\n        svgArc + svgDiameter + \",0\" +\n        svgArc + (-svgDiameter) + \",0\";\n};\n\n\n\n/**\n * Renderer producing SVG output.\n * @implements {Renderer}\n */\nfunction SvgRenderer(target) {\n    /**\n     * @type {SvgPath}\n     * @private\n     */\n    this.C/*_path*/;\n\n    /**\n     * @type {Object.<string,SvgPath>}\n     * @private\n     */\n    this.D/*_pathsByColor*/ = { };\n\n    /**\n     * @type {SvgElement|SvgWriter}\n     * @private\n     */\n    this.R/*_target*/ = target;\n\n    /**\n     * @type {number}\n     */\n    this.k/*iconSize*/ = target.k/*iconSize*/;\n}\nvar SvgRenderer__prototype = SvgRenderer.prototype;\n\n/**\n * Fills the background with the specified color.\n * @param {string} fillColor  Fill color on the format #rrggbb[aa].\n */\nSvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) {\n    var match = /^(#......)(..)?/.exec(fillColor),\n          opacity = match[2] ? parseHex(match[2], 0) / 255 : 1;\n    this.R/*_target*/.m/*setBackground*/(match[1], opacity);\n};\n\n/**\n * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.\n * @param {string} color Fill color on format #xxxxxx.\n */\nSvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) {\n    this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath());\n};\n\n/**\n * Marks the end of the currently drawn shape.\n */\nSvgRenderer__prototype.P/*endShape*/ = function endShape () { };\n\n/**\n * Adds a polygon with the current fill color to the SVG.\n * @param points An array of Point objects.\n */\nSvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) {\n    this.C/*_path*/.g/*addPolygon*/(points);\n};\n\n/**\n * Adds a circle with the current fill color to the SVG.\n * @param {Point} point The upper left corner of the circle bounding box.\n * @param {number} diameter The diameter of the circle.\n * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).\n */\nSvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) {\n    this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise);\n};\n\n/**\n * Called when the icon has been completely drawn.\n */\nSvgRenderer__prototype.finish = function finish () {\n        var this$1 = this;\n\n    var pathsByColor = this.D/*_pathsByColor*/;\n    for (var color in pathsByColor) {\n        // hasOwnProperty cannot be shadowed in pathsByColor\n        // eslint-disable-next-line no-prototype-builtins\n        if (pathsByColor.hasOwnProperty(color)) {\n            this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/);\n        }\n    }\n};\n\nvar SVG_CONSTANTS = {\n    T/*XMLNS*/: \"http://www.w3.org/2000/svg\",\n    U/*WIDTH*/: \"width\",\n    V/*HEIGHT*/: \"height\",\n};\n\n/**\n * Renderer producing SVG output.\n */\nfunction SvgWriter(iconSize) {\n    /**\n     * @type {number}\n     */\n    this.k/*iconSize*/ = iconSize;\n\n    /**\n     * @type {string}\n     * @private\n     */\n    this.F/*_s*/ =\n        '<svg xmlns=\"' + SVG_CONSTANTS.T/*XMLNS*/ + '\" width=\"' +\n        iconSize + '\" height=\"' + iconSize + '\" viewBox=\"0 0 ' +\n        iconSize + ' ' + iconSize + '\">';\n}\nvar SvgWriter__prototype = SvgWriter.prototype;\n\n/**\n * Fills the background with the specified color.\n * @param {string} fillColor  Fill color on the format #rrggbb.\n * @param {number} opacity  Opacity in the range [0.0, 1.0].\n */\nSvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) {\n    if (opacity) {\n        this.F/*_s*/ += '<rect width=\"100%\" height=\"100%\" fill=\"' +\n            fillColor + '\" opacity=\"' + opacity.toFixed(2) + '\"/>';\n    }\n};\n\n/**\n * Writes a path to the SVG string.\n * @param {string} color Fill color on format #rrggbb.\n * @param {string} dataString The SVG path data string.\n */\nSvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) {\n    this.F/*_s*/ += '<path fill=\"' + color + '\" d=\"' + dataString + '\"/>';\n};\n\n/**\n * Gets the rendered image as an SVG string.\n */\nSvgWriter__prototype.toString = function toString () {\n    return this.F/*_s*/ + \"</svg>\";\n};\n\n/**\n * Draws an identicon as an SVG string.\n * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.\n * @param {number} size - Icon size in pixels.\n * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any\n *    global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be\n *    specified in place of a configuration object.\n * @returns {string} SVG string\n */\nfunction toSvg(hashOrValue, size, config) {\n    var writer = new SvgWriter(size);\n    iconGenerator(new SvgRenderer(writer),\n        isValidHash(hashOrValue) || computeHash(hashOrValue),\n        config);\n    return writer.toString();\n}\n\n/**\n * Creates a new element and adds it to the specified parent.\n * @param {Element} parentNode\n * @param {string} name\n * @param {...(string|number)} keyValuePairs\n */\nfunction SvgElement_append(parentNode, name) {\n    var keyValuePairs = [], len = arguments.length - 2;\n    while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ];\n\n    var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name);\n\n    for (var i = 0; i + 1 < keyValuePairs.length; i += 2) {\n        el.setAttribute(\n            /** @type {string} */(keyValuePairs[i]),\n            /** @type {string} */(keyValuePairs[i + 1])\n            );\n    }\n\n    parentNode.appendChild(el);\n}\n\n\n/**\n * Renderer producing SVG output.\n */\nfunction SvgElement(element) {\n    // Don't use the clientWidth and clientHeight properties on SVG elements\n    // since Firefox won't serve a proper value of these properties on SVG\n    // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811)\n    // Instead use 100px as a hardcoded size (the svg viewBox will rescale\n    // the icon to the correct dimensions)\n    var iconSize = this.k/*iconSize*/ = Math.min(\n        (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100),\n        (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100)\n        );\n\n    /**\n     * @type {Element}\n     * @private\n     */\n    this.W/*_el*/ = element;\n\n    // Clear current SVG child elements\n    while (element.firstChild) {\n        element.removeChild(element.firstChild);\n    }\n\n    // Set viewBox attribute to ensure the svg scales nicely.\n    element.setAttribute(\"viewBox\", \"0 0 \" + iconSize + \" \" + iconSize);\n    element.setAttribute(\"preserveAspectRatio\", \"xMidYMid meet\");\n}\nvar SvgElement__prototype = SvgElement.prototype;\n\n/**\n * Fills the background with the specified color.\n * @param {string} fillColor  Fill color on the format #rrggbb.\n * @param {number} opacity  Opacity in the range [0.0, 1.0].\n */\nSvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) {\n    if (opacity) {\n        SvgElement_append(this.W/*_el*/, \"rect\",\n            SVG_CONSTANTS.U/*WIDTH*/, \"100%\",\n            SVG_CONSTANTS.V/*HEIGHT*/, \"100%\",\n            \"fill\", fillColor,\n            \"opacity\", opacity);\n    }\n};\n\n/**\n * Appends a path to the SVG element.\n * @param {string} color Fill color on format #xxxxxx.\n * @param {string} dataString The SVG path data string.\n */\nSvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) {\n    SvgElement_append(this.W/*_el*/, \"path\",\n        \"fill\", color,\n        \"d\", dataString);\n};\n\n/**\n * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute.\n */\nfunction updateAll() {\n    if (documentQuerySelectorAll) {\n        update(ICON_SELECTOR);\n    }\n}\n\n/**\n * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute that have not already\n * been rendered.\n */\nfunction updateAllConditional() {\n    if (documentQuerySelectorAll) {\n        /** @type {NodeListOf<HTMLElement>} */\n        var elements = documentQuerySelectorAll(ICON_SELECTOR);\n\n        for (var i = 0; i < elements.length; i++) {\n            var el = elements[i];\n            if (!el[IS_RENDERED_PROPERTY]) {\n                update(el);\n            }\n        }\n    }\n}\n\n/**\n * Updates the identicon in the specified `<canvas>` or `<svg>` elements.\n * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type\n *    `<svg>` or `<canvas>`, or a CSS selector to such an element.\n * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or\n *    `data-jdenticon-value` attribute will be evaluated.\n * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any\n *    global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be\n *    specified in place of a configuration object.\n */\nfunction update(el, hashOrValue, config) {\n    renderDomElement(el, hashOrValue, config, function (el, iconType) {\n        if (iconType) {\n            return iconType == ICON_TYPE_SVG ?\n                new SvgRenderer(new SvgElement(el)) :\n                new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext(\"2d\"));\n        }\n    });\n}\n\n/**\n * Updates the identicon in the specified canvas or svg elements.\n * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type\n *    `<svg>` or `<canvas>`, or a CSS selector to such an element.\n * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or\n *    `data-jdenticon-value` attribute will be evaluated.\n * @param {Object|number|undefined} config\n * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer.\n */\nfunction renderDomElement(el, hashOrValue, config, rendererFactory) {\n    if (typeof el === \"string\") {\n        if (documentQuerySelectorAll) {\n            var elements = documentQuerySelectorAll(el);\n            for (var i = 0; i < elements.length; i++) {\n                renderDomElement(elements[i], hashOrValue, config, rendererFactory);\n            }\n        }\n        return;\n    }\n\n    // Hash selection. The result from getValidHash or computeHash is\n    // accepted as a valid hash.\n    var hash =\n        // 1. Explicit valid hash\n        isValidHash(hashOrValue) ||\n\n        // 2. Explicit value (`!= null` catches both null and undefined)\n        hashOrValue != null && computeHash(hashOrValue) ||\n\n        // 3. `data-jdenticon-hash` attribute\n        isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) ||\n\n        // 4. `data-jdenticon-value` attribute.\n        // We want to treat an empty attribute as an empty value.\n        // Some browsers return empty string even if the attribute\n        // is not specified, so use hasAttribute to determine if\n        // the attribute is specified.\n        el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/));\n\n    if (!hash) {\n        // No hash specified. Don't render an icon.\n        return;\n    }\n\n    var renderer = rendererFactory(el, getIdenticonType(el));\n    if (renderer) {\n        // Draw icon\n        iconGenerator(renderer, hash, config);\n        el[IS_RENDERED_PROPERTY] = true;\n    }\n}\n\n/**\n * Renders an identicon for all matching supported elements.\n *\n * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not\n * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be\n * evaluated.\n * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global\n * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be\n * specified in place of a configuration object.\n */\nfunction jdenticonJqueryPlugin(hashOrValue, config) {\n    this[\"each\"](function (index, el) {\n        update(el, hashOrValue, config);\n    });\n    return this;\n}\n\n// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js\n\nvar jdenticon = updateAll;\n\ndefineConfigProperty(jdenticon);\n\n// Export public API\njdenticon[\"configure\"] = configure;\njdenticon[\"drawIcon\"] = drawIcon;\njdenticon[\"toSvg\"] = toSvg;\njdenticon[\"update\"] = update;\njdenticon[\"updateCanvas\"] = update;\njdenticon[\"updateSvg\"] = update;\n\n/**\n * Specifies the version of the Jdenticon package in use.\n * @type {string}\n */\njdenticon[\"version\"] = \"3.3.0\";\n\n/**\n * Specifies which bundle of Jdenticon that is used.\n * @type {string}\n */\njdenticon[\"bundle\"] = \"browser-umd\";\n\n// Basic jQuery plugin\nvar jQuery = GLOBAL[\"jQuery\"];\nif (jQuery) {\n    jQuery[\"fn\"][\"jdenticon\"] = jdenticonJqueryPlugin;\n}\n\n/**\n * This function is called once upon page load.\n */\nfunction jdenticonStartup() {\n    var replaceMode = (\n        jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] ||\n        GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] ||\n        { }\n    )[\"replaceMode\"];\n\n    if (replaceMode != \"never\") {\n        updateAllConditional();\n\n        if (replaceMode == \"observe\") {\n            observer(update);\n        }\n    }\n}\n\n// Schedule to render all identicons on the page once it has been loaded.\nwhenDocumentIsReady(jdenticonStartup);\n\nreturn jdenticon;\n\n});"
  },
  {
    "path": "src/static/scripts/jquery-4.0.0.slim.js",
    "content": "/*!\n * jQuery JavaScript Library v4.0.0+slim\n * https://jquery.com/\n *\n * Copyright OpenJS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.com/license/\n *\n * Date: 2026-01-18T00:20Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\tmodule.exports = factory( global, true );\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n\"use strict\";\n\nif ( !window.document ) {\n\tthrow new Error( \"jQuery requires a window with a document\" );\n}\n\nvar arr = [];\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\n// Support: IE 11+\n// IE doesn't have Array#flat; provide a fallback.\nvar flat = arr.flat ? function( array ) {\n\treturn arr.flat.call( array );\n} : function( array ) {\n\treturn arr.concat.apply( [], array );\n};\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\n// [[Class]] -> type pairs\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\n// All support tests are defined in their respective modules.\nvar support = {};\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\treturn typeof obj === \"object\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n\nfunction isWindow( obj ) {\n\treturn obj != null && obj === obj.window;\n}\n\nfunction isArrayLike( obj ) {\n\n\tvar length = !!obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( typeof obj === \"function\" || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\n\nvar document$1 = window.document;\n\nvar preservedScriptAttributes = {\n\ttype: true,\n\tsrc: true,\n\tnonce: true,\n\tnoModule: true\n};\n\nfunction DOMEval( code, node, doc ) {\n\tdoc = doc || document$1;\n\n\tvar i,\n\t\tscript = doc.createElement( \"script\" );\n\n\tscript.text = code;\n\tfor ( i in preservedScriptAttributes ) {\n\t\tif ( node && node[ i ] ) {\n\t\t\tscript[ i ] = node[ i ];\n\t\t}\n\t}\n\n\tif ( doc.head.appendChild( script ).parentNode ) {\n\t\tscript.parentNode.removeChild( script );\n\t}\n}\n\nvar version = \"4.0.0+slim\",\n\n\trhtmlSuffix = /HTML$/i,\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t};\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teven: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn ( i + 1 ) % 2;\n\t\t} ) );\n\t},\n\n\todd: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn i % 2;\n\t\t} ) );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t}\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && typeof target !== \"function\" ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent Object.prototype pollution\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( name === \"__proto__\" || target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\t\t\t\t\tsrc = target[ name ];\n\n\t\t\t\t\t// Ensure proper type for the source value\n\t\t\t\t\tif ( copyIsArray && !Array.isArray( src ) ) {\n\t\t\t\t\t\tclone = [];\n\t\t\t\t\t} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {\n\t\t\t\t\t\tclone = {};\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src;\n\t\t\t\t\t}\n\t\t\t\t\tcopyIsArray = false;\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a provided context; falls back to the global one\n\t// if not specified.\n\tglobalEval: function( code, options, doc ) {\n\t\tDOMEval( code, { nonce: options && options.nonce }, doc );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\n\t// Retrieve the text value of an array of DOM nodes\n\ttext: function( elem ) {\n\t\tvar node,\n\t\t\tret = \"\",\n\t\t\ti = 0,\n\t\t\tnodeType = elem.nodeType;\n\n\t\tif ( !nodeType ) {\n\n\t\t\t// If no nodeType, this is expected to be an array\n\t\t\twhile ( ( node = elem[ i++ ] ) ) {\n\n\t\t\t\t// Do not traverse comment nodes\n\t\t\t\tret += jQuery.text( node );\n\t\t\t}\n\t\t}\n\t\tif ( nodeType === 1 || nodeType === 11 ) {\n\t\t\treturn elem.textContent;\n\t\t}\n\t\tif ( nodeType === 9 ) {\n\t\t\treturn elem.documentElement.textContent;\n\t\t}\n\t\tif ( nodeType === 3 || nodeType === 4 ) {\n\t\t\treturn elem.nodeValue;\n\t\t}\n\n\t\t// Do not include comment or processing instruction nodes\n\n\t\treturn ret;\n\t},\n\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\tisXMLDoc: function( elem ) {\n\t\tvar namespace = elem && elem.namespaceURI,\n\t\t\tdocElem = elem && ( elem.ownerDocument || elem ).documentElement;\n\n\t\t// Assume HTML when documentElement doesn't yet exist, such as inside\n\t\t// document fragments.\n\t\treturn !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || \"HTML\" );\n\t},\n\n\t// Note: an element does not contain itself\n\tcontains: function( a, b ) {\n\t\tvar bup = b && b.parentNode;\n\n\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\n\t\t\t// Support: IE 9 - 11+\n\t\t\t// IE doesn't have `contains` on SVG.\n\t\t\ta.contains ?\n\t\t\t\ta.contains( bup ) :\n\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t) );\n\t},\n\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn flat( ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\n\tfunction( _i, name ) {\n\t\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n\t} );\n\nfunction nodeName( elem, name ) {\n\treturn elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n}\n\nvar pop = arr.pop;\n\n// https://www.w3.org/TR/css3-selectors/#whitespace\nvar whitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\";\n\nvar isIE = document$1.documentMode;\n\nvar rbuggyQSA = isIE && new RegExp(\n\n\t// Support: IE 9 - 11+\n\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\":enabled|:disabled|\" +\n\n\t// Support: IE 11+\n\t// IE 11 doesn't find elements on a `[name='']` query in some cases.\n\t// Adding a temporary attribute to the document before the selection works\n\t// around the issue.\n\t\"\\\\[\" + whitespace + \"*name\" + whitespace + \"*=\" +\n\twhitespace + \"*(?:''|\\\"\\\")\"\n\n);\n\nvar rtrimCSS = new RegExp(\n\t\"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\",\n\t\"g\"\n);\n\n// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram\nvar identifier = \"(?:\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace +\n\t\"?|\\\\\\\\[^\\\\r\\\\n\\\\f]|[\\\\w-]|[^\\0-\\\\x7f])+\";\n\nvar rleadingCombinator = new RegExp( \"^\" + whitespace + \"*([>+~]|\" +\n\twhitespace + \")\" + whitespace + \"*\" );\n\nvar rdescend = new RegExp( whitespace + \"|>\" );\n\nvar rsibling = /[+~]/;\n\nvar documentElement$1 = document$1.documentElement;\n\n// Support: IE 9 - 11+\n// IE requires a prefix.\nvar matches = documentElement$1.matches || documentElement$1.msMatchesSelector;\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\n\t\t// Use (key + \" \") to avoid collision with native prototype properties\n\t\t// (see https://github.com/jquery/sizzle/issues/157)\n\t\tif ( keys.push( key + \" \" ) > jQuery.expr.cacheLength ) {\n\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn ( cache[ key + \" \" ] = value );\n\t}\n\treturn cache;\n}\n\n/**\n * Checks a node for validity as a jQuery selector context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors\nvar attributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\n\t// Operator (capture 2)\n\t\"*([*^$|!~]?=)\" + whitespace +\n\n\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" +\n\twhitespace + \"*\\\\]\";\n\nvar pseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\n\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\n\t// 2. simple (capture 6)\n\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\n\t// 3. anything else (capture 2)\n\t\".*\" +\n\t\")\\\\)|)\";\n\nvar filterMatchExpr = {\n\tID: new RegExp( \"^#(\" + identifier + \")\" ),\n\tCLASS: new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\tTAG: new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\tATTR: new RegExp( \"^\" + attributes ),\n\tPSEUDO: new RegExp( \"^\" + pseudos ),\n\tCHILD: new RegExp(\n\t\t\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" +\n\t\twhitespace + \"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" +\n\t\twhitespace + \"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" )\n};\n\nvar rpseudo = new RegExp( pseudos );\n\n// CSS escapes\n// https://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\nvar runescape = new RegExp( \"\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace +\n\t\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\", \"g\" ),\n\tfunescape = function( escape, nonHex ) {\n\t\tvar high = \"0x\" + escape.slice( 1 ) - 0x10000;\n\n\t\tif ( nonHex ) {\n\n\t\t\t// Strip the backslash prefix from a non-hex escape sequence\n\t\t\treturn nonHex;\n\t\t}\n\n\t\t// Replace a hexadecimal escape sequence with the encoded Unicode code point\n\t\t// Support: IE <=11+\n\t\t// For values outside the Basic Multilingual Plane (BMP), manually construct a\n\t\t// surrogate pair\n\t\treturn high < 0 ?\n\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t};\n\nfunction unescapeSelector( sel ) {\n\treturn sel.replace( runescape, funescape );\n}\n\nfunction selectorError( msg ) {\n\tjQuery.error( \"Syntax error, unrecognized expression: \" + msg );\n}\n\nvar rcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" );\n\nvar tokenCache = createCache();\n\nfunction tokenize( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = jQuery.expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || ( match = rcomma.exec( soFar ) ) ) {\n\t\t\tif ( match ) {\n\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[ 0 ].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( ( tokens = [] ) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( ( match = rleadingCombinator.exec( soFar ) ) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push( {\n\t\t\t\tvalue: matched,\n\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[ 0 ].replace( rtrimCSS, \" \" )\n\t\t\t} );\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in filterMatchExpr ) {\n\t\t\tif ( ( match = jQuery.expr.match[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||\n\t\t\t\t( match = preFilters[ type ]( match ) ) ) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push( {\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t} );\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\tif ( parseOnly ) {\n\t\treturn soFar.length;\n\t}\n\n\treturn soFar ?\n\t\tselectorError( selector ) :\n\n\t\t// Cache the tokens\n\t\ttokenCache( selector, groups ).slice( 0 );\n}\n\nvar preFilter = {\n\tATTR: function( match ) {\n\t\tmatch[ 1 ] = unescapeSelector( match[ 1 ] );\n\n\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\tmatch[ 3 ] = unescapeSelector( match[ 3 ] || match[ 4 ] || match[ 5 ] || \"\" );\n\n\t\tif ( match[ 2 ] === \"~=\" ) {\n\t\t\tmatch[ 3 ] = \" \" + match[ 3 ] + \" \";\n\t\t}\n\n\t\treturn match.slice( 0, 4 );\n\t},\n\n\tCHILD: function( match ) {\n\n\t\t/* matches from filterMatchExpr[\"CHILD\"]\n\t\t\t1 type (only|nth|...)\n\t\t\t2 what (child|of-type)\n\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t5 sign of xn-component\n\t\t\t6 x of xn-component\n\t\t\t7 sign of y-component\n\t\t\t8 y of y-component\n\t\t*/\n\t\tmatch[ 1 ] = match[ 1 ].toLowerCase();\n\n\t\tif ( match[ 1 ].slice( 0, 3 ) === \"nth\" ) {\n\n\t\t\t// nth-* requires argument\n\t\t\tif ( !match[ 3 ] ) {\n\t\t\t\tselectorError( match[ 0 ] );\n\t\t\t}\n\n\t\t\t// numeric x and y parameters for jQuery.expr.filter.CHILD\n\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\tmatch[ 4 ] = +( match[ 4 ] ?\n\t\t\t\tmatch[ 5 ] + ( match[ 6 ] || 1 ) :\n\t\t\t\t2 * ( match[ 3 ] === \"even\" || match[ 3 ] === \"odd\" )\n\t\t\t);\n\t\t\tmatch[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === \"odd\" );\n\n\t\t// other types prohibit arguments\n\t\t} else if ( match[ 3 ] ) {\n\t\t\tselectorError( match[ 0 ] );\n\t\t}\n\n\t\treturn match;\n\t},\n\n\tPSEUDO: function( match ) {\n\t\tvar excess,\n\t\t\tunquoted = !match[ 6 ] && match[ 2 ];\n\n\t\tif ( filterMatchExpr.CHILD.test( match[ 0 ] ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Accept quoted arguments as-is\n\t\tif ( match[ 3 ] ) {\n\t\t\tmatch[ 2 ] = match[ 4 ] || match[ 5 ] || \"\";\n\n\t\t// Strip excess characters from unquoted arguments\n\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\n\t\t\t// Get excess from tokenize (recursively)\n\t\t\t( excess = tokenize( unquoted, true ) ) &&\n\n\t\t\t// advance to the next closing parenthesis\n\t\t\t( excess = unquoted.indexOf( \")\", unquoted.length - excess ) -\n\t\t\t\tunquoted.length ) ) {\n\n\t\t\t// excess is a negative index\n\t\t\tmatch[ 0 ] = match[ 0 ].slice( 0, excess );\n\t\t\tmatch[ 2 ] = unquoted.slice( 0, excess );\n\t\t}\n\n\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\treturn match.slice( 0, 3 );\n\t}\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[ i ].value;\n\t}\n\treturn selector;\n}\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nfunction access( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( typeof value !== \"function\" ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, _key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\t\tvalue :\n\t\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n}\n\n// Only count HTML whitespace\n// Other whitespace should count in values\n// https://infra.spec.whatwg.org/#ascii-whitespace\nvar rnothtmlwhite = /[^\\x20\\t\\r\\n\\f]+/g;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ||\n\n\t\t\t\t// For compat with previous handling of boolean attributes,\n\t\t\t\t// remove when `false` passed. For ARIA attributes -\n\t\t\t\t// many of which recognize a `\"false\"` value - continue to\n\t\t\t\t// set the `\"false\"` value as jQuery <4 did.\n\t\t\t\t( value === false && name.toLowerCase().indexOf( \"aria-\" ) !== 0 ) ) {\n\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = elem.getAttribute( name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Support: IE <=11+\n// An input loses its value after becoming a radio\nif ( isIE ) {\n\tjQuery.attrHooks.type = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( value === \"radio\" && nodeName( elem, \"input\" ) ) {\n\t\t\t\tvar val = elem.value;\n\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\tif ( val ) {\n\t\t\t\t\telem.value = val;\n\t\t\t\t}\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t};\n}\n\n// CSS string/identifier serialization\n// https://drafts.csswg.org/cssom/#common-serializing-idioms\nvar rcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\x80-\\uFFFF\\w-]/g;\n\nfunction fcssescape( ch, asCodePoint ) {\n\tif ( asCodePoint ) {\n\n\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\tif ( ch === \"\\0\" ) {\n\t\t\treturn \"\\uFFFD\";\n\t\t}\n\n\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t}\n\n\t// Other potentially-special ASCII characters get backslash-escaped\n\treturn \"\\\\\" + ch;\n}\n\njQuery.escapeSelector = function( sel ) {\n\treturn ( sel + \"\" ).replace( rcssescape, fcssescape );\n};\n\nvar sort = arr.sort;\n\nvar splice = arr.splice;\n\nvar hasDuplicate;\n\n// Document order sorting\nfunction sortOrder( a, b ) {\n\n\t// Flag for duplicate removal\n\tif ( a === b ) {\n\t\thasDuplicate = true;\n\t\treturn 0;\n\t}\n\n\t// Sort on method existence if only one input has compareDocumentPosition\n\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\tif ( compare ) {\n\t\treturn compare;\n\t}\n\n\t// Calculate position if both inputs belong to the same document\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tcompare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?\n\t\ta.compareDocumentPosition( b ) :\n\n\t\t// Otherwise we know they are disconnected\n\t\t1;\n\n\t// Disconnected nodes\n\tif ( compare & 1 ) {\n\n\t\t// Choose the first element that is related to the document\n\t\t// Support: IE 11+\n\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t// two documents; shallow comparisons work.\n\t\t// eslint-disable-next-line eqeqeq\n\t\tif ( a == document$1 || a.ownerDocument == document$1 &&\n\t\t\tjQuery.contains( document$1, a ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t// Support: IE 11+\n\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t// two documents; shallow comparisons work.\n\t\t// eslint-disable-next-line eqeqeq\n\t\tif ( b == document$1 || b.ownerDocument == document$1 &&\n\t\t\tjQuery.contains( document$1, b ) ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\t// Maintain original order\n\t\treturn 0;\n\t}\n\n\treturn compare & 4 ? -1 : 1;\n}\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\njQuery.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\thasDuplicate = false;\n\n\tsort.call( results, sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( ( elem = results[ i++ ] ) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tsplice.call( results, duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\treturn results;\n};\n\njQuery.fn.uniqueSort = function() {\n\treturn this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) );\n};\n\nvar i,\n\toutermostContext,\n\n\t// Local document vars\n\tdocument,\n\tdocumentElement,\n\tdocumentIsHTML,\n\n\t// Instance-specific data\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\tcompilerCache = createCache(),\n\tnonnativeSelectorCache = createCache(),\n\n\t// Regular expressions\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = jQuery.extend( {\n\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\tneedsContext: new RegExp( \"^\" + whitespace +\n\t\t\t\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" + whitespace +\n\t\t\t\"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t}, filterMatchExpr ),\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr$1 = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\t// Used for iframes; see `setDocument`.\n\t// Support: IE 9 - 11+\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE.\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tinDisabledFieldset = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && nodeName( elem, \"fieldset\" );\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\nfunction find( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\t\tsetDocument( context );\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && ( match = rquickExpr$1.exec( selector ) ) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( ( m = match[ 1 ] ) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( ( elem = context.getElementById( m ) ) ) {\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn results;\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif ( newContext && ( elem = newContext.getElementById( m ) ) &&\n\t\t\t\t\t\t\tjQuery.contains( context, elem ) ) {\n\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[ 2 ] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( !nonnativeSelectorCache[ selector + \" \" ] &&\n\t\t\t\t( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) {\n\n\t\t\t\tnewSelector = selector;\n\t\t\t\tnewContext = context;\n\n\t\t\t\t// qSA considers elements outside a scoping root when evaluating child or\n\t\t\t\t// descendant combinators, which is not what we want.\n\t\t\t\t// In such cases, we work around the behavior by prefixing every selector in the\n\t\t\t\t// list with an ID selector referencing the scope context.\n\t\t\t\t// The technique has to be used as well when a leading combinator is used\n\t\t\t\t// as such selectors are not recognized by querySelectorAll.\n\t\t\t\t// Thanks to Andrew Dupont for this technique.\n\t\t\t\tif ( nodeType === 1 &&\n\t\t\t\t\t( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) &&\n\t\t\t\t\t\ttestContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\n\t\t\t\t\t// Outside of IE, if we're not changing the context we can\n\t\t\t\t\t// use :scope instead of an ID.\n\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\t\tif ( newContext != context || isIE ) {\n\n\t\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\t\tif ( ( nid = context.getAttribute( \"id\" ) ) ) {\n\t\t\t\t\t\t\tnid = jQuery.escapeSelector( nid );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontext.setAttribute( \"id\", ( nid = jQuery.expando ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[ i ] = ( nid ? \"#\" + nid : \":scope\" ) + \" \" +\n\t\t\t\t\t\t\ttoSelector( groups[ i ] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\tnonnativeSelectorCache( selector, true );\n\t\t\t\t} finally {\n\t\t\t\t\tif ( nid === jQuery.expando ) {\n\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrimCSS, \"$1\" ), context, results, seed );\n}\n\n/**\n * Mark a function for special use by jQuery selector module\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ jQuery.expando ] = true;\n\treturn fn;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\treturn nodeName( elem, \"input\" ) && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\treturn ( nodeName( elem, \"input\" ) || nodeName( elem, \"button\" ) ) &&\n\t\t\telem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11+\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tinDisabledFieldset( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction( function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction( function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ ( j = matchIndexes[ i ] ) ] ) {\n\t\t\t\t\tseed[ j ] = !( matches[ j ] = seed[ j ] );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t} );\n}\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [node] An element or document object to use to set the document\n */\nfunction setDocument( node ) {\n\tvar subWindow,\n\t\tdoc = node ? node.ownerDocument || node : document$1;\n\n\t// Return early if doc is invalid or already selected\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( doc == document || doc.nodeType !== 9 ) {\n\t\treturn;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocumentElement = document.documentElement;\n\tdocumentIsHTML = !jQuery.isXMLDoc( document );\n\n\t// Support: IE 9 - 11+\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (see trac-13936)\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( isIE && document$1 != document &&\n\t\t( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {\n\t\tsubWindow.addEventListener( \"unload\", unloadHandler );\n\t}\n}\n\nfind.matches = function( expr, elements ) {\n\treturn find( expr, null, null, elements );\n};\n\nfind.matchesSelector = function( elem, expr ) {\n\tsetDocument( elem );\n\n\tif ( documentIsHTML &&\n\t\t!nonnativeSelectorCache[ expr + \" \" ] &&\n\t\t( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\treturn matches.call( elem, expr );\n\t\t} catch ( e ) {\n\t\t\tnonnativeSelectorCache( expr, true );\n\t\t}\n\t}\n\n\treturn find( expr, document, null, [ elem ] ).length > 0;\n};\n\njQuery.expr = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tfind: {\n\t\tID: function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t},\n\n\t\tTAG: function( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t},\n\n\t\tCLASS: function( className, context ) {\n\t\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\t\treturn context.getElementsByClassName( className );\n\t\t\t}\n\t\t}\n\t},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: preFilter,\n\n\tfilter: {\n\t\tID: function( id ) {\n\t\t\tvar attrId = unescapeSelector( id );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute( \"id\" ) === attrId;\n\t\t\t};\n\t\t},\n\n\t\tTAG: function( nodeNameSelector ) {\n\t\t\tvar expectedNodeName = unescapeSelector( nodeNameSelector ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\n\t\t\t\tfunction() {\n\t\t\t\t\treturn true;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn nodeName( elem, expectedNodeName );\n\t\t\t\t};\n\t\t},\n\n\t\tCLASS: function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t( pattern = new RegExp( \"(^|\" + whitespace + \")\" + className +\n\t\t\t\t\t\"(\" + whitespace + \"|$)\" ) ) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test(\n\t\t\t\t\t\ttypeof elem.className === \"string\" && elem.className ||\n\t\t\t\t\t\t\ttypeof elem.getAttribute !== \"undefined\" &&\n\t\t\t\t\t\t\t\telem.getAttribute( \"class\" ) ||\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t);\n\t\t\t\t} );\n\t\t},\n\n\t\tATTR: function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = jQuery.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\tif ( operator === \"=\" ) {\n\t\t\t\t\treturn result === check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"!=\" ) {\n\t\t\t\t\treturn result !== check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"^=\" ) {\n\t\t\t\t\treturn check && result.indexOf( check ) === 0;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"*=\" ) {\n\t\t\t\t\treturn check && result.indexOf( check ) > -1;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"$=\" ) {\n\t\t\t\t\treturn check && result.slice( -check.length ) === check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"~=\" ) {\n\t\t\t\t\treturn ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" )\n\t\t\t\t\t\t.indexOf( check ) > -1;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"|=\" ) {\n\t\t\t\t\treturn result === check || result.slice( 0, check.length + 1 ) === check + \"-\";\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\t\t\t};\n\t\t},\n\n\t\tCHILD: function( type, what, _argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tvar cache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( ( node = node[ dir ] ) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnodeName( node, name ) :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\t\t\t\t\t\t\touterCache = parent[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t( parent[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\touterCache = elem[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t\t( elem[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnodeName( node, name ) :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t( node[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\tPSEUDO: function( pseudo, argument ) {\n\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// https://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar fn = jQuery.expr.pseudos[ pseudo ] ||\n\t\t\t\tjQuery.expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\tselectorError( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as jQuery does\n\t\t\tif ( fn[ jQuery.expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\n\t\t// Potentially complex pseudos\n\t\tnot: markFunction( function( selector ) {\n\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrimCSS, \"$1\" ) );\n\n\t\t\treturn matcher[ jQuery.expando ] ?\n\t\t\t\tmarkFunction( function( seed, matches, _context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\t\t\t\t\tseed[ i ] = !( matches[ i ] = elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} ) :\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tinput[ 0 ] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\n\t\t\t\t\t// Don't keep the element\n\t\t\t\t\t// (see https://github.com/jquery/sizzle/issues/299)\n\t\t\t\t\tinput[ 0 ] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t} ),\n\n\t\thas: markFunction( function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn find( selector, elem ).length > 0;\n\t\t\t};\n\t\t} ),\n\n\t\tcontains: markFunction( function( text ) {\n\t\t\ttext = unescapeSelector( text );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t} ),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// https://www.w3.org/TR/selectors/#lang-pseudo\n\t\tlang: markFunction( function( lang ) {\n\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test( lang || \"\" ) ) {\n\t\t\t\tselectorError( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = unescapeSelector( lang ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( ( elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute( \"xml:lang\" ) || elem.getAttribute( \"lang\" ) ) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t} ),\n\n\t\t// Miscellaneous\n\t\ttarget: function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\troot: function( elem ) {\n\t\t\treturn elem === documentElement;\n\t\t},\n\n\t\tfocus: function( elem ) {\n\t\t\treturn elem === document.activeElement &&\n\t\t\t\tdocument.hasFocus() &&\n\t\t\t\t!!( elem.type || elem.href || ~elem.tabIndex );\n\t\t},\n\n\t\t// Boolean properties\n\t\tenabled: createDisabledPseudo( false ),\n\t\tdisabled: createDisabledPseudo( true ),\n\n\t\tchecked: function( elem ) {\n\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\treturn ( nodeName( elem, \"input\" ) && !!elem.checked ) ||\n\t\t\t\t( nodeName( elem, \"option\" ) && !!elem.selected );\n\t\t},\n\n\t\tselected: function( elem ) {\n\n\t\t\t// Support: IE <=11+\n\t\t\t// Accessing the selectedIndex property\n\t\t\t// forces the browser to treat the default option as\n\t\t\t// selected when in an optgroup.\n\t\t\tif ( isIE && elem.parentNode ) {\n\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\tempty: function( elem ) {\n\n\t\t\t// https://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tparent: function( elem ) {\n\t\t\treturn !jQuery.expr.pseudos.empty( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\theader: function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\tinput: function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\tbutton: function( elem ) {\n\t\t\treturn nodeName( elem, \"input\" ) && elem.type === \"button\" ||\n\t\t\t\tnodeName( elem, \"button\" );\n\t\t},\n\n\t\ttext: function( elem ) {\n\t\t\treturn nodeName( elem, \"input\" ) && elem.type === \"text\";\n\t\t},\n\n\t\t// Position-in-collection\n\t\tfirst: createPositionalPseudo( function() {\n\t\t\treturn [ 0 ];\n\t\t} ),\n\n\t\tlast: createPositionalPseudo( function( _matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t} ),\n\n\t\teq: createPositionalPseudo( function( _matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t} ),\n\n\t\teven: createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\todd: createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\tlt: createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i;\n\n\t\t\tif ( argument < 0 ) {\n\t\t\t\ti = argument + length;\n\t\t\t} else if ( argument > length ) {\n\t\t\t\ti = length;\n\t\t\t} else {\n\t\t\t\ti = argument;\n\t\t\t}\n\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\tgt: createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} )\n\t}\n};\n\njQuery.expr.pseudos.nth = jQuery.expr.pseudos.eq;\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tjQuery.expr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tjQuery.expr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = jQuery.expr.pseudos;\njQuery.expr.setFilters = new setFilters();\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ jQuery.expando ] || ( elem[ jQuery.expando ] = {} );\n\n\t\t\t\t\t\tif ( skip && nodeName( elem, skip ) ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( ( oldCache = outerCache[ key ] ) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn ( newCache[ 2 ] = oldCache[ 2 ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\touterCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[ i ]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[ 0 ];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tfind( selector, contexts[ i ], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ jQuery.expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ jQuery.expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction( function( seed, results, context, xml ) {\n\t\tvar temp, i, elem, matcherOut,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed ||\n\t\t\t\tmultipleContexts( selector || \"*\",\n\t\t\t\t\tcontext.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems;\n\n\t\tif ( matcher ) {\n\n\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter\n\t\t\t// or preexisting results,\n\t\t\tmatcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t[] :\n\n\t\t\t\t// ...otherwise use results directly\n\t\t\t\tresults;\n\n\t\t\t// Find primary matches\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t} else {\n\t\t\tmatcherOut = matcherIn;\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( ( elem = temp[ i ] ) ) {\n\t\t\t\t\tmatcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) ) {\n\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( ( matcherIn[ i ] = elem ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, ( matcherOut = [] ), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) &&\n\t\t\t\t\t\t( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) {\n\n\t\t\t\t\t\tseed[ temp ] = !( results[ temp ] = elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t} );\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = jQuery.expr.relative[ tokens[ 0 ].type ],\n\t\timplicitRelative = leadingRelative || jQuery.expr.relative[ \" \" ],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf.call( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\n\t\t\t// Support: IE 11+\n\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\tvar ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || (\n\t\t\t\t( checkContext = context ).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\n\t\t\t// Avoid hanging onto element\n\t\t\t// (see https://github.com/jquery/sizzle/issues/299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( matcher = jQuery.expr.relative[ tokens[ i ].type ] ) ) {\n\t\t\tmatchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];\n\t\t} else {\n\t\t\tmatcher = jQuery.expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ jQuery.expando ] ) {\n\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( jQuery.expr.relative[ tokens[ j ].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 )\n\t\t\t\t\t\t\t.concat( { value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" } )\n\t\t\t\t\t).replace( rtrimCSS, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && jQuery.expr.find.TAG( \"*\", outermost ),\n\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 );\n\n\t\t\tif ( outermost ) {\n\n\t\t\t\t// Support: IE 11+\n\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\toutermostContext = context == document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\tfor ( ; ( elem = elems[ i ] ) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\n\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\t\tif ( !context && elem.ownerDocument != document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( ( matcher = elementMatchers[ j++ ] ) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml ) ) {\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( ( elem = !matcher && elem ) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( ( matcher = setMatchers[ j++ ] ) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !( unmatched[ i ] || setMatched[ i ] ) ) {\n\t\t\t\t\t\t\t\tsetMatched[ i ] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tjQuery.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\nfunction compile( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[ i ] );\n\t\t\tif ( cached[ jQuery.expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector,\n\t\t\tmatcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n}\n\n/**\n * A low-level selection function that works with jQuery's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with jQuery selector compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nfunction select( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( ( selector = compiled.selector || selector ) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[ 0 ] = match[ 0 ].slice( 0 );\n\t\tif ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML &&\n\t\t\t\tjQuery.expr.relative[ tokens[ 1 ].type ] ) {\n\n\t\t\tcontext = ( jQuery.expr.find.ID(\n\t\t\t\tunescapeSelector( token.matches[ 0 ] ),\n\t\t\t\tcontext\n\t\t\t) || [] )[ 0 ];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr.needsContext.test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[ i ];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( jQuery.expr.relative[ ( type = token.type ) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( ( find = jQuery.expr.find[ type ] ) ) {\n\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( ( seed = find(\n\t\t\t\t\tunescapeSelector( token.matches[ 0 ] ),\n\t\t\t\t\trsibling.test( tokens[ 0 ].type ) &&\n\t\t\t\t\t\ttestContext( context.parentNode ) || context\n\t\t\t\t) ) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n}\n\n// Initialize against the default document\nsetDocument();\n\njQuery.find = find;\n\n// These have always been private, but they used to be documented as part of\n// Sizzle so let's maintain them for now for backwards compatibility purposes.\nfind.compile = compile;\nfind.select = select;\nfind.setDocument = setDocument;\nfind.tokenize = tokenize;\n\nfunction dir( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n}\n\nfunction siblings( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n}\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n// rsingleTag matches a string consisting of a single HTML element with no attributes\n// and captures the element's name\nvar rsingleTag = /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;\n\nfunction isObviousHtml( input ) {\n\treturn input[ 0 ] === \"<\" &&\n\t\tinput[ input.length - 1 ] === \">\" &&\n\t\tinput.length >= 3;\n}\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( typeof qualifier === \"function\" ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n// Initialize a jQuery object\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521)\n\t// Strict HTML recognition (trac-11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\tif ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( typeof selector === \"function\" ) {\n\t\t\treturn rootjQuery.ready !== undefined ?\n\t\t\t\trootjQuery.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\n\t\t} else {\n\n\t\t\t// Handle obvious HTML strings\n\t\t\tmatch = selector + \"\";\n\t\t\tif ( isObviousHtml( match ) ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip\n\t\t\t\t// the regex check. This also handles browser-supported HTML wrappers\n\t\t\t\t// like TrustedHTML.\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t// Handle HTML strings or selectors\n\t\t\t} else if ( typeof selector === \"string\" ) {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t} else {\n\t\t\t\treturn jQuery.makeArray( selector, this );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\t// Note: match[1] may be a string or a TrustedHTML wrapper\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document$1,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( typeof this[ match ] === \"function\" ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document$1.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr) & $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || rootjQuery ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\t\t}\n\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document$1 );\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to jQuery#find\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\tif ( elem.contentDocument != null &&\n\n\t\t\t// Support: IE 11+\n\t\t\t// <object> elements with no `data` attribute has an object\n\t\t\t// `contentDocument` with a `null` prototype.\n\t\t\tgetProto( elem.contentDocument ) ) {\n\n\t\t\treturn elem.contentDocument;\n\t\t}\n\n\t\t// Support: IE 9 - 11+\n\t\t// Treat the template element as a regular one in browsers that\n\t\t// don't support it.\n\t\tif ( nodeName( elem, \"template\" ) ) {\n\t\t\telem = elem.content || elem;\n\t\t}\n\n\t\treturn jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\n\n// Matches dashed string for camelizing\nvar rdashAlpha = /-([a-z])/g;\n\n// Used by camelCase as callback to replace()\nfunction fcamelCase( _all, letter ) {\n\treturn letter.toUpperCase();\n}\n\n// Convert dashed to camelCase\nfunction camelCase( string ) {\n\treturn string.replace( rdashAlpha, fcamelCase );\n}\n\n/**\n * Determines whether an object can have data\n */\nfunction acceptData( owner ) {\n\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n}\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = Object.create( null );\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see trac-8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( camelCase );\n\t\t\t} else {\n\t\t\t\tkey = camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\n\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t\t// The attrs elements can be null (trac-14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// Use proper attribute retrieval (trac-12072)\n\t\t\t\tvar tabindex = elem.getAttribute( \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\n\t\t\t\t\t// href-less anchor's `tabIndex` property value is `0` and\n\t\t\t\t\t// the `tabindex` attribute value: `null`. We want `-1`.\n\t\t\t\t\trclickable.test( elem.nodeName ) && elem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11+\n// Accessing the selectedIndex property forces the browser to respect\n// setting selected on the option. The getter ensures a default option\n// is selected when in an optgroup. ESLint rule \"no-unused-expressions\"\n// is disabled for this code since it considers such accessions noop.\nif ( isIE ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n// Strip and collapse whitespace according to HTML spec\n// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace\nfunction stripAndCollapse( value ) {\n\tvar tokens = value.match( rnothtmlwhite ) || [];\n\treturn tokens.join( \" \" );\n}\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\nfunction classesToArray( value ) {\n\tif ( Array.isArray( value ) ) {\n\t\treturn value;\n\t}\n\tif ( typeof value === \"string\" ) {\n\t\treturn value.match( rnothtmlwhite ) || [];\n\t}\n\treturn [];\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classNames, cur, curValue, className, i, finalValue;\n\n\t\tif ( typeof value === \"function\" ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\tif ( classNames.length ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tcurValue = getClass( this );\n\t\t\t\tcur = this.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\t\tclassName = classNames[ i ];\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + className + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += className + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\tthis.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classNames, cur, curValue, className, i, finalValue;\n\n\t\tif ( typeof value === \"function\" ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\tif ( classNames.length ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tcurValue = getClass( this );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = this.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\t\tclassName = classNames[ i ];\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + className + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + className + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\tthis.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar classNames, className, i, self;\n\n\t\tif ( typeof value === \"function\" ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\tif ( typeof stateVal === \"boolean\" ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\tif ( classNames.length ) {\n\t\t\treturn this.each( function() {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\tself = jQuery( this );\n\n\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\tclassName = classNames[ i ];\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, valueIsFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvalueIsFunction = typeof value === \"function\";\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\tif ( option.selected &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\tif ( ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery( option ).val(), values ) > -1\n\t\t\t\t\t) ) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\nif ( isIE ) {\n\tjQuery.valHooks.option = {\n\t\tget: function( elem ) {\n\n\t\t\tvar val = elem.getAttribute( \"value\" );\n\t\t\treturn val != null ?\n\t\t\t\tval :\n\n\t\t\t\t// Support: IE <=10 - 11+\n\t\t\t\t// option.text throws exceptions (trac-14686, trac-14858)\n\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t}\n\t};\n}\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n} );\n\nvar rcheckableType = /^(?:checkbox|radio)$/i;\n\nvar rtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Only attach events to objects that accept data\n\t\tif ( !acceptData( elem ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement$1, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = Object.create( null );\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\n\t\t\t// Make a writable jQuery.Event from the native event object\n\t\t\tevent = jQuery.event.fix( nativeEvent ),\n\n\t\t\thandlers = (\n\t\t\t\tdataPriv.get( this, \"events\" ) || Object.create( null )\n\t\t\t)[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// If the event is namespaced, then each handler is only invoked if it is\n\t\t\t\t// specially universal or its namespaces are a superset of the event's.\n\t\t\t\tif ( !event.rnamespace || handleObj.namespace === false ||\n\t\t\t\t\tevent.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: Firefox <=42 - 66+\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11+\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (trac-13208)\n\t\t\t\t// Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (trac-13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: typeof hook === \"function\" ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: jQuery.extend( Object.create( null ), {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tclick: {\n\n\t\t\t// Utilize native event to ensure correct state for checkable inputs\n\t\t\tsetup: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Claim the first handler\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\t// dataPriv.set( el, \"click\", ... )\n\t\t\t\t\tleverageNative( el, \"click\", true );\n\t\t\t\t}\n\n\t\t\t\t// Return false to allow normal processing in the caller\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\ttrigger: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Force setup before triggering a click\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\tleverageNative( el, \"click\" );\n\t\t\t\t}\n\n\t\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, suppress native .click() on links\n\t\t\t// Also prevent it if we're currently inside a leveraged native-event stack\n\t\t\t_default: function( event ) {\n\t\t\t\tvar target = event.target;\n\t\t\t\treturn rcheckableType.test( target.type ) &&\n\t\t\t\t\ttarget.click && nodeName( target, \"input\" ) &&\n\t\t\t\t\tdataPriv.get( target, \"click\" ) ||\n\t\t\t\t\tnodeName( target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\t\t\t\tif ( event.result !== undefined ) {\n\n\t\t\t\t\t// Setting `event.originalEvent.returnValue` in modern\n\t\t\t\t\t// browsers does the same as just calling `preventDefault()`,\n\t\t\t\t\t// the browsers ignore the value anyway.\n\t\t\t\t\t// Incidentally, IE 11 is the only browser from our supported\n\t\t\t\t\t// ones which respects the value returned from a `beforeunload`\n\t\t\t\t\t// handler attached by `addEventListener`; other browsers do\n\t\t\t\t\t// so only for inline handlers, so not setting the value\n\t\t\t\t\t// directly shouldn't reduce any functionality.\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} )\n};\n\n// Ensure the presence of an event listener that handles manually-triggered\n// synthetic events by interrupting progress until reinvoked in response to\n// *native* events that it fires directly, ensuring that state changes have\n// already occurred before other listeners are invoked.\nfunction leverageNative( el, type, isSetup ) {\n\n\t// Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add\n\tif ( !isSetup ) {\n\t\tif ( dataPriv.get( el, type ) === undefined ) {\n\t\t\tjQuery.event.add( el, type, returnTrue );\n\t\t}\n\t\treturn;\n\t}\n\n\t// Register the controller as a special universal handler for all event namespaces\n\tdataPriv.set( el, type, false );\n\tjQuery.event.add( el, type, {\n\t\tnamespace: false,\n\t\thandler: function( event ) {\n\t\t\tvar result,\n\t\t\t\tsaved = dataPriv.get( this, type );\n\n\t\t\t// This controller function is invoked under multiple circumstances,\n\t\t\t// differentiated by the stored value in `saved`:\n\t\t\t// 1. For an outer synthetic `.trigger()`ed event (detected by\n\t\t\t//    `event.isTrigger & 1` and non-array `saved`), it records arguments\n\t\t\t//    as an array and fires an [inner] native event to prompt state\n\t\t\t//    changes that should be observed by registered listeners (such as\n\t\t\t//    checkbox toggling and focus updating), then clears the stored value.\n\t\t\t// 2. For an [inner] native event (detected by `saved` being\n\t\t\t//    an array), it triggers an inner synthetic event, records the\n\t\t\t//    result, and preempts propagation to further jQuery listeners.\n\t\t\t// 3. For an inner synthetic event (detected by `event.isTrigger & 1` and\n\t\t\t//    array `saved`), it prevents double-propagation of surrogate events\n\t\t\t//    but otherwise allows everything to proceed (particularly including\n\t\t\t//    further listeners).\n\t\t\t// Possible `saved` data shapes: `[...], `{ value }`, `false`.\n\t\t\tif ( ( event.isTrigger & 1 ) && this[ type ] ) {\n\n\t\t\t\t// Interrupt processing of the outer synthetic .trigger()ed event\n\t\t\t\tif ( !saved.length ) {\n\n\t\t\t\t\t// Store arguments for use when handling the inner native event\n\t\t\t\t\t// There will always be at least one argument (an event object),\n\t\t\t\t\t// so this array will not be confused with a leftover capture object.\n\t\t\t\t\tsaved = slice.call( arguments );\n\t\t\t\t\tdataPriv.set( this, type, saved );\n\n\t\t\t\t\t// Trigger the native event and capture its result\n\t\t\t\t\tthis[ type ]();\n\t\t\t\t\tresult = dataPriv.get( this, type );\n\t\t\t\t\tdataPriv.set( this, type, false );\n\n\t\t\t\t\tif ( saved !== result ) {\n\n\t\t\t\t\t\t// Cancel the outer synthetic event\n\t\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\t\tevent.preventDefault();\n\n\t\t\t\t\t\t// Support: Chrome 86+\n\t\t\t\t\t\t// In Chrome, if an element having a focusout handler is\n\t\t\t\t\t\t// blurred by clicking outside of it, it invokes the handler\n\t\t\t\t\t\t// synchronously. If that handler calls `.remove()` on\n\t\t\t\t\t\t// the element, the data is cleared, leaving `result`\n\t\t\t\t\t\t// undefined. We need to guard against this.\n\t\t\t\t\t\treturn result && result.value;\n\t\t\t\t\t}\n\n\t\t\t\t// If this is an inner synthetic event for an event with a bubbling\n\t\t\t\t// surrogate (focus or blur), assume that the surrogate already\n\t\t\t\t// propagated from triggering the native event and prevent that\n\t\t\t\t// from happening again here.\n\t\t\t\t} else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t}\n\n\t\t\t// If this is a native event triggered above, everything is now in order.\n\t\t\t// Fire an inner synthetic event with the original arguments.\n\t\t\t} else if ( saved.length ) {\n\n\t\t\t\t// ...and capture the result\n\t\t\t\tdataPriv.set( this, type, {\n\t\t\t\t\tvalue: jQuery.event.trigger(\n\t\t\t\t\t\tsaved[ 0 ],\n\t\t\t\t\t\tsaved.slice( 1 ),\n\t\t\t\t\t\tthis\n\t\t\t\t\t)\n\t\t\t\t} );\n\n\t\t\t\t// Abort handling of the native event by all jQuery handlers while allowing\n\t\t\t\t// native handlers on the same element to run. On target, this is achieved\n\t\t\t\t// by stopping immediate propagation just on the jQuery event. However,\n\t\t\t\t// the native event is re-wrapped by a jQuery one on each level of the\n\t\t\t\t// propagation so the only way to stop it for jQuery is to stop it for\n\t\t\t\t// everyone via native `stopPropagation()`. This is not a problem for\n\t\t\t\t// focus/blur which don't bubble, but it does also stop click on checkboxes\n\t\t\t\t// and radios. We accept this limitation.\n\t\t\t\tevent.stopPropagation();\n\t\t\t\tevent.isImmediatePropagationStopped = returnTrue;\n\t\t\t}\n\t\t}\n\t} );\n}\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\tthis.target = src.target;\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || Date.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcode: true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\twhich: true\n}, jQuery.event.addProp );\n\njQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( type, delegateType ) {\n\n\t// Support: IE 11+\n\t// Attach a single focusin/focusout handler on the document while someone wants focus/blur.\n\t// This is because the former are synchronous in IE while the latter are async. In other\n\t// browsers, all those handlers are invoked synchronously.\n\tfunction focusMappedHandler( nativeEvent ) {\n\n\t\t// `eventHandle` would already wrap the event, but we need to change the `type` here.\n\t\tvar event = jQuery.event.fix( nativeEvent );\n\t\tevent.type = nativeEvent.type === \"focusin\" ? \"focus\" : \"blur\";\n\t\tevent.isSimulated = true;\n\n\t\t// focus/blur don't bubble while focusin/focusout do; simulate the former by only\n\t\t// invoking the handler at the lower level.\n\t\tif ( event.target === event.currentTarget ) {\n\n\t\t\t// The setup part calls `leverageNative`, which, in turn, calls\n\t\t\t// `jQuery.event.add`, so event handle will already have been set\n\t\t\t// by this point.\n\t\t\tdataPriv.get( this, \"handle\" )( event );\n\t\t}\n\t}\n\n\tjQuery.event.special[ type ] = {\n\n\t\t// Utilize native event if possible so blur/focus sequence is correct\n\t\tsetup: function() {\n\n\t\t\t// Claim the first handler\n\t\t\t// dataPriv.set( this, \"focus\", ... )\n\t\t\t// dataPriv.set( this, \"blur\", ... )\n\t\t\tleverageNative( this, type, true );\n\n\t\t\tif ( isIE ) {\n\t\t\t\tthis.addEventListener( delegateType, focusMappedHandler );\n\t\t\t} else {\n\n\t\t\t\t// Return false to allow normal processing in the caller\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\ttrigger: function() {\n\n\t\t\t// Force setup before trigger\n\t\t\tleverageNative( this, type );\n\n\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\treturn true;\n\t\t},\n\n\t\tteardown: function() {\n\t\t\tif ( isIE ) {\n\t\t\t\tthis.removeEventListener( delegateType, focusMappedHandler );\n\t\t\t} else {\n\n\t\t\t\t// Return false to indicate standard teardown should be applied\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\t// Suppress native focus or blur if we're currently inside\n\t\t// a leveraged native-event stack\n\t\t_default: function( event ) {\n\t\t\treturn dataPriv.get( event.target, type );\n\t\t},\n\n\t\tdelegateType: delegateType\n\t};\n} );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\tstopPropagationCallback = function( e ) {\n\t\te.stopPropagation();\n\t};\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special, lastElement,\n\t\t\teventPath = [ elem || document$1 ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = lastElement = tmp = elem = elem || document$1;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (trac-9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document$1 ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tlastElement = cur;\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || Object.create( null ) )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (trac-6170)\n\t\t\t\tif ( ontype && typeof elem[ type ] === \"function\" && !isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.addEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\telem[ type ]();\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.removeEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\nvar isAttached = function( elem ) {\n\t\treturn jQuery.contains( elem.ownerDocument, elem ) ||\n\t\t\telem.getRootNode( composed ) === elem.ownerDocument;\n\t},\n\tcomposed = { composed: true };\n\n// Support: IE 9 - 11+\n// Check attachment across shadow DOM boundaries when possible (gh-3504).\n// Provide a fallback for browsers without Shadow DOM v1 support.\nif ( !documentElement$1.getRootNode ) {\n\tisAttached = function( elem ) {\n\t\treturn jQuery.contains( elem.ownerDocument, elem );\n\t};\n}\n\n// rtagName captures the name from the first start tag in a string of HTML\n// https://html.spec.whatwg.org/multipage/syntax.html#tag-open-state\n// https://html.spec.whatwg.org/multipage/syntax.html#tag-name-state\nvar rtagName = /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)/i;\n\nvar wrapMap = {\n\n\t// Table parts need to be wrapped with `<table>` or they're\n\t// stripped to their contents when put in a div.\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do, so we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ \"table\" ],\n\tcol: [ \"colgroup\", \"table\" ],\n\ttr: [ \"tbody\", \"table\" ],\n\ttd: [ \"tr\", \"tbody\", \"table\" ]\n};\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11+\n\t// Use typeof to avoid zero-argument method invocation on host objects (trac-15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\n\t\t// Use slice to snapshot the live collection from gEBTN\n\t\tret = arr.slice.call( context.getElementsByTagName( tag || \"*\" ) );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\nvar rscriptType = /^$|^module$|\\/(?:java|ecma)script/i;\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, attached, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( toType( elem ) === \"object\" && ( elem.nodeType || isArrayLike( elem ) ) ) {\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || arr;\n\n\t\t\t\t// Create wrappers & descend into them.\n\t\t\t\tj = wrap.length;\n\t\t\t\twhile ( --j > -1 ) {\n\t\t\t\t\ttmp = tmp.appendChild( context.createElement( wrap[ j ] ) );\n\t\t\t\t}\n\n\t\t\t\ttmp.innerHTML = jQuery.htmlPrefilter( elem );\n\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (trac-12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tattached = isAttached( elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( attached ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tif ( ( elem.type || \"\" ).slice( 0, 5 ) === \"true/\" ) {\n\t\telem.type = elem.type.slice( 5 );\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = flat( args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tvalueIsFunction = typeof value === \"function\";\n\n\tif ( valueIsFunction ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (trac-8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Re-enable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.get( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src && ( node.type || \"\" ).toLowerCase()  !== \"module\" ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl && !node.noModule ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src, {\n\t\t\t\t\t\t\t\t\tnonce: node.nonce,\n\t\t\t\t\t\t\t\t\tcrossOrigin: node.crossOrigin\n\t\t\t\t\t\t\t\t}, doc );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDOMEval( node.textContent, node, doc );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nvar\n\n\t// Support: IE <=10 - 11+\n\t// In IE using regex groups here causes severe slowdowns.\n\trnoInnerhtml = /<script|<style|<link/i;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( elem ).children( \"tbody\" )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar type, i, l,\n\t\tevents = dataPriv.get( src, \"events\" );\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( events ) {\n\t\tdataPriv.remove( dest, \"handle events\" );\n\t\tfor ( type in events ) {\n\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tdataUser.set( dest, jQuery.extend( {}, dataUser.get( src ) ) );\n\t}\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && isAttached( node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html;\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = isAttached( elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( isIE && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew jQuery#find here for performance reasons:\n\t\t\t// https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\n\t\t\t\t// Support: IE <=11+\n\t\t\t\t// IE fails to set the defaultValue to the correct value when\n\t\t\t\t// cloning textareas.\n\t\t\t\tif ( nodeName( destElements[ i ], \"textarea\" ) ) {\n\t\t\t\t\tdestElements[ i ].defaultValue = srcElements[ i ].defaultValue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\t\t\tpush.apply( ret, elems );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( typeof html === \"function\" ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( typeof html === \"function\" ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar htmlIsFunction = typeof html === \"function\";\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\nvar pnum = /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/.source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar rcustomProp = /^--/;\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar ralphaStart = /^[a-z]/,\n\n\t// The regex visualized:\n\t//\n\t//                         /----------\\\n\t//                        |            |    /-------\\\n\t//                        |  / Top  \\  |   |         |\n\t//         /--- Border ---+-| Right  |-+---+- Width -+---\\\n\t//        |                 | Bottom |                    |\n\t//        |                  \\ Left /                     |\n\t//        |                                               |\n\t//        |                              /----------\\     |\n\t//        |          /-------------\\    |            |    |- END\n\t//        |         |               |   |  / Top  \\  |    |\n\t//        |         |  / Margin  \\  |   | | Right  | |    |\n\t//        |---------+-|           |-+---+-| Bottom |-+----|\n\t//        |            \\ Padding /         \\ Left /       |\n\t// BEGIN -|                                               |\n\t//        |                /---------\\                    |\n\t//        |               |           |                   |\n\t//        |               |  / Min \\  |    / Width  \\     |\n\t//         \\--------------+-|       |-+---|          |---/\n\t//                           \\ Max /       \\ Height /\n\trautoPx = /^(?:Border(?:Top|Right|Bottom|Left)?(?:Width|)|(?:Margin|Padding)?(?:Top|Right|Bottom|Left)?|(?:Min|Max)?(?:Width|Height))$/;\n\nfunction isAutoPx( prop ) {\n\n\t// The first test is used to ensure that:\n\t// 1. The prop starts with a lowercase letter (as we uppercase it for the second regex).\n\t// 2. The prop is not empty.\n\treturn ralphaStart.test( prop ) &&\n\t\trautoPx.test( prop[ 0 ].toUpperCase() + prop.slice( 1 ) );\n}\n\n// Matches dashed string for camelizing\nvar rmsPrefix = /^-ms-/;\n\n// Convert dashed to camelCase, handle vendor prefixes.\n// Used by the css & effects modules.\n// Support: IE <=9 - 11+\n// Microsoft forgot to hump their vendor prefix (trac-9572)\nfunction cssCamelCase( string ) {\n\treturn camelCase( string.replace( rmsPrefix, \"ms-\" ) );\n}\n\nfunction getStyles( elem ) {\n\n\t// Support: IE <=11+ (trac-14150)\n\t// In IE popup's `window` is the opener window which makes `window.getComputedStyle( elem )`\n\t// break. Using `elem.ownerDocument.defaultView` avoids the issue.\n\tvar view = elem.ownerDocument.defaultView;\n\n\t// `document.implementation.createHTMLDocument( \"\" )` has a `null` `defaultView`\n\t// property; check `defaultView` truthiness to fallback to window in such a case.\n\tif ( !view ) {\n\t\tview = window;\n\t}\n\n\treturn view.getComputedStyle( elem );\n}\n\n// A method for quickly swapping in/out CSS properties to get correct calculations.\nfunction swap( elem, options, callback ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.call( elem );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n}\n\nfunction curCSS( elem, name, computed ) {\n\tvar ret,\n\t\tisCustomProp = rcustomProp.test( name );\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for `.css('--customProperty')` (gh-3144)\n\tif ( computed ) {\n\n\t\t// A fallback to direct property access is needed as `computed`, being\n\t\t// the output of `getComputedStyle`, contains camelCased keys and\n\t\t// `getPropertyValue` requires kebab-case ones.\n\t\t//\n\t\t// Support: IE <=9 - 11+\n\t\t// IE only supports `\"float\"` in `getPropertyValue`; in computed styles\n\t\t// it's only available as `\"cssFloat\"`. We no longer modify properties\n\t\t// sent to `.css()` apart from camelCasing, so we need to check both.\n\t\t// Normally, this would create difference in behavior: if\n\t\t// `getPropertyValue` returns an empty string, the value returned\n\t\t// by `.css()` would be `undefined`. This is usually the case for\n\t\t// disconnected elements. However, in IE even disconnected elements\n\t\t// with no styles return `\"none\"` for `getPropertyValue( \"float\" )`\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( isCustomProp && ret ) {\n\n\t\t\t// Support: Firefox 105 - 135+\n\t\t\t// Spec requires trimming whitespace for custom properties (gh-4926).\n\t\t\t// Firefox only trims leading whitespace.\n\t\t\t//\n\t\t\t// Fall back to `undefined` if empty string returned.\n\t\t\t// This collapses a missing definition with property defined\n\t\t\t// and set to an empty string but there's no standard API\n\t\t\t// allowing us to differentiate them without a performance penalty\n\t\t\t// and returning `undefined` aligns with older jQuery.\n\t\t\t//\n\t\t\t// rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED\n\t\t\t// as whitespace while CSS does not, but this is not a problem\n\t\t\t// because CSS preprocessing replaces them with U+000A LINE FEED\n\t\t\t// (which *is* CSS whitespace)\n\t\t\t// https://www.w3.org/TR/css-syntax-3/#input-preprocessing\n\t\t\tret = ret.replace( rtrimCSS, \"$1\" ) || undefined;\n\t\t}\n\n\t\tif ( ret === \"\" && !isAttached( elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11+\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted, scale,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = function() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( isAutoPx( prop ) ? \"px\" : \"\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = elem.nodeType &&\n\t\t\t( !isAutoPx( prop ) || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Support: Firefox <=54 - 66+\n\t\t// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)\n\t\tinitial = initial / 2;\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\twhile ( maxIterations-- ) {\n\n\t\t\t// Evaluate and update our best guess (doubling guesses that zero out).\n\t\t\t// Finish if the scale equals or crosses 1 (making the old*new product non-positive).\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\t\t\tif ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {\n\t\t\t\tmaxIterations = 0;\n\t\t\t}\n\t\t\tinitialInUnit = initialInUnit / scale;\n\n\t\t}\n\n\t\tinitialInUnit = initialInUnit * 2;\n\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t}\n\treturn adjusted;\n}\n\nvar cssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document$1.createElement( \"div\" ).style;\n\n// Return a vendor-prefixed property or undefined\nfunction vendorPropName( name ) {\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a potentially-mapped vendor prefixed property\nfunction finalPropName( name ) {\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\treturn vendorPropName( name ) || name;\n}\n\nvar reliableTrDimensionsVal, reliableColDimensionsVal,\n\ttable = document$1.createElement( \"table\" );\n\n// Executing table tests requires only one layout, so they're executed\n// at the same time to save the second computation.\nfunction computeTableStyleTests() {\n\tif (\n\n\t\t// This is a singleton, we need to execute it only once\n\t\t!table ||\n\n\t\t// Finish early in limited (non-browser) environments\n\t\t!table.style\n\t) {\n\t\treturn;\n\t}\n\n\tvar trStyle,\n\t\tcol = document$1.createElement( \"col\" ),\n\t\ttr = document$1.createElement( \"tr\" ),\n\t\ttd = document$1.createElement( \"td\" );\n\n\ttable.style.cssText = \"position:absolute;left:-11111px;\" +\n\t\t\"border-collapse:separate;border-spacing:0\";\n\ttr.style.cssText = \"box-sizing:content-box;border:1px solid;height:1px\";\n\ttd.style.cssText = \"height:9px;width:9px;padding:0\";\n\n\tcol.span = 2;\n\n\tdocumentElement$1\n\t\t.appendChild( table )\n\t\t.appendChild( col )\n\t\t.parentNode\n\t\t.appendChild( tr )\n\t\t.appendChild( td )\n\t\t.parentNode\n\t\t.appendChild( td.cloneNode( true ) );\n\n\t// Don't run until window is visible\n\tif ( table.offsetWidth === 0 ) {\n\t\tdocumentElement$1.removeChild( table );\n\t\treturn;\n\t}\n\n\ttrStyle = window.getComputedStyle( tr );\n\n\t// Support: Firefox 135+\n\t// Firefox always reports computed width as if `span` was 1.\n\t// Support: Safari 18.3+\n\t// In Safari, computed width for columns is always 0.\n\t// In both these browsers, using `offsetWidth` solves the issue.\n\t// Support: IE 11+\n\t// In IE, `<col>` computed width is `\"auto\"` unless `width` is set\n\t// explicitly via CSS so measurements there remain incorrect. Because of\n\t// the lack of a proper workaround, we accept this limitation, treating\n\t// IE as passing the test.\n\treliableColDimensionsVal = isIE || Math.round( parseFloat(\n\t\twindow.getComputedStyle( col ).width )\n\t) === 18;\n\n\t// Support: IE 10 - 11+\n\t// IE misreports `getComputedStyle` of table rows with width/height\n\t// set in CSS while `offset*` properties report correct values.\n\t// Support: Firefox 70 - 135+\n\t// Only Firefox includes border widths\n\t// in computed dimensions for table rows. (gh-4529)\n\treliableTrDimensionsVal = Math.round( parseFloat( trStyle.height ) +\n\t\tparseFloat( trStyle.borderTopWidth ) +\n\t\tparseFloat( trStyle.borderBottomWidth ) ) === tr.offsetHeight;\n\n\tdocumentElement$1.removeChild( table );\n\n\t// Nullify the table so it wouldn't be stored in the memory;\n\t// it will also be a sign that checks were already performed.\n\ttable = null;\n}\n\njQuery.extend( support, {\n\treliableTrDimensions: function() {\n\t\tcomputeTableStyleTests();\n\t\treturn reliableTrDimensionsVal;\n\t},\n\n\treliableColDimensions: function() {\n\t\tcomputeTableStyleTests();\n\t\treturn reliableColDimensionsVal;\n\t}\n} );\n\nvar cssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t};\n\nfunction setPositiveNumber( _elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {\n\tvar i = dimension === \"width\" ? 1 : 0,\n\t\textra = 0,\n\t\tdelta = 0,\n\t\tmarginDelta = 0;\n\n\t// Adjustment may not be necessary\n\tif ( box === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\treturn 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin\n\t\t// Count margin delta separately to only add it after scroll gutter adjustment.\n\t\t// This is needed to make negative margins work with `outerHeight( true )` (gh-3982).\n\t\tif ( box === \"margin\" ) {\n\t\t\tmarginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\t// If we get here with a content-box, we're seeking \"padding\" or \"border\" or \"margin\"\n\t\tif ( !isBorderBox ) {\n\n\t\t\t// Add padding\n\t\t\tdelta += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// For \"border\" or \"margin\", add border\n\t\t\tif ( box !== \"padding\" ) {\n\t\t\t\tdelta += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\n\t\t\t// But still keep track of it otherwise\n\t\t\t} else {\n\t\t\t\textra += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\n\t\t// If we get here with a border-box (content + padding + border), we're seeking \"content\" or\n\t\t// \"padding\" or \"margin\"\n\t\t} else {\n\n\t\t\t// For \"content\", subtract padding\n\t\t\tif ( box === \"content\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// For \"content\" or \"padding\", subtract border\n\t\t\tif ( box !== \"margin\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Account for positive content-box scroll gutter when requested by providing computedVal\n\tif ( !isBorderBox && computedVal >= 0 ) {\n\n\t\t// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border\n\t\t// Assuming integer scroll gutter, subtract the rest and round down\n\t\tdelta += Math.max( 0, Math.ceil(\n\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\tcomputedVal -\n\t\t\tdelta -\n\t\t\textra -\n\t\t\t0.5\n\n\t\t// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter\n\t\t// Use an explicit zero to avoid NaN (gh-3964)\n\t\t) ) || 0;\n\t}\n\n\treturn delta + marginDelta;\n}\n\nfunction getWidthOrHeight( elem, dimension, extra ) {\n\n\t// Start with computed style\n\tvar styles = getStyles( elem ),\n\n\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).\n\t\t// Fake content-box until we know it's needed to know the true value.\n\t\tboxSizingNeeded = isIE || extra,\n\t\tisBorderBox = boxSizingNeeded &&\n\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\tvalueIsBorderBox = isBorderBox,\n\n\t\tval = curCSS( elem, dimension, styles ),\n\t\toffsetProp = \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );\n\n\t// Return a confounding non-pixel value or feign ignorance, as appropriate.\n\tif ( rnumnonpx.test( val ) ) {\n\t\tif ( !extra ) {\n\t\t\treturn val;\n\t\t}\n\t\tval = \"auto\";\n\t}\n\n\n\tif (\n\t\t(\n\n\t\t\t// Fall back to offsetWidth/offsetHeight when value is \"auto\"\n\t\t\t// This happens for inline elements with no explicit setting (gh-3571)\n\t\t\tval === \"auto\" ||\n\n\t\t\t// Support: IE 9 - 11+\n\t\t\t// Use offsetWidth/offsetHeight for when box sizing is unreliable.\n\t\t\t// In those cases, the computed value can be trusted to be border-box.\n\t\t\t( isIE && isBorderBox ) ||\n\n\t\t\t( !support.reliableColDimensions() && nodeName( elem, \"col\" ) ) ||\n\n\t\t\t( !support.reliableTrDimensions() && nodeName( elem, \"tr\" ) )\n\t\t) &&\n\n\t\t// Make sure the element is visible & connected\n\t\telem.getClientRects().length ) {\n\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t\t// Where available, offsetWidth/offsetHeight approximate border box dimensions.\n\t\t// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the\n\t\t// retrieved value as a content box dimension.\n\t\tvalueIsBorderBox = offsetProp in elem;\n\t\tif ( valueIsBorderBox ) {\n\t\t\tval = elem[ offsetProp ];\n\t\t}\n\t}\n\n\t// Normalize \"\" and auto\n\tval = parseFloat( val ) || 0;\n\n\t// Adjust for the element's box model\n\treturn ( val +\n\t\tboxModelAdjustment(\n\t\t\telem,\n\t\t\tdimension,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles,\n\n\t\t\t// Provide the current computed size to request scroll gutter calculation (gh-3589)\n\t\t\tval\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = cssCamelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (trac-7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug trac-9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (trac-7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If the value is a number, add `px` for certain CSS properties\n\t\t\tif ( type === \"number\" ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( isAutoPx( origName ) ? \"px\" : \"\" );\n\t\t\t}\n\n\t\t\t// Support: IE <=9 - 11+\n\t\t\t// background-* props of a cloned element affect the source element (trac-8908)\n\t\t\tif ( isIE && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = cssCamelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( _i, dimension ) {\n\tjQuery.cssHooks[ dimension ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Elements with `display: none` can have dimension info if\n\t\t\t\t// we invisibly show them.\n\t\t\t\treturn jQuery.css( elem, \"display\" ) === \"none\" ?\n\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\treturn getWidthOrHeight( elem, dimension, extra );\n\t\t\t\t\t} ) :\n\t\t\t\t\tgetWidthOrHeight( elem, dimension, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = getStyles( elem ),\n\n\t\t\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)\n\t\t\t\tisBorderBox = extra &&\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\tsubtract = extra ?\n\t\t\t\t\tboxModelAdjustment(\n\t\t\t\t\t\telem,\n\t\t\t\t\t\tdimension,\n\t\t\t\t\t\textra,\n\t\t\t\t\t\tisBorderBox,\n\t\t\t\t\t\tstyles\n\t\t\t\t\t) :\n\t\t\t\t\t0;\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ dimension ] = value;\n\t\t\t\tvalue = jQuery.css( elem, dimension );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( prefix !== \"margin\" ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n// isHiddenWithinTree reports if an element has a non-\"none\" display style (inline and/or\n// through the CSS cascade), which is useful in deciding whether or not to make it visible.\n// It differs from the :hidden selector (jQuery.expr.pseudos.hidden) in two important ways:\n// * A hidden ancestor does not force an element to be classified as hidden.\n// * Being disconnected from the document does not force an element to be classified as hidden.\n// These differences improve the behavior of .toggle() et al. when applied to elements that are\n// detached or contained within hidden ancestors (gh-2404, gh-2863).\nfunction isHiddenWithinTree( elem, el ) {\n\n\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t// in that case, element will be second argument\n\telem = elem;\n\n\t// Inline style trumps all\n\treturn elem.style.display === \"none\" ||\n\t\telem.style.display === \"\" &&\n\t\tjQuery.css( elem, \"display\" ) === \"none\";\n}\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && toType( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = typeof valueOrFunction === \"function\" ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\tif ( a == null ) {\n\t\treturn \"\";\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} ).filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} ).map( function( _i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml, parserErrorElem;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11+\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {}\n\n\tparserErrorElem = xml && xml.getElementsByTagName( \"parsererror\" )[ 0 ];\n\tif ( !xml || parserErrorElem ) {\n\t\tjQuery.error( \"Invalid XML: \" + (\n\t\t\tparserErrorElem ?\n\t\t\t\tjQuery.map( parserErrorElem.childNodes, function( el ) {\n\t\t\t\t\treturn el.textContent;\n\t\t\t\t} ).join( \"\\n\" ) :\n\t\t\t\tdata\n\t\t) );\n\t}\n\treturn xml;\n};\n\n// Argument \"data\" should be string of html or a TrustedHTML wrapper of obvious HTML\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" && !isObviousHtml( data + \"\" ) ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using DOMParser\n\t\tcontext = ( new window.DOMParser() )\n\t\t\t.parseFromString( \"\", \"text/html\" );\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( typeof options === \"function\" ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\n\t// offset() relates an element's border box to the document origin\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11+\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\t// Get document-relative position by adding viewport scroll to viewport-relative gBCR\n\t\trect = elem.getBoundingClientRect();\n\t\twin = elem.ownerDocument.defaultView;\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset,\n\t\t\tleft: rect.left + win.pageXOffset\n\t\t};\n\t},\n\n\t// position() relates an element's margin box to its offset parent's padding box\n\t// This corresponds to the behavior of CSS absolute positioning\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset, doc,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// position:fixed elements are offset from the viewport, which itself always has zero offset\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume position:fixed implies availability of getBoundingClientRect\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\toffset = this.offset();\n\n\t\t\t// Account for the *real* offset parent, which can be the document or its root element\n\t\t\t// when a statically positioned element is identified\n\t\t\tdoc = elem.ownerDocument;\n\t\t\toffsetParent = elem.offsetParent || doc.documentElement;\n\t\t\twhile ( offsetParent &&\n\t\t\t\toffsetParent !== doc.documentElement &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\n\t\t\t\toffsetParent = offsetParent.offsetParent || doc.documentElement;\n\t\t\t}\n\t\t\tif ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) !== \"static\" ) {\n\n\t\t\t\t// Incorporate borders into its offset, since they are outside its content origin\n\t\t\t\tparentOffset = jQuery( offsetParent ).offset();\n\t\t\t\tparentOffset.top += jQuery.css( offsetParent, \"borderTopWidth\", true );\n\t\t\t\tparentOffset.left += jQuery.css( offsetParent, \"borderLeftWidth\", true );\n\t\t\t}\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t//    documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement$1;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( {\n\t\tpadding: \"inner\" + name,\n\t\tcontent: type,\n\t\t\"\": \"outer\" + name\n\t}, function( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t},\n\n\thover: function( fnOver, fnOut ) {\n\t\treturn this\n\t\t\t.on( \"mouseenter\", fnOver )\n\t\t\t.on( \"mouseleave\", fnOut || fnOver );\n\t}\n} );\n\njQuery.each(\n\t( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( _i, name ) {\n\n\t\t// Handle event binding\n\t\tjQuery.fn[ name ] = function( data, fn ) {\n\t\t\treturn arguments.length > 0 ?\n\t\t\t\tthis.on( name, null, data, fn ) :\n\t\t\t\tthis.trigger( name );\n\t\t};\n\t}\n);\n\n// Bind a function to a context, optionally partially applying any\n// arguments.\n// jQuery.proxy is deprecated to promote standards (specifically Function#bind)\n// However, it is not slated for removal any time soon\njQuery.proxy = function( fn, context ) {\n\tvar tmp, args, proxy;\n\n\tif ( typeof context === \"string\" ) {\n\t\ttmp = fn[ context ];\n\t\tcontext = fn;\n\t\tfn = tmp;\n\t}\n\n\t// Quick check to determine if target is callable, in the spec\n\t// this throws a TypeError, but we will just return undefined.\n\tif ( typeof fn !== \"function\" ) {\n\t\treturn undefined;\n\t}\n\n\t// Simulated bind\n\targs = slice.call( arguments, 2 );\n\tproxy = function() {\n\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t};\n\n\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\treturn proxy;\n};\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\n\njQuery.expr[ \":\" ] = jQuery.expr.filters = jQuery.expr.pseudos;\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (trac-7102#comment:10, gh-557)\n// and CommonJS for browser emulators (trac-13566)\nif ( typeof noGlobal === \"undefined\" ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\nvar readyCallbacks = [],\n\twhenReady = function( fn ) {\n\t\treadyCallbacks.push( fn );\n\t},\n\texecuteReady = function( fn ) {\n\n\t\t// Prevent errors from freezing future callback execution (gh-1823)\n\t\t// Not backwards-compatible as this does not execute sync\n\t\twindow.setTimeout( function() {\n\t\t\tfn.call( document$1, jQuery );\n\t\t} );\n\t};\n\njQuery.fn.ready = function( fn ) {\n\twhenReady( fn );\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See trac-6781\n\treadyWait: 1,\n\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\twhenReady = function( fn ) {\n\t\t\treadyCallbacks.push( fn );\n\n\t\t\twhile ( readyCallbacks.length ) {\n\t\t\t\tfn = readyCallbacks.shift();\n\t\t\t\tif ( typeof fn === \"function\" ) {\n\t\t\t\t\texecuteReady( fn );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\twhenReady();\n\t}\n} );\n\n// Make jQuery.ready Promise consumable (gh-1778)\njQuery.ready.then = jQuery.fn.ready;\n\n/**\n * The ready event handler and self cleanup method\n */\nfunction completed() {\n\tdocument$1.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\nif ( document$1.readyState !== \"loading\" ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument$1.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\nreturn jQuery;\n\n} );\n"
  },
  {
    "path": "src/static/templates/404.hbs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" />\n    <meta name=\"robots\" content=\"noindex,nofollow\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"{{urlpath}}/vw_static/vaultwarden-favicon.png\">\n    <title>Page not found!</title>\n    <link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/bootstrap.css\" />\n    <link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/404.css\" />\n</head>\n\n<body class=\"bg-light\">\n\n    <nav class=\"navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top\">\n        <div class=\"container\">\n            <a class=\"navbar-brand\" href=\"{{urlpath}}/\"><img class=\"vaultwarden-icon\" src=\"{{urlpath}}/vw_static/vaultwarden-icon.png\" alt=\"V\">aultwarden</a>\n            <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarCollapse\"\n                    aria-controls=\"navbarCollapse\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n                <span class=\"navbar-toggler-icon\"></span>\n            </button>\n            <div class=\"collapse navbar-collapse\" id=\"navbarCollapse\">\n                <ul class=\"navbar-nav me-auto\">\n            </div>\n        </div>\n    </nav>\n\n    <main class=\"container inner content text-center\">\n        <h2>Page not found!</h2>\n        <p class=\"lead\">Sorry, but the page you were looking for could not be found.</p>\n        <p class=\"display-6\">\n            <a href=\"{{urlpath}}/\"><img class=\"vw-404\" src=\"{{urlpath}}/vw_static/404.png\" alt=\"Return to the web vault?\"></a></p>\n        <p>You can <a href=\"{{urlpath}}/\">return to the web-vault</a>, or <a href=\"https://github.com/dani-garcia/vaultwarden\">contact us</a>.</p>\n    </main>\n\n    <div class=\"container footer text-muted content\">Vaultwarden (unofficial Bitwarden&reg; server)</div>\n</body>\n</html>\n"
  },
  {
    "path": "src/static/templates/admin/base.hbs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"robots\" content=\"noindex,nofollow\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"{{urlpath}}/vw_static/vaultwarden-favicon.png\">\n    <title>Vaultwarden Admin Panel</title>\n    <link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/bootstrap.css\" />\n    <link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/admin.css\" />\n    <script src=\"{{urlpath}}/vw_static/admin.js\"></script>\n</head>\n<body>\n    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"d-none\">\n        <symbol id=\"vw-icon-sun\" viewBox=\"0 0 24 24\">\n            <circle cx=\"12\" cy=\"12\" r=\"5\" fill=\"currentColor\"/>\n            <g stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\">\n                <path d=\"M12 2v3M12 19v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M2 12h3M19 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12\"/>\n            </g>\n        </symbol>\n        <symbol id=\"vw-icon-moon\" viewBox=\"0 0 24 24\">\n            <path fill=\"currentColor\" stroke-width=\".8\" d=\"M18.4 17.8A9 8.6 0 0 1 13 2a10.5 10 0 1 0 9 14.4 9.4 9 0 0 1-3.6 1.4\"/>\n        </symbol>\n        <symbol id=\"vw-icon-auto\" viewBox=\"0 0 24 24\">\n            <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n            <path fill=\"currentColor\" d=\"M12 3a9 9 0 1 1 0 18Z\"/>\n        </symbol>\n    </svg>\n    <nav class=\"navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top\">\n        <div class=\"container-xl\">\n            <a class=\"navbar-brand\" href=\"{{urlpath}}/admin\"><img class=\"vaultwarden-icon\" src=\"{{urlpath}}/vw_static/vaultwarden-icon.png\" alt=\"V\">aultwarden Admin</a>\n            <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarCollapse\"\n                aria-controls=\"navbarCollapse\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n                <span class=\"navbar-toggler-icon\"></span>\n            </button>\n            <div class=\"collapse navbar-collapse\" id=\"navbarCollapse\">\n                <ul class=\"navbar-nav me-auto\">\n                    {{#if logged_in}}\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"{{urlpath}}/admin\">Settings</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"{{urlpath}}/admin/users/overview\">Users</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"{{urlpath}}/admin/organizations/overview\">Organizations</a>\n                    </li>\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"{{urlpath}}/admin/diagnostics\">Diagnostics</a>\n                    </li>\n                    {{/if}}\n                    <li class=\"nav-item\">\n                        <a class=\"nav-link\" href=\"{{urlpath}}/\" target=\"_blank\" rel=\"noreferrer\">Vault</a>\n                    </li>\n                </ul>\n\n                <ul class=\"navbar-nav mx-3\">\n                    <li class=\"nav-item dropdown\">\n                        <button\n                            class=\"btn btn-link nav-link py-0 px-0 px-md-2 dropdown-toggle d-flex align-items-center\"\n                            id=\"bd-theme\" type=\"button\" aria-expanded=\"false\" data-bs-toggle=\"dropdown\"\n                            data-bs-display=\"static\" aria-label=\"Toggle theme (auto)\">\n                            <span class=\"my-1 fs-4 theme-icon-active\">\n                                <svg class=\"vw-theme-icon\" focusable=\"false\" aria-hidden=\"true\">\n                                    <use data-theme-icon-use href=\"#vw-icon-auto\"></use>\n                                </svg>\n                            </span>\n                            <span class=\"d-md-none ms-2\" id=\"bd-theme-text\">Toggle theme</span>\n                        </button>\n                        <ul class=\"dropdown-menu dropdown-menu-end\" aria-labelledby=\"bd-theme-text\">\n                            <li>\n                                <button type=\"button\" class=\"dropdown-item d-flex align-items-center\"\n                                    data-bs-theme-value=\"light\" aria-pressed=\"false\">\n                                    <span class=\"me-2 fs-4 theme-icon\">\n                                        <svg class=\"vw-theme-icon\" focusable=\"false\" aria-hidden=\"true\">\n                                            <use data-theme-icon-use href=\"#vw-icon-sun\"></use>\n                                        </svg>\n                                    </span>\n                                    Light\n                                </button>\n                            </li>\n                            <li>\n                                <button type=\"button\" class=\"dropdown-item d-flex align-items-center\"\n                                    data-bs-theme-value=\"dark\" aria-pressed=\"false\">\n                                    <span class=\"me-2 fs-4 theme-icon\">\n                                        <svg class=\"vw-theme-icon\" focusable=\"false\" aria-hidden=\"true\">\n                                            <use data-theme-icon-use href=\"#vw-icon-moon\"></use>\n                                        </svg>\n                                    </span>\n                                    Dark\n                                </button>\n                            </li>\n                            <li>\n                                <button type=\"button\" class=\"dropdown-item d-flex align-items-center active\"\n                                    data-bs-theme-value=\"auto\" aria-pressed=\"true\">\n                                    <span class=\"me-2 fs-4 theme-icon\">\n                                        <svg class=\"vw-theme-icon\" focusable=\"false\" aria-hidden=\"true\">\n                                            <use data-theme-icon-use href=\"#vw-icon-auto\"></use>\n                                        </svg>\n                                    </span>\n                                    Auto\n                                </button>\n                            </li>\n                        </ul>\n                    </li>\n                </ul>\n\n                {{#if logged_in}}\n                <a class=\"btn btn-sm btn-secondary\" href=\"{{urlpath}}/admin/logout\">Log Out</a>\n                {{/if}}\n\n            </div>\n        </div>\n    </nav>\n\n    {{> (lookup this \"page_content\") }}\n\n    <!-- This script needs to be at the bottom, else it will fail! -->\n    <script src=\"{{urlpath}}/vw_static/bootstrap.bundle.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "src/static/templates/admin/diagnostics.hbs",
    "content": "<main class=\"container-xl\">\n    <div id=\"diagnostics-block\" class=\"my-3 p-3 rounded shadow\">\n        <h6 class=\"border-bottom pb-2 mb-2\">Diagnostics</h6>\n\n        <h3>Versions</h3>\n        <div class=\"row\">\n            <div class=\"col-md\">\n                <dl class=\"row\">\n                    <dt class=\"col-sm-5\">Server Installed\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"server-success\" title=\"Latest version is installed.\">Ok</span>\n                        <span class=\"badge bg-warning text-dark d-none abbr-badge\" id=\"server-warning\" title=\"An update is available.\">Update</span>\n                        <span class=\"badge bg-info text-dark d-none abbr-badge\" id=\"server-branch\" title=\"This is a branched version.\">Branched</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"server-installed\">{{page_data.current_release}}</span>\n                    </dd>\n                    <dt class=\"col-sm-5\">Server Latest\n                        <span class=\"badge bg-secondary d-none abbr-badge\" id=\"server-failed\" title=\"Unable to determine latest version.\">Unknown</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"server-latest\">{{page_data.latest_release}}<span id=\"server-latest-commit\" class=\"d-none\">-{{page_data.latest_commit}}</span></span>\n                    </dd>\n                    {{#if page_data.web_vault_enabled}}\n                    <dt class=\"col-sm-5\">Web Installed\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"web-success\" title=\"Latest version is installed.\">Ok</span>\n                        <span class=\"badge bg-warning text-dark d-none abbr-badge\" id=\"web-warning\" title=\"An update is available.\">Update</span>\n                        <span class=\"badge bg-info text-dark d-none abbr-badge\" id=\"web-prerelease\" title=\"You are using a pre-release version.\">Pre-Release</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"web-installed\">{{page_data.active_web_release}}</span>\n                    </dd>\n                    <dt class=\"col-sm-5\">Web Latest\n                        <span class=\"badge bg-secondary d-none abbr-badge\" id=\"web-failed\" title=\"Unable to determine latest version.\">Unknown</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"web-latest\">{{page_data.latest_web_release}}</span>\n                    </dd>\n                    {{/if}}\n                    {{#unless page_data.web_vault_enabled}}\n                    <dt class=\"col-sm-5\">Web Installed</dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"web-installed\">Web Vault is disabled</span>\n                    </dd>\n                    {{/unless}}\n                    <dt class=\"col-sm-5\">Database</dt>\n                    <dd class=\"col-sm-7\">\n                        <span><b>{{page_data.db_type}}:</b> {{page_data.db_version}}</span>\n                    </dd>\n                </dl>\n            </div>\n        </div>\n\n        <h3>Checks</h3>\n        <div class=\"row\">\n            <div class=\"col-md\">\n                <dl class=\"row\">\n                    <dt class=\"col-sm-5\">OS/Arch</dt>\n                    <dd class=\"col-sm-7\">\n                        <span class=\"d-block\"><b>{{ page_data.host_os }} / {{ page_data.host_arch }}</b></span>\n                    </dd>\n                    <dt class=\"col-sm-5\">Running within a container</dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.running_within_container}}\n                        <span class=\"d-block\"><b>Yes (Base: {{ page_data.container_base_image }})</b></span>\n                    {{/if}}\n                    {{#unless page_data.running_within_container}}\n                        <span class=\"d-block\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n                    <dt class=\"col-sm-5\">Uses config.json</dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.overrides}}\n                        <span class=\"d-inline\"><b>Yes</b></span>\n                        <span class=\"badge bg-info text-dark abbr-badge\" title=\"Environment variables are overwritten by a config.json.&#013;&#010;{{page_data.overrides}}\">Details</span>\n                    {{/if}}\n                    {{#unless page_data.overrides}}\n                        <span class=\"d-block\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n                    <dt class=\"col-sm-5\">Uses a reverse proxy</dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.ip_header_exists}}\n                        <span class=\"d-block\" title=\"IP Header found.\"><b>Yes</b></span>\n                    {{/if}}\n                    {{#unless page_data.ip_header_exists}}\n                        <span class=\"d-block\" title=\"No IP Header found.\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n                    {{!-- Only show this if the IP Header Exists --}}\n                    {{#if page_data.ip_header_exists}}\n                    <dt class=\"col-sm-5\">IP header\n                    {{#if page_data.ip_header_match}}\n                        <span class=\"badge bg-success abbr-badge\" title=\"IP_HEADER config seems to be valid.\">Match</span>\n                    {{/if}}\n                    {{#unless page_data.ip_header_match}}\n                        <span class=\"badge bg-danger abbr-badge\" title=\"IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.\">No Match</span>\n                    {{/unless}}\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.ip_header_match}}\n                        <span class=\"d-block\"><b>Config/Server:</b> {{ page_data.ip_header_name }}</span>\n                    {{/if}}\n                    {{#unless page_data.ip_header_match}}\n                        <span class=\"d-block\"><b>Config:</b> {{ page_data.ip_header_config }}</span>\n                        <span class=\"d-block\"><b>Server:</b> {{ page_data.ip_header_name }}</span>\n                    {{/unless}}\n                    </dd>\n                    {{/if}}\n                    {{!-- End if IP Header Exists --}}\n                    <dt class=\"col-sm-5\">Internet access\n                    {{#if page_data.has_http_access}}\n                        <span class=\"badge bg-success abbr-badge\" title=\"We have internet access!\">Ok</span>\n                    {{/if}}\n                    {{#unless page_data.has_http_access}}\n                        <span class=\"badge bg-danger abbr-badge\" title=\"There seems to be no internet access. Please fix.\">Error</span>\n                    {{/unless}}\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.has_http_access}}\n                        <span class=\"d-block\"><b>Yes</b></span>\n                    {{/if}}\n                    {{#unless page_data.has_http_access}}\n                        <span class=\"d-block\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n                    <dt class=\"col-sm-5\">Internet access via a proxy</dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.uses_proxy}}\n                        <span class=\"d-block\" title=\"Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured).\"><b>Yes</b></span>\n                    {{/if}}\n                    {{#unless page_data.uses_proxy}}\n                        <span class=\"d-block\" title=\"We have direct internet access, no outgoing proxy configured.\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n                    <dt class=\"col-sm-5\">Websocket enabled\n                        {{#if page_data.enable_websocket}}\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"websocket-success\" title=\"Websocket connection is working.\">Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"websocket-error\" title=\"Websocket connection error, validate your reverse proxy configuration!\">Error</span>\n                        {{/if}}\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                    {{#if page_data.enable_websocket}}\n                        <span class=\"d-block\" title=\"Websocket connections are enabled (ENABLE_WEBSOCKET is true).\"><b>Yes</b></span>\n                    {{/if}}\n                    {{#unless page_data.enable_websocket}}\n                        <span class=\"d-block\" title=\"Websocket connections are disabled (ENABLE_WEBSOCKET is false).\"><b>No</b></span>\n                    {{/unless}}\n                    </dd>\n\n                    <dt class=\"col-sm-5\">DNS (github.com)\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"dns-success\" title=\"DNS Resolving works!\">Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"dns-warning\" title=\"DNS Resolving failed. Please fix.\">Error</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"dns-resolved\">{{page_data.dns_resolved}}</span>\n                    </dd>\n                    <dt class=\"col-sm-5\">Date & Time (Local)\n                        {{#if page_data.tz_env}}\n                            <span class=\"badge bg-success abbr-badge\" title=\"Configured TZ environment variable\">{{page_data.tz_env}}</span>\n                        {{/if}}\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span><b>Server:</b> {{page_data.server_time_local}}</span>\n                    </dd>\n                    <dt class=\"col-sm-5\">Date & Time (UTC)\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"time-success\" title=\"Server and browser times are within 15 seconds of each other.\">Server/Browser Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"time-warning\" title=\"Server and browser times are more than 15 seconds apart.\">Server/Browser Error</span>\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"ntp-server-success\" title=\"Server and NTP times are within 15 seconds of each other.\">Server NTP Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"ntp-server-warning\" title=\"Server and NTP times are more than 15 seconds apart.\">Server NTP Error</span>\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"ntp-browser-success\" title=\"Browser and NTP times are within 15 seconds of each other.\">Browser NTP Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"ntp-browser-warning\" title=\"Browser and NTP times are more than 15 seconds apart.\">Browser NTP Error</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"ntp-time\" class=\"d-block\"><b>NTP:</b> <span id=\"ntp-server-string\">{{page_data.ntp_time}}</span></span>\n                        <span id=\"time-server\" class=\"d-block\"><b>Server:</b> <span id=\"time-server-string\">{{page_data.server_time}}</span></span>\n                        <span id=\"time-browser\" class=\"d-block\"><b>Browser:</b> <span id=\"time-browser-string\"></span></span>\n                    </dd>\n\n                    <dt class=\"col-sm-5\">Domain configuration\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"domain-success\" title=\"The domain variable matches the browser location and seems to be configured correctly.\">Match</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"domain-warning\" title=\"The domain variable does not match the browser location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!\">No Match</span>\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"https-success\" title=\"Configured to use HTTPS\">HTTPS</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"https-warning\" title=\"Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!\">No HTTPS</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"domain-server\" class=\"d-block\"><b>Server:</b> <span id=\"domain-server-string\">{{page_data.admin_url}}</span></span>\n                        <span id=\"domain-browser\" class=\"d-block\"><b>Browser:</b> <span id=\"domain-browser-string\"></span></span>\n                    </dd>\n\n                    <dt class=\"col-sm-5\">HTTP Response validation\n                        <span class=\"badge bg-success d-none abbr-badge\" id=\"http-response-success\" title=\"All headers and HTTP request responses seem to be ok.\">Ok</span>\n                        <span class=\"badge bg-danger d-none abbr-badge\" id=\"http-response-warning\" title=\"Some headers or HTTP request responses return invalid data!\">Error</span>\n                    </dt>\n                    <dd class=\"col-sm-7\">\n                        <span id=\"http-response-errors\" class=\"d-block\"></span>\n                    </dd>\n                </dl>\n            </div>\n        </div>\n\n        <h3>Support</h3>\n        <div class=\"row\">\n            <div class=\"col-md\">\n                <dl class=\"row\">\n                    <dd class=\"col-sm-12\">\n                        If you need support please check the following links first before you create a new issue:\n                         <a href=\"https://vaultwarden.discourse.group/\" target=\"_blank\" rel=\"noreferrer noopener\">Vaultwarden Forum</a>\n                         | <a href=\"https://github.com/dani-garcia/vaultwarden/discussions\" target=\"_blank\" rel=\"noreferrer noopener\">Github Discussions</a>\n                    </dd>\n                </dl>\n                <dl class=\"row\">\n                    <dd class=\"col-sm-12\">\n                        You can use the button below to pre-generate a string which you can copy/paste on either the Forum or when Creating a new issue at Github.<br>\n                        We try to hide the most sensitive values from the generated support string by default, but please verify if there is nothing in there which you want to hide!<br>\n                    </dd>\n                </dl>\n                <dl class=\"row\">\n                    <dt class=\"col-sm-3\">\n                        <button type=\"button\" id=\"gen-support\" class=\"btn btn-primary\">Generate Support String</button>\n                        <br><br>\n                        <button type=\"button\" id=\"copy-support\" class=\"btn btn-info mb-3 d-none\">Copy To Clipboard</button>\n                        <div class=\"toast-container position-absolute float-start vw-copy-toast\">\n                            <div id=\"toastClipboardCopy\" class=\"toast fade hide\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" data-bs-autohide=\"true\" data-bs-delay=\"1500\">\n                                <div class=\"toast-body\">\n                                    Copied to clipboard!\n                                </div>\n                            </div>\n                        </div>\n                    </dt>\n                    <dd class=\"col-sm-9\">\n                        <pre id=\"support-string\" class=\"pre-scrollable d-none w-100 border p-2\"></pre>\n                    </dd>\n                </dl>\n            </div>\n        </div>\n    </div>\n</main>\n<script src=\"{{urlpath}}/vw_static/admin_diagnostics.js\"></script>\n<script type=\"application/json\" id=\"diagnostics_json\">{{to_json page_data}}</script>\n"
  },
  {
    "path": "src/static/templates/admin/login.hbs",
    "content": "<main class=\"container-xl\">\n    {{#if error}}\n    <div class=\"align-items-center p-3 mb-3 text-opacity-50 text-dark bg-warning rounded shadow\">\n        <div>\n            <h6 class=\"mb-0 text-dark\">{{error}}</h6>\n        </div>\n    </div>\n    {{/if}}\n\n    <div class=\"align-items-center p-3 mb-3 text-opacity-75 text-light bg-danger rounded shadow\">\n        <div>\n            <h6 class=\"mb-0 text-light\">Authentication key needed to continue</h6>\n            <small>Please provide it below:</small>\n\n            <form class=\"form-inline\" method=\"post\" action=\"{{urlpath}}/admin\">\n                <input type=\"password\" autocomplete=\"password\" class=\"form-control w-50 mr-2\" name=\"token\" placeholder=\"Enter admin token\" autofocus=\"autofocus\">\n                {{#if redirect}}\n                <input type=\"hidden\" id=\"redirect\" name=\"redirect\" value=\"/{{redirect}}\">\n                {{/if}}\n                <button type=\"submit\" class=\"btn btn-primary mt-2\">Enter</button>\n            </form>\n        </div>\n    </div>\n</main>\n"
  },
  {
    "path": "src/static/templates/admin/organizations.hbs",
    "content": "<main class=\"container-xl\">\n    <div id=\"organizations-block\" class=\"my-3 p-3 rounded shadow\">\n        <h6 class=\"border-bottom pb-2 mb-3\">Organizations</h6>\n        <div class=\"table-responsive-xl small\">\n            <table id=\"orgs-table\" class=\"table table-sm table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th class=\"vw-org-details\">Organization</th>\n                        <th class=\"vw-users\">Users</th>\n                        <th class=\"vw-entries\">Entries</th>\n                        <th class=\"vw-attachments\">Attachments</th>\n                        <th class=\"vw-misc\">Misc</th>\n                        <th class=\"vw-actions\">Actions</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {{#each page_data}}\n                    <tr>\n                        <td>\n                            <svg width=\"48\" height=\"48\" class=\"float-start me-2 rounded\" data-jdenticon-value=\"{{id}}\">\n                            <div class=\"float-start\">\n                                <strong>{{name}}</strong>\n                                <span class=\"me-2\">({{billingEmail}})</span>\n                                <span class=\"d-block\">\n                                    <span class=\"badge bg-success font-monospace\">{{id}}</span>\n                                </span>\n                            </div>\n                        </td>\n                        <td>\n                            <span class=\"d-block\">{{user_count}}</span>\n                        </td>\n                        <td>\n                            <span class=\"d-block\">{{cipher_count}}</span>\n                        </td>\n                        <td>\n                            <span class=\"d-block\"><strong>Amount:</strong> {{attachment_count}}</span>\n                            {{#if attachment_count}}\n                            <span class=\"d-block\"><strong>Size:</strong> {{attachment_size}}</span>\n                            {{/if}}\n                        </td>\n                        <td>\n                            <span class=\"d-block\"><strong>Collections:</strong> {{collection_count}}</span>\n                            <span class=\"d-block\"><strong>Groups:</strong> {{group_count}}</span>\n                            <span class=\"d-block\"><strong>Events:</strong> {{event_count}}</span>\n                        </td>\n                        <td class=\"text-end px-1 small\">\n                            <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-delete-organization data-vw-org-uuid=\"{{id}}\" data-vw-org-name=\"{{name}}\" data-vw-billing-email=\"{{billingEmail}}\">Delete Organization</button><br>\n                        </td>\n                    </tr>\n                    {{/each}}\n                </tbody>\n            </table>\n        </div>\n\n        <div class=\"mt-3 clearfix\">\n            <button type=\"button\" class=\"btn btn-sm btn-primary float-end\" id=\"reload\">Reload organizations</button>\n        </div>\n    </div>\n</main>\n\n<link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/datatables.css\" />\n<script src=\"{{urlpath}}/vw_static/jquery-4.0.0.slim.js\"></script>\n<script src=\"{{urlpath}}/vw_static/datatables.js\"></script>\n<script src=\"{{urlpath}}/vw_static/admin_organizations.js\"></script>\n<script src=\"{{urlpath}}/vw_static/jdenticon-3.3.0.js\"></script>\n"
  },
  {
    "path": "src/static/templates/admin/settings.hbs",
    "content": "<main class=\"container-xl\">\n    <div id=\"admin_token_warning\" class=\"alert alert-warning alert-dismissible fade show d-none\">\n        <button type=\"button\" class=\"btn-close\" data-bs-target=\"admin_token_warning\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>\n        You are using a plain text `ADMIN_TOKEN` which is insecure.<br>\n        Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.<br>\n        See: <a href=\"https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token\" target=\"_blank\" rel=\"noopener noreferrer\">Enabling admin page - Secure the `ADMIN_TOKEN`</a>\n    </div>\n    <div id=\"config-block\" class=\"align-items-center p-3 mb-3 bg-secondary rounded shadow\">\n        <div>\n            <h6 class=\"text-white mb-3\">Configuration</h6>\n            <div class=\"small text-white mb-3\">\n                <span class=\"font-weight-bolder\">NOTE:</span> The settings here override the environment variables. Once saved, it's recommended to stop setting them to avoid confusion.<br>\n                This does not apply to the read-only section, which can only be set via environment variables.<br>\n                Settings which are overridden are shown with <span class=\"is-overridden-true alert-row px-1\">a yellow colored background</span>.\n            </div>\n\n            <form class=\"form needs-validation\" id=\"config-form\" novalidate>\n                {{#each page_data.config}}\n                {{#if groupdoc}}\n                <div class=\"card mb-3\">\n                    <button id=\"b_{{group}}\" type=\"button\" class=\"card-header text-start btn btn-link text-decoration-none\" aria-expanded=\"false\" aria-controls=\"g_{{group}}\" data-bs-toggle=\"collapse\" data-bs-target=\"#g_{{group}}\">{{groupdoc}}</button>\n                    <div id=\"g_{{group}}\" class=\"card-body collapse\">\n                        {{#each elements}}\n                        {{#if editable}}\n                        <div class=\"row my-2 align-items-center is-overridden-{{overridden}} alert-row\" title=\"[{{name}}] {{doc.description}}\">\n                            {{#case type \"text\" \"number\" \"password\"}}\n                            <label for=\"input_{{name}}\" class=\"col-sm-3 col-form-label\">{{doc.name}}</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"input-group\">\n                                <input class=\"form-control conf-{{type}}\" id=\"input_{{name}}\" type=\"{{type}}\"\n                                    name=\"{{name}}\" value=\"{{value}}\" {{#if default}} placeholder=\"Default: {{default}}\"{{/if}}>\n                                {{#case type \"password\"}}\n                                    <button class=\"btn btn-outline-secondary input-group-text\" type=\"button\" data-vw-pw-toggle=\"input_{{name}}\">Show/hide</button>\n                                {{/case}}\n                                </div>\n                            </div>\n                            {{/case}}\n                            {{#case type \"checkbox\"}}\n                            <div class=\"col-sm-3 col-form-label\">{{doc.name}}</div>\n                            <div class=\"col-sm-8\">\n                                <div class=\"form-check\">\n                                    <input class=\"form-check-input conf-{{type}}\" type=\"checkbox\" id=\"input_{{name}}\"\n                                        name=\"{{name}}\" {{#if value}} checked {{/if}}>\n\n                                    <label class=\"form-check-label\" for=\"input_{{name}}\"> Default: {{default}} </label>\n                                </div>\n                            </div>\n                            {{/case}}\n                        </div>\n                        {{/if}}\n                        {{/each}}\n                        {{#case group \"smtp\"}}\n                            <div class=\"row my-2 align-items-center pt-3 border-top\" title=\"Send a test email to given email address\">\n                                <label for=\"smtp-test-email\" class=\"col-sm-3 col-form-label\">Test SMTP</label>\n                                <div class=\"col-sm-8 input-group\">\n                                    <input class=\"form-control\" id=\"smtp-test-email\" type=\"email\" placeholder=\"Enter test email\" required spellcheck=\"false\">\n                                    <button type=\"button\" class=\"btn btn-outline-primary input-group-text\" id=\"smtpTest\">Send test email</button>\n                                    <div class=\"invalid-tooltip\">Please provide a valid email address</div>\n                                </div>\n                            </div>\n                        {{/case}}\n                    </div>\n                </div>\n                {{/if}}\n                {{/each}}\n\n                <div class=\"card mb-3\">\n                    <button id=\"b_readonly\" type=\"button\" class=\"card-header text-start btn btn-link text-decoration-none\" aria-expanded=\"false\" aria-controls=\"g_readonly\"\n                            data-bs-toggle=\"collapse\" data-bs-target=\"#g_readonly\">Read-Only Config</button>\n                    <div id=\"g_readonly\" class=\"card-body collapse\">\n                        <div class=\"small mb-3\">\n                            NOTE: These options can't be modified in the editor because they would require the server\n                            to be restarted. To modify them, you need to set the correct environment variables when\n                            launching the server. You can check the variable names in the tooltips of each option.\n                        </div>\n\n                        {{#each page_data.config}}\n                        {{#each elements}}\n                        {{#unless editable}}\n                        <div class=\"row my-2 align-items-center alert-row\" title=\"[{{name}}] {{doc.description}}\">\n                            {{#case type \"text\" \"number\" \"password\"}}\n                            <label for=\"input_{{name}}\" class=\"col-sm-3 col-form-label\">{{doc.name}}</label>\n                            <div class=\"col-sm-8\">\n                                <div class=\"input-group\">\n                                {{!--\n                                      Also set the database_url input as password here.\n                                      If we would set it to password in config.rs it will not be character masked for the support string.\n                                      And sometimes this is more useful for providing support than just 3 asterisk.\n                                --}}\n                                {{#if (eq name \"database_url\")}}\n                                    <input readonly class=\"form-control\" id=\"input_{{name}}\" type=\"password\" value=\"{{value}}\" {{#if default}} placeholder=\"Default: {{default}}\" {{/if}}>\n                                    <button class=\"btn btn-outline-secondary\" type=\"button\" data-vw-pw-toggle=\"input_{{name}}\">Show/hide</button>\n                                {{else}}\n                                    <input readonly class=\"form-control\" id=\"input_{{name}}\" type=\"{{type}}\" value=\"{{value}}\" {{#if default}} placeholder=\"Default: {{default}}\" {{/if}} spellcheck=\"false\">\n                                    {{#case type \"password\"}}\n                                    <button class=\"btn btn-outline-secondary\" type=\"button\" data-vw-pw-toggle=\"input_{{name}}\">Show/hide</button>\n                                    {{/case}}\n                                {{/if}}\n                                </div>\n                            </div>\n                            {{/case}}\n                            {{#case type \"checkbox\"}}\n                            <div class=\"col-sm-3 col-form-label\">{{doc.name}}</div>\n                            <div class=\"col-sm-8\">\n                                <div class=\"form-check align-middle\">\n                                    <input disabled class=\"form-check-input\" type=\"checkbox\" id=\"input_{{name}}\"\n                                        {{#if value}} checked {{/if}}>\n\n                                    <label class=\"form-check-label\" for=\"input_{{name}}\"> Default: {{default}} </label>\n                                </div>\n                            </div>\n                            {{/case}}\n                        </div>\n                        {{/unless}}\n                        {{/each}}\n                        {{/each}}\n\n                    </div>\n                </div>\n\n                {{#if page_data.can_backup}}\n                <div class=\"card mb-3\">\n                    <button id=\"b_database\" type=\"button\" class=\"card-header text-start btn btn-link text-decoration-none\" aria-expanded=\"false\" aria-controls=\"g_database\"\n                            data-bs-toggle=\"collapse\" data-bs-target=\"#g_database\">Backup Database</button>\n                    <div id=\"g_database\" class=\"card-body collapse\">\n                        <div class=\"small mb-3\">\n                            WARNING: This function only creates a backup copy of the SQLite database.\n                            This does not include any configuration or file attachment data that may\n                            also be needed to fully restore a vaultwarden instance. For details on\n                            how to perform complete backups, refer to the wiki page on\n                            <a href=\"https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault\" target=\"_blank\" rel=\"noopener noreferrer\">backups</a>.\n                        </div>\n                        <button type=\"button\" class=\"btn btn-primary\" id=\"backupDatabase\">Backup Database</button>\n                    </div>\n                </div>\n                {{/if}}\n\n                <button type=\"submit\" class=\"btn btn-primary\">Save</button>\n                <button type=\"button\" class=\"btn btn-danger float-end\" id=\"deleteConf\">Reset defaults</button>\n            </form>\n        </div>\n    </div>\n</main>\n<style>\n    #config-block ::placeholder {\n        /* Most modern browsers support this now. */\n        color: orangered;\n    }\n\n    .is-overridden-true {\n        --bs-alert-color: #664d03;\n        --bs-alert-bg: #fff3cd;\n        --bs-alert-border-color: #ffecb5;\n    }\n</style>\n<script src=\"{{urlpath}}/vw_static/admin_settings.js\"></script>\n"
  },
  {
    "path": "src/static/templates/admin/users.hbs",
    "content": "<main class=\"container-xl\">\n    <div id=\"users-block\" class=\"my-3 p-3 rounded shadow\">\n        <h6 class=\"border-bottom pb-2 mb-3\">Registered Users</h6>\n        <div class=\"table-responsive-xl small\">\n            <table id=\"users-table\" class=\"table table-sm table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th class=\"vw-account-details\">User</th>\n                        {{#if sso_enabled}}\n                        <th class=\"vw-sso-identifier\">SSO Identifier</th>\n                        {{/if}}\n                        <th class=\"vw-created-at\">Created at</th>\n                        <th class=\"vw-last-active\">Last Active</th>\n                        <th class=\"vw-entries\">Entries</th>\n                        <th class=\"vw-attachments\">Attachments</th>\n                        <th class=\"vw-organizations\">Organizations</th>\n                        <th class=\"vw-actions\">Actions</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {{#each page_data}}\n                    <tr>\n                        <td>\n                            <svg width=\"48\" height=\"48\" class=\"float-start me-2 rounded\" data-jdenticon-value=\"{{email}}\">\n                            <div>\n                                <strong>{{name}}</strong>\n                                <span class=\"d-block\">{{email}}</span>\n                                <span class=\"d-block\">\n                                    {{#unless user_enabled}}\n                                        <span class=\"badge bg-danger me-2\" title=\"User is disabled\">Disabled</span>\n                                    {{/unless}}\n                                    {{#if twoFactorEnabled}}\n                                        <span class=\"badge bg-success me-2\" title=\"2FA is enabled\">2FA</span>\n                                    {{/if}}\n                                    {{#case _status 1}}\n                                        <span class=\"badge bg-warning text-dark me-2\" title=\"User is invited\">Invited</span>\n                                    {{/case}}\n                                    {{#if emailVerified}}\n                                        <span class=\"badge bg-success me-2\" title=\"Email has been verified\">Verified</span>\n                                    {{/if}}\n                                </span>\n                            </div>\n                        </td>\n                        {{#if ../sso_enabled}}\n                        <td>\n                            <span class=\"d-block\">{{sso_identifier}}</span>\n                        </td>\n                        {{/if}}\n                        <td>\n                            <span class=\"d-block\">{{created_at}}</span>\n                        </td>\n                        <td>\n                            <span class=\"d-block\">{{last_active}}</span>\n                        </td>\n                        <td>\n                            <span class=\"d-block\">{{cipher_count}}</span>\n                        </td>\n                        <td>\n                            <span class=\"d-block\"><strong>Amount:</strong> {{attachment_count}}</span>\n                            {{#if attachment_count}}\n                            <span class=\"d-block\"><strong>Size:</strong> {{attachment_size}}</span>\n                            {{/if}}\n                        </td>\n                        <td>\n                            <div class=\"overflow-auto vw-org-cell\" data-vw-user-email=\"{{email}}\" data-vw-user-uuid=\"{{id}}\">\n                            {{#each organizations}}\n                            <button class=\"badge\" data-bs-toggle=\"modal\" data-bs-target=\"#userOrgTypeDialog\" data-vw-org-type=\"{{type}}\" data-vw-org-uuid=\"{{id}}\" data-vw-org-name=\"{{name}}\">{{name}}</button>\n                            {{/each}}\n                            </div>\n                        </td>\n                        <td class=\"text-end px-1 small\">\n                            <span data-vw-user-uuid=\"{{id}}\" data-vw-user-email=\"{{email}}\">\n                                {{#if twoFactorEnabled}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-remove2fa>Remove all 2FA</button><br>\n                                {{/if}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-deauth-user>Deauthorize sessions</button><br>\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-delete-user>Delete User</button><br>\n                                {{#if ../sso_enabled}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-delete-sso-user>Delete SSO Association</button><br>\n                                {{/if}}\n                                {{#if user_enabled}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-disable-user>Disable User</button><br>\n                                {{else}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-enable-user>Enable User</button><br>\n                                {{/if}}\n                                {{#case _status 1}}\n                                <button type=\"button\" class=\"btn btn-sm btn-link p-0 border-0 float-right\" vw-resend-user-invite>Resend invite</button><br>\n                                {{/case}}\n                            </span>\n                        </td>\n                    </tr>\n                    {{/each}}\n                </tbody>\n            </table>\n        </div>\n\n        <div class=\"mt-3 clearfix\">\n            <button type=\"button\" class=\"btn btn-sm btn-danger\" id=\"updateRevisions\"\n                title=\"Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.\">\n                Force clients to resync\n            </button>\n\n            <button type=\"button\" class=\"btn btn-sm btn-primary float-end\" id=\"reload\">Reload users</button>\n        </div>\n    </div>\n\n    <div id=\"inviteUserFormBlock\" class=\"align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow\">\n        <div>\n            <h6 class=\"mb-0 text-white\">Invite User</h6>\n            <small>Email:</small>\n\n            <form class=\"form-inline input-group w-50\" id=\"inviteUserForm\">\n                <input type=\"email\" class=\"form-control me-2\" id=\"inviteEmail\" placeholder=\"Enter email\" required spellcheck=\"false\">\n                <button type=\"submit\" class=\"btn btn-primary\">Invite</button>\n            </form>\n        </div>\n    </div>\n\n    <div id=\"userOrgTypeDialog\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n        <div class=\"modal-dialog modal-dialog-centered modal-sm\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <h6 class=\"modal-title\">\n                        <b>Update User Type:</b><br><b>Organization:</b> <span id=\"userOrgTypeDialogOrgName\"></span><br><b>User:</b> <span id=\"userOrgTypeDialogUserEmail\"></span>\n                    </h6>\n                    <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n                </div>\n                <form class=\"form\" id=\"userOrgTypeForm\">\n                    <input type=\"hidden\" name=\"user_uuid\" id=\"userOrgTypeUserUuid\" value=\"\">\n                    <input type=\"hidden\" name=\"org_uuid\" id=\"userOrgTypeOrgUuid\" value=\"\">\n                    <div class=\"modal-body\">\n                        <div class=\"radio\">\n                            <label><input type=\"radio\" value=\"2\" class=\"form-radio-input\" name=\"user_type\" id=\"userOrgTypeUser\">&nbsp;User</label>\n                        </div>\n                        <div class=\"radio\">\n                            <label><input type=\"radio\" value=\"3\" class=\"form-radio-input\" name=\"user_type\" id=\"userOrgTypeManager\">&nbsp;Manager</label>\n                        </div>\n                        <div class=\"radio\">\n                            <label><input type=\"radio\" value=\"1\" class=\"form-radio-input\" name=\"user_type\" id=\"userOrgTypeAdmin\">&nbsp;Admin</label>\n                        </div>\n                        <div class=\"radio\">\n                            <label><input type=\"radio\" value=\"0\" class=\"form-radio-input\" name=\"user_type\" id=\"userOrgTypeOwner\">&nbsp;Owner</label>\n                        </div>\n                    </div>\n                    <div class=\"modal-footer\">\n                        <button type=\"button\" class=\"btn btn-sm btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button>\n                        <button type=\"submit\" class=\"btn btn-sm btn-primary\">Change Role</button>\n                    </div>\n                </form>\n            </div>\n        </div>\n    </div>\n</main>\n\n<link rel=\"stylesheet\" href=\"{{urlpath}}/vw_static/datatables.css\" />\n<script src=\"{{urlpath}}/vw_static/jquery-4.0.0.slim.js\"></script>\n<script src=\"{{urlpath}}/vw_static/datatables.js\"></script>\n<script src=\"{{urlpath}}/vw_static/admin_users.js\"></script>\n<script src=\"{{urlpath}}/vw_static/jdenticon-3.3.0.js\"></script>\n"
  },
  {
    "path": "src/static/templates/email/admin_reset_password.hbs",
    "content": "Master Password Has Been Changed\n<!---------------->\nThe 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.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/admin_reset_password.html.hbs",
    "content": "Master Password Has Been Changed\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            The master password for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{user_name}}</b> has been changed by an administrator in your <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b> organization. If you did not initiate this request, please reach out to your administrator immediately.\n        </td>\n    </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/change_email.hbs",
    "content": "Your Email Change\n<!---------------->\nTo finalize changing your email address enter the following code in web vault: {{token}}\n\nIf you did not try to change your email address, contact your administrator.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/change_email.html.hbs",
    "content": "Your Email Change\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         To finalize changing your email address enter the following code in web vault: <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{token}}</b>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not try to change your email address, contact your administrator.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/change_email_existing.hbs",
    "content": "Your Email Change\n<!---------------->\nA user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).\n\nIf you did not try to change an email address, contact your administrator.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/change_email_existing.html.hbs",
    "content": "Your Email Change\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). An account already exists with this email ({{ existing_address }}).\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not try to change an email address, contact your administrator.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/change_email_invited.hbs",
    "content": "Your Email Change\n<!---------------->\nA user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). You already have been invited to join Vaultwarden using this address.\n\nTo change your email address you first would have to delete the account associated with this email address ({{ existing_address }}):\nRequest account deletion: {{url}}/#/recover-delete\n\nOnce that is done you can change the email address of your existing account to this address. Any invitation would have to be redone.\n\nIf you did not try to change an email address, contact your administrator.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/change_email_invited.html.hbs",
    "content": "Your Email Change\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n      A user ({{ acting_address }}) recently tried to change their account to use this email address ({{ existing_address }}). You already have been invited to join Vaultwarden using this address.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n      To change your email address you first would have to delete the account associated with this email address ({{ existing_address }}):\n         <a data-testid=\"recover-delete\" href=\"{{url}}/#/recover-delete\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n\t    Request account deletion\n\t </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n      Once that is done you can change the email address of your existing account to this address. Any invitation would have to be redone.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not try to change an email address, contact your administrator.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/delete_account.hbs",
    "content": "Delete Your Account\n<!---------------->\nClick the link below to delete your account.\n\nDelete Your Account: {{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}\n\nIf you did not request this email to delete your account, you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/delete_account.html.hbs",
    "content": "Delete Your Account\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Click the link below to delete your account.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         <a href=\"{{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}}\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n         Delete Your Account\n         </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not request this email to delete your account, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/email_footer.hbs",
    "content": "                           </td>\n                        </tr>\n                        </table>\n                     </td>\n                  </tr>\n               </table>\n\n               <table class=\"footer\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;\">\n                  <tr style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n                     <td class=\"aligncenter social-icons\" align=\"center\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 5px 0 20px 0;\" valign=\"top\">\n                        <table cellpadding=\"0\" cellspacing=\"0\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;\">\n                           <tr style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n                                 <td style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://github.com/dani-garcia/vaultwarden\" target=\"_blank\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;\"><img src=\"{{img_src}}mail-github.png\" alt=\"GitHub\" width=\"30\" height=\"30\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;\" /></a></td>\n                           </tr>\n                        </table>\n                     </td>\n                  </tr>\n               </table>\n\n            </td>\n         </tr>\n      </table>\n   </body>\n</html>\n"
  },
  {
    "path": "src/static/templates/email/email_footer_text.hbs",
    "content": "\n===\nGithub: https://github.com/dani-garcia/vaultwarden\n"
  },
  {
    "path": "src/static/templates/email/email_header.hbs",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/1999/xhtml\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n   <head>\n      <meta name=\"viewport\" content=\"width=device-width\" />\n      <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n      <title>Vaultwarden</title>\n   </head>\n   <body style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;\" bgcolor=\"#f6f6f6\">\n      <style type=\"text/css\">\n         body {\n         margin: 0;\n         font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n         box-sizing: border-box;\n         font-size: 16px;\n         color: #333;\n         line-height: 25px;\n         -webkit-font-smoothing: antialiased;\n         -webkit-text-size-adjust: none;\n         }\n         body * {\n         margin: 0;\n         font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n         box-sizing: border-box;\n         font-size: 16px;\n         color: #333;\n         line-height: 25px;\n         -webkit-font-smoothing: antialiased;\n         -webkit-text-size-adjust: none;\n         }\n         img {\n         max-width: 100%;\n         border: none;\n         }\n         body {\n         -webkit-font-smoothing: antialiased;\n         -webkit-text-size-adjust: none;\n         width: 100% !important;\n         height: 100%;\n         line-height: 25px;\n         }\n         body {\n         background-color: #f6f6f6;\n         }\n         @media only screen and (max-width: 600px) {\n         body {\n         padding: 0 !important;\n         }\n         .container {\n         padding: 0 !important;\n         width: 100% !important;\n         }\n         .container-table {\n         padding: 0 !important;\n         width: 100% !important;\n         }\n         .content {\n         padding: 0 0 10px 0 !important;\n         }\n         .content-wrap {\n         padding: 10px !important;\n         }\n         .invoice {\n         width: 100% !important;\n         }\n         .main {\n         border-right: none !important;\n         border-left: none !important;\n         border-radius: 0 !important;\n         }\n         .logo {\n         padding-top: 10px !important;\n         }\n         .footer {\n         margin-top: 10px !important;\n         }\n         .indented {\n         padding-left: 10px;\n         }\n         }\n      </style>\n      <table class=\"body-wrap\" cellpadding=\"0\" cellspacing=\"0\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;\" bgcolor=\"#f6f6f6\">\n         <tr style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n            <td valign=\"middle\" class=\"aligncenter middle logo\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;\" align=\"center\">\n                <img src=\"{{img_src}}logo-gray.png\" alt=\"Vaultwarden\" width=\"190\" height=\"39\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;\" />\n            </td>\n         </tr>\n         <tr style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n            <td class=\"container\" align=\"center\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;\" valign=\"top\">\n               <table cellpadding=\"0\" cellspacing=\"0\" class=\"container-table\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: max-content;\">\n                  <tr style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;\">\n                     <td class=\"content\" align=\"center\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;\" valign=\"top\">\n                        <table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;\" bgcolor=\"white\">\n                           <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                              <td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_invite_accepted.hbs",
    "content": "Emergency access contact {{{grantee_email}}} accepted\n<!---------------->\nThis email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact.\n\nTo confirm this user, log into the web vault ({{url}}), go to settings and confirm the user.\n\nIf you do not wish to confirm this user, you can also remove them on the same page.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_invite_accepted.html.hbs",
    "content": "Emergency access contact {{{grantee_email}}} accepted\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            This email is to notify you that {{grantee_email}} has accepted your invitation to become an emergency access contact.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            To confirm this user, log into the <a href=\"{{url}}/\">web vault</a>, go to settings and confirm the user.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you do not wish to confirm this user, you can also remove them on the same page.\n        </td>\n    </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_invite_confirmed.hbs",
    "content": "Emergency access contact for {{{grantor_name}}} confirmed\n<!---------------->\nThis email is to notify you that you have been confirmed as an emergency access contact for *{{grantor_name}}*.\n\nYou can now initiate emergency access requests from the web vault ({{url}}).\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_invite_confirmed.html.hbs",
    "content": "Emergency access contact for {{{grantor_name}}} confirmed\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           This email is to notify you that you have been confirmed as an emergency access contact for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantor_name}}</b>.\n       </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n           You can now initiate emergency access requests from the <a href=\"{{url}}/\">web vault</a>.\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_approved.hbs",
    "content": "Emergency access request for {{{grantor_name}}} approved\n<!---------------->\n{{grantor_name}} has approved your emergency access request. You may now login on the web vault ({{url}}) and access their account.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_approved.html.hbs",
    "content": "Emergency access request for {{{grantor_name}}} approved\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantor_name}}</b> has approved your emergency access request. You may now login on the <a href=\"{{url}}/\">web vault</a> and access their account.\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_initiated.hbs",
    "content": "Emergency access request by {{{grantee_name}}} initiated\n<!---------------->\n{{grantee_name}} has initiated an emergency access request to {{atype}} your account. You may login on the web vault ({{url}}) and manually approve or reject this request.\n\nIf you do nothing, the request will automatically be approved after {{wait_time_days}} day(s).\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_initiated.html.hbs",
    "content": "Emergency access request by {{{grantee_name}}} initiated\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantee_name}}</b> has initiated an emergency access request to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{atype}}</b> your account. You may login on the <a href=\"{{url}}/\">web vault</a> and manually approve or reject this request.\n       </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n           If you do nothing, the request will automatically be approved after <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{wait_time_days}}</b> day(s).\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_rejected.hbs",
    "content": "Emergency access request to {{{grantor_name}}} rejected\n<!---------------->\n{{grantor_name}} has rejected your emergency access request.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_rejected.html.hbs",
    "content": "Emergency access request to {{{grantor_name}}} rejected\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantor_name}}</b> has rejected your emergency access request.\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_reminder.hbs",
    "content": "Emergency access request by {{{grantee_name}}} is pending\n<!---------------->\n{{grantee_name}} has a pending emergency access request to {{atype}} your account. You may login on the web vault ({{url}}) and manually approve or reject this request.\n\nIf you do nothing, the request will automatically be approved after {{days_left}} day(s).\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_reminder.html.hbs",
    "content": "Emergency access request by {{{grantee_name}}} is pending\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantee_name}}</b> has a pending emergency access request to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{atype}}</b> your account. You may login on the <a href=\"{{url}}/\">web vault</a> and manually approve or reject this request.\n       </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n           If you do nothing, the request will automatically be approved after <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{days_left}}</b> day(s).\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_timed_out.hbs",
    "content": "Emergency access request by {{{grantee_name}}} granted\n<!---------------->\n{{grantee_name}} has been granted emergency access to {{atype}} your account. You may login on the web vault ({{url}}) and manually revoke this request.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/emergency_access_recovery_timed_out.html.hbs",
    "content": "Emergency access request by {{{grantee_name}}} granted\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n           <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantee_name}}</b> has been granted emergency access to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{atype}}</b> your account. You may login on the <a href=\"{{url}}/\">web vault</a> and manually revoke this request.\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/incomplete_2fa_login.hbs",
    "content": "Incomplete Two-Step Login From {{{device_name}}}\n<!---------------->\nSomeone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.\n\n* Date: {{datetime}}\n* IP Address: {{ip}}\n* Device Name: {{device_name}}\n* Device Type: {{device_type}}\n\nIf this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/incomplete_2fa_login.html.hbs",
    "content": "Incomplete Two-Step Login From {{{device_name}}}\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         <b>Date</b>: {{datetime}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>IP Address:</b> {{ip}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>Device Name:</b> {{device_name}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>Device Type:</b> {{device_type}}\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/invite_accepted.hbs",
    "content": "Invitation to {{{org_name}}} accepted\n<!---------------->\nThis email is to notify you that {{email}} has accepted your invitation to join {{org_name}}.\nPlease log in via {{url}} to the vaultwarden server and confirm them from the organization management page.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/invite_accepted.html.hbs",
    "content": "Invitation to {{{org_name}}} accepted\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         This email is to notify you that {{email}} has accepted your invitation to join <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b>.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         Please <a href=\"{{url}}/\">log in</a> to the vaultwarden server and confirm them from the organization management page.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If you do not wish to confirm this user, you can also remove them from the organization on the same page.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/invite_confirmed.hbs",
    "content": "Invitation to {{{org_name}}} confirmed\n<!---------------->\nThis email is to notify you that you have been confirmed as a user of {{org_name}}.\nAny collections and logins being shared with you by this organization will now appear in your Vaultwarden vault at {{url}}.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/invite_confirmed.html.hbs",
    "content": "Invitation to {{{org_name}}} confirmed\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         This email is to notify you that you have been confirmed as a user of <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b>.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         Any collections and logins being shared with you by this organization will now appear in your Vaultwarden vault. <br>\n         <a href=\"{{url}}/\">Log in</a>\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/new_device_logged_in.hbs",
    "content": "New Device Logged In From {{{device_name}}}\n<!---------------->\nYour account was just logged into from a new device.\n\n* Date: {{datetime}}\n* IP Address: {{ip}}\n* Device Name: {{device_name}}\n* Device Type: {{device_type}}\n\nYou can deauthorize all devices that have access to your account from the web vault ( {{url}} ) under Settings > My Account > Deauthorize Sessions.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/new_device_logged_in.html.hbs",
    "content": "New Device Logged In From {{{device_name}}}\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         Your account was just logged into from a new device.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         <b>Date:</b> {{datetime}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>IP Address:</b> {{ip}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>Device Name:</b> {{device_name}}\n      </td>\n   </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b>Device Type:</b> {{device_type}}\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You can deauthorize all devices that have access to your account from the <a href=\"{{url}}/\">web vault</a> under Settings > My Account > Deauthorize Sessions.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/protected_action.hbs",
    "content": "Your Vaultwarden Verification Code\n<!---------------->\nYour email verification code is: {{token}}\n\nUse this code to complete the protected action in Vaultwarden.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/protected_action.html.hbs",
    "content": "Your Vaultwarden Verification Code\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your email verification code is: <b>{{token}}</b>\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Use this code to complete the protected action in Vaultwarden.\n        </td>\n    </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/pw_hint_none.hbs",
    "content": "Your master password hint\n<!---------------->\nYou (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint.\n\nIf you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account ( {{url}}/#/recover-delete ) so that you can register again and start over. All data associated with your account will be deleted.\n\nIf you did not request your master password hint you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/pw_hint_none.html.hbs",
    "content": "Sorry, you have no password hint...\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href=\"{{url}}/#/recover-delete\">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If you did not request your master password hint you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/pw_hint_some.hbs",
    "content": "Your master password hint\n<!---------------->\nYou (or someone) recently requested your master password hint.\n\nYour hint is: *{{hint}}*\nLog in to the web vault: {{url}}\n\nIf you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account ( {{url}}/#/recover-delete ) so that you can register again and start over. All data associated with your account will be deleted.\n\nIf you did not request your master password hint you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/pw_hint_some.html.hbs",
    "content": "Your master password hint\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         You (or someone) recently requested your master password hint.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n         Your hint is: \"{{hint}}\"<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n         Log in: <a href=\"{{url}}/\">Web Vault</a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href=\"{{url}}/#/recover-delete\">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n         If you did not request your master password hint you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/register_verify_email.hbs",
    "content": "Verify Your Email\n<!---------------->\nVerify this email address to finish creating your account by clicking the link below.\n\nVerify Email Address Now: {{{url}}}\n\nIf you did not request to verify your account, you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/register_verify_email.html.hbs",
    "content": "Verify Your Email\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Verify this email address to finish creating your account by clicking the link below.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         <a href=\"{{{url}}}\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n         Verify Email Address Now\n         </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not request to verify your account, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/send_2fa_removed_from_org.hbs",
    "content": "Your access to {{{org_name}}} has been revoked.\n<!---------------->\nYour user account has been removed from the *{{org_name}}* organization because you do not have two-step login configured.\nBefore you can re-join this organization you need to set up two-step login on your user account.\n\nYou can enable two-step login in your account settings.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/send_2fa_removed_from_org.html.hbs",
    "content": "Your access to {{{org_name}}} has been revoked.\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Your user account has been removed from the <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b> organization because you do not have two-step login configured.<br>\n         Before you can re-join this organization you need to set up two-step login on your user account.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         You can enable two-step login in your account settings.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/send_emergency_access_invite.hbs",
    "content": "Emergency access for {{{grantor_name}}}\n<!---------------->\nYou have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link:\n\nClick here to join: {{{url}}}\n\nIf you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/send_emergency_access_invite.html.hbs",
    "content": "Emergency access for {{{grantor_name}}}\n<!---------------->\n{{> email/email_header }}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n          You have been invited to become an emergency contact for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{grantor_name}}</b>.\n       </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n          <a href=\"{{{url}}}\"\n             clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n          Become emergency contact\n          </a>\n       </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n       <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n           If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.\n       </td>\n    </tr>\n </table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/send_org_invite.hbs",
    "content": "Join {{{org_name}}}\n<!---------------->\nYou have been invited to join the *{{org_name}}* organization.\n\n\nClick here to join: {{{url}}}\n\n\nIf you do not wish to join this organization, you can safely ignore this email.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/send_org_invite.html.hbs",
    "content": "Join {{{org_name}}}\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         You have been invited to join the <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b> organization.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         <a data-testid=\"invite\" href=\"{{{url}}}\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n         Join Organization Now\n         </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you do not wish to join this organization, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/send_single_org_removed_from_org.hbs",
    "content": "Your access to {{{org_name}}} has been revoked\n<!---------------->\nYour access to the *{{org_name}}* organization has been revoked because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before your access can be restored you need to leave all other organizations or join with a different account.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/send_single_org_removed_from_org.html.hbs",
    "content": "Your access to {{{org_name}}} has been revoked\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Your access to the <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{org_name}}</b> organization has been revoked because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before your access can be restored you need to leave all other organizations or join with a different account.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/smtp_test.hbs",
    "content": "Vaultwarden SMTP Test\n<!---------------->\nThis is a test email to verify the SMTP configuration for {{url}}.\n\nWhen you can read this email it is probably configured correctly.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/smtp_test.html.hbs",
    "content": "Vaultwarden SMTP Test\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         This is a test email to verify the SMTP configuration for <a href=\"{{url}}\">{{url}}</a>.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         When you can read this email it is probably configured correctly.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/sso_change_email.hbs",
    "content": "Your Email Changed\n<!---------------->\nYour email was changed in your SSO Provider. Please update your email in Account Settings ({{url}}).\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/sso_change_email.html.hbs",
    "content": "Your Email Changed\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Your email was changed in your SSO Provider. Please update your email in <a href=\"{{url}}/\">Account Settings</a>.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/twofactor_email.hbs",
    "content": "Vaultwarden Login Verification Code\n<!---------------->\nYour two-step verification code is: {{token}}\n\nUse this code to complete logging in with Vaultwarden.\n{{> email/email_footer_text }}\n"
  },
  {
    "path": "src/static/templates/email/twofactor_email.html.hbs",
    "content": "Vaultwarden Login Verification Code\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your two-step verification code is: <b data-testid=\"2fa\">{{token}}</b>\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Use this code to complete logging in with Vaultwarden.\n        </td>\n    </tr>\n</table>\n{{> email/email_footer }}\n"
  },
  {
    "path": "src/static/templates/email/verify_email.hbs",
    "content": "Verify Your Email\n<!---------------->\nVerify this email address for your account by clicking the link below.\n\nVerify Email Address Now: {{url}}/#/verify-email/?userId={{user_id}}&token={{token}}\n\nIf you did not request to verify your account, you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/verify_email.html.hbs",
    "content": "Verify Your Email\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Verify this email address for your account by clicking the link below.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         <a data-testid=\"verify\" href=\"{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n         Verify Email Address Now\n         </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not request to verify your account, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/welcome.hbs",
    "content": "Welcome\n<!---------------->\nThank you for creating an account at {{url}}. You may now log in with your new account.\n\nIf you did not request to create an account, you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/welcome.html.hbs",
    "content": "Welcome\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Thank you for creating an account at <a href=\"{{url}}/\">{{url}}</a>. You may now log in with your new account.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not request to create an account, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/email/welcome_must_verify.hbs",
    "content": "Welcome\n<!---------------->\nThank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below.\n\nVerify Email Address Now: {{url}}/#/verify-email/?userId={{user_id}}&token={{token}}\n\nIf you did not request to create an account, you can safely ignore this email.\n{{> email/email_footer_text }}"
  },
  {
    "path": "src/static/templates/email/welcome_must_verify.html.hbs",
    "content": "Welcome\n<!---------------->\n{{> email/email_header }}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         Thank you for creating an account at <a href=\"{{url}}/\">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         <a href=\"{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}\"\n            clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n         Verify Email Address Now\n         </a>\n      </td>\n   </tr>\n   <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n      <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n         If you did not request to create an account, you can safely ignore this email.\n      </td>\n   </tr>\n</table>\n{{> email/email_footer }}"
  },
  {
    "path": "src/static/templates/scss/user.vaultwarden.scss.hbs",
    "content": "/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */\n"
  },
  {
    "path": "src/static/templates/scss/vaultwarden.scss.hbs",
    "content": "/**** START Static Vaultwarden changes ****/\n/* This combines all selectors extending it into one */\n%vw-hide {\n  display: none !important;\n}\n\n/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */\n.vw-hide,\nhead {\n  @extend %vw-hide;\n}\n\n/* Hide the Subscription Page tab */\nbit-nav-item[route=\"settings/subscription\"] {\n  @extend %vw-hide;\n}\n\n/* Hide any link pointing to Free Bitwarden Families */\na[href$=\"/settings/sponsored-families\"] {\n  @extend %vw-hide;\n}\n\n/* Hide the sso `Email` input field */\n{{#if (not sso_enabled)}}\n.vw-email-sso {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the default/continue `Email` input field */\n{{#if sso_enabled}}\n.vw-email-continue {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the `Continue` button on the login page */\n{{#if sso_enabled}}\n.vw-continue-login {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the `Enterprise Single Sign-On` button on the login page */\n{{#if (webver \">=2025.5.1\")}}\n{{#if (not sso_enabled)}}\n.vw-sso-login {\n  @extend %vw-hide;\n}\n{{/if}}\n{{else}}\napp-root ng-component > form > div:nth-child(1) > div > button[buttontype=\"secondary\"].\\!tw-text-primary-600:nth-child(4) {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the `Log in with passkey` settings */\napp-user-layout app-password-settings app-webauthn-login-settings {\n  @extend %vw-hide;\n}\n/* Hide Log in with passkey on the login page */\n{{#if (webver \">=2025.5.1\")}}\n.vw-passkey-login {\n  @extend %vw-hide;\n}\n{{else}}\napp-root ng-component > form > div:nth-child(1) > div > button[buttontype=\"secondary\"].\\!tw-text-primary-600:nth-child(3) {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the or text followed by the two buttons hidden above */\n{{#if (webver \">=2025.5.1\")}}\n{{#if (or (not sso_enabled) sso_only)}}\n.vw-or-text {\n  @extend %vw-hide;\n}\n{{/if}}\n{{else}}\napp-root ng-component > form > div:nth-child(1) > div:nth-child(3) > div:nth-child(2) {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide the `Other` button on the login page */\n{{#if (or (not sso_enabled) sso_only)}}\n.vw-other-login {\n  @extend %vw-hide;\n}\n{{/if}}\n\n/* Hide Two-Factor menu in Organization settings */\nbit-nav-item[route=\"settings/two-factor\"],\na[href$=\"/settings/two-factor\"] {\n  @extend %vw-hide;\n}\n\n/* Hide organization plans */\napp-organization-plans > form > bit-section:nth-child(2) {\n  @extend %vw-hide;\n}\n\n/* Hide Collection Management Form */\napp-org-account form.ng-untouched:nth-child(5) {\n  @extend %vw-hide;\n}\n\n/* Hide 'Member Access' Report Card from Org Reports */\napp-org-reports-home > app-report-list > div.tw-inline-grid > div:nth-child(6) {\n  @extend %vw-hide;\n}\n\n/* Hide Device Verification form at the Two Step Login screen */\napp-security > app-two-factor-setup > form {\n  @extend %vw-hide;\n}\n\n/* Hide unsupported Custom Role options */\nbit-dialog div.tw-ml-4:has(bit-form-control input),\nbit-dialog div.tw-col-span-4:has(input[formcontrolname*=\"access\"], input[formcontrolname*=\"manage\"]) {\n  @extend %vw-hide;\n}\n\n/* Change collapsed menu icon to Vaultwarden */\nbit-nav-logo bit-nav-item a:before {\n  content: \"\";\n  background-image: url(\"../images/icon-white.svg\");\n  background-repeat: no-repeat;\n  background-position: center center;\n  height: 32px;\n  display: block;\n}\nbit-nav-logo bit-nav-item .bwi-shield {\n  @extend %vw-hide;\n}\n/* Hide Device Login Protection button on user settings page */\napp-user-layout app-danger-zone button:nth-child(1) {\n  @extend %vw-hide;\n}\n/**** END Static Vaultwarden Changes ****/\n/**** START Dynamic Vaultwarden Changes ****/\n{{#if signup_disabled}}\n/* From web vault 2025.1.2 and onwards, the signup button is hidden\n  when signups are disabled as the web vault checks the /api/config endpoint.\n  Note that the clients tend to cache this endpoint for about 1 hour, so it might\n  take a while for the change to take effect. To avoid the button appearing\n  when it shouldn't, we'll keep this style in place for a couple of versions */\n/* Hide the register link on the login screen */\n{{#if (webver \"<2025.3.0\")}}\napp-login form div + div + div + div + hr,\napp-login form div + div + div + div + hr + p {\n  @extend %vw-hide;\n}\n{{else}}\napp-root a[routerlink=\"/signup\"] {\n  @extend %vw-hide;\n}\n{{/if}}\n{{/if}}\n\n{{#if remember_2fa_disabled}}\n/* Hide checkbox to remember 2FA token for 30 days */\napp-two-factor-auth > form > bit-form-control {\n  @extend %vw-hide;\n}\n{{/if}}\n\n{{#unless mail_2fa_enabled}}\n/* Hide `Email` 2FA if mail is not enabled */\n.providers-2fa-1 {\n  @extend %vw-hide;\n}\n{{/unless}}\n\n{{#unless yubico_enabled}}\n/* Hide `YubiKey OTP security key` 2FA if it is not enabled */\n.providers-2fa-3 {\n  @extend %vw-hide;\n}\n{{/unless}}\n\n{{#unless webauthn_2fa_supported}}\n/* Hide `Passkey` 2FA if it is not supported */\n.providers-2fa-7 {\n  @extend %vw-hide;\n}\n{{/unless}}\n\n{{#unless emergency_access_allowed}}\n/* Hide Emergency Access if not allowed */\nbit-nav-item[route=\"settings/emergency-access\"] {\n  @extend %vw-hide;\n}\n{{/unless}}\n\n{{#unless sends_allowed}}\n/* Hide Sends if not allowed */\nbit-nav-item[route=\"sends\"] {\n  @extend %vw-hide;\n}\n{{/unless}}\n\n{{#unless password_hints_allowed}}\n/* Hide password hints if not allowed */\na[routerlink=\"/hint\"],\n{{#if (webver \"<2025.12.2\")}}\napp-change-password > form > .form-group:nth-child(5),\nauth-input-password > form > bit-form-field:nth-child(4) {\n{{else}}\n.vw-password-hint {\n{{/if}}\n  @extend %vw-hide;\n}\n{{/unless}}\n/**** End Dynamic Vaultwarden Changes ****/\n/**** Include a special user stylesheet for custom changes ****/\n{{#if load_user_scss}}\n{{> scss/user.vaultwarden.scss }}\n{{/if}}\n"
  },
  {
    "path": "src/util.rs",
    "content": "//\n// Web Headers and caching\n//\nuse std::{collections::HashMap, io::Cursor, path::Path};\n\nuse num_traits::ToPrimitive;\nuse rocket::{\n    fairing::{Fairing, Info, Kind},\n    http::{ContentType, Header, HeaderMap, Method, Status},\n    response::{self, Responder},\n    Data, Orbit, Request, Response, Rocket,\n};\n\nuse tokio::{\n    runtime::Handle,\n    time::{sleep, Duration},\n};\n\nuse crate::{config::PathType, CONFIG};\n\npub struct AppHeaders();\n\n#[rocket::async_trait]\nimpl Fairing for AppHeaders {\n    fn info(&self) -> Info {\n        Info {\n            name: \"Application Headers\",\n            kind: Kind::Response,\n        }\n    }\n\n    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {\n        let req_uri_path = req.uri().path();\n        let req_headers = req.headers();\n\n        // Check if this connection is an Upgrade/WebSocket connection and return early\n        // We do not want add any extra headers, this could cause issues with reverse proxies or CloudFlare\n        if req_uri_path.ends_with(\"notifications/hub\") || req_uri_path.ends_with(\"notifications/anonymous-hub\") {\n            match (req_headers.get_one(\"connection\"), req_headers.get_one(\"upgrade\")) {\n                (Some(c), Some(u))\n                    if c.to_lowercase().contains(\"upgrade\") && u.to_lowercase().contains(\"websocket\") =>\n                {\n                    // Remove headers which could cause websocket connection issues\n                    res.remove_header(\"X-Frame-Options\");\n                    res.remove_header(\"X-Content-Type-Options\");\n                    res.remove_header(\"Permissions-Policy\");\n                    return;\n                }\n                (_, _) => (),\n            }\n        }\n\n        // NOTE: When modifying or adding security headers be sure to also update the diagnostic checks in `src/static/scripts/admin_diagnostics.js` in `checkSecurityHeaders`\n        res.set_raw_header(\"Permissions-Policy\", \"accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()\");\n        res.set_raw_header(\"Referrer-Policy\", \"same-origin\");\n        res.set_raw_header(\"X-Content-Type-Options\", \"nosniff\");\n        res.set_raw_header(\"X-Robots-Tag\", \"noindex, nofollow\");\n\n        // Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP\n        res.set_raw_header(\"X-XSS-Protection\", \"0\");\n\n        // The `Cross-Origin-Resource-Policy` header should not be set on images or on the `icon_external` route.\n        // Otherwise some clients, like the Bitwarden Desktop, will fail to download the icons\n        let mut is_image = true;\n        if !(res.headers().get_one(\"Content-Type\").is_some_and(|v| v.starts_with(\"image/\"))\n            || req.route().is_some_and(|v| v.name.as_deref() == Some(\"icon_external\")))\n        {\n            is_image = false;\n            res.set_raw_header(\"Cross-Origin-Resource-Policy\", \"same-origin\");\n        }\n\n        // Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files.\n        // This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.\n        // This is the same behavior as upstream Bitwarden.\n        if !req_uri_path.ends_with(\"connector.html\") {\n            let csp = if is_image {\n                // Prevent scripts, frames, objects, etc., from loading with images, mainly for SVG images, since these could contain JavaScript and other unsafe items.\n                // Even though we sanitize SVG images before storing and viewing them, it's better to prevent allowing these elements.\n                String::from(\"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'none'; frame-src 'none'; object-src 'none\")\n            } else {\n                // # Frame Ancestors:\n                // Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb\n                // Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US\n                // Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/\n                // # img/child/frame src:\n                // Have I Been Pwned to allow those calls to work.\n                // # Connect src:\n                // Leaked Passwords check: api.pwnedpasswords.com\n                // 2FA/MFA Site check: api.2fa.directory\n                // # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/\n                // app.simplelogin.io, app.addy.io, api.fastmail.com, api.forwardemail.net\n                format!(\n                    \"default-src 'none'; \\\n                    font-src 'self'; \\\n                    manifest-src 'self'; \\\n                    base-uri 'self'; \\\n                    form-action 'self'; \\\n                    media-src 'self'; \\\n                    object-src 'self' blob:; \\\n                    script-src 'self' 'wasm-unsafe-eval'; \\\n                    style-src 'self' 'unsafe-inline'; \\\n                    child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \\\n                    frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; \\\n                    frame-ancestors 'self' \\\n                    chrome-extension://nngceckbapebfimnlniiiahkandclblb \\\n                    chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh \\\n                    moz-extension://* \\\n                    {allowed_iframe_ancestors}; \\\n                    img-src 'self' data: \\\n                    https://haveibeenpwned.com \\\n                    {icon_service_csp}; \\\n                    connect-src 'self' \\\n                    https://api.pwnedpasswords.com \\\n                    https://api.2fa.directory \\\n                    https://app.simplelogin.io/api/ \\\n                    https://app.addy.io/api/ \\\n                    https://api.fastmail.com/ \\\n                    https://api.forwardemail.net \\\n                    {allowed_connect_src};\\\n                    \",\n                    icon_service_csp = CONFIG._icon_service_csp(),\n                    allowed_iframe_ancestors = CONFIG.allowed_iframe_ancestors(),\n                    allowed_connect_src = CONFIG.allowed_connect_src(),\n                )\n            };\n\n            res.set_raw_header(\"Content-Security-Policy\", csp);\n            res.set_raw_header(\"X-Frame-Options\", \"SAMEORIGIN\");\n        } else {\n            // It looks like this header get's set somewhere else also, make sure this is not sent for these files, it will cause MFA issues.\n            res.remove_header(\"X-Frame-Options\");\n        }\n\n        // Disable cache unless otherwise specified\n        if !res.headers().contains(\"cache-control\") {\n            res.set_raw_header(\"Cache-Control\", \"no-cache, no-store, max-age=0\");\n        }\n    }\n}\n\npub struct Cors();\n\nimpl Cors {\n    fn get_header(headers: &HeaderMap<'_>, name: &str) -> String {\n        match headers.get_one(name) {\n            Some(h) => h.to_string(),\n            _ => String::new(),\n        }\n    }\n\n    // Check a request's `Origin` header against the list of allowed origins.\n    // If a match exists, return it. Otherwise, return None.\n    fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> {\n        let origin = Cors::get_header(headers, \"Origin\");\n        let safari_extension_origin = \"file://\";\n        let desktop_custom_file_origin = \"bw-desktop-file://bundle\";\n\n        if origin == CONFIG.domain_origin()\n            || origin == safari_extension_origin\n            || origin == desktop_custom_file_origin\n            || (CONFIG.sso_enabled() && origin == CONFIG.sso_authority())\n        {\n            Some(origin)\n        } else {\n            None\n        }\n    }\n}\n\n#[rocket::async_trait]\nimpl Fairing for Cors {\n    fn info(&self) -> Info {\n        Info {\n            name: \"Cors\",\n            kind: Kind::Response,\n        }\n    }\n\n    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {\n        let req_headers = request.headers();\n\n        if let Some(origin) = Cors::get_allowed_origin(req_headers) {\n            response.set_header(Header::new(\"Access-Control-Allow-Origin\", origin));\n        }\n\n        // Preflight request\n        if request.method() == Method::Options {\n            let req_allow_headers = Cors::get_header(req_headers, \"Access-Control-Request-Headers\");\n            let req_allow_method = Cors::get_header(req_headers, \"Access-Control-Request-Method\");\n\n            response.set_header(Header::new(\"Access-Control-Allow-Methods\", req_allow_method));\n            response.set_header(Header::new(\"Access-Control-Allow-Headers\", req_allow_headers));\n            response.set_header(Header::new(\"Access-Control-Allow-Credentials\", \"true\"));\n            response.set_status(Status::Ok);\n            response.set_header(ContentType::Plain);\n            response.set_sized_body(Some(0), Cursor::new(\"\"));\n        }\n    }\n}\n\npub struct Cached<R> {\n    response: R,\n    is_immutable: bool,\n    ttl: u64,\n}\n\nimpl<R> Cached<R> {\n    pub fn long(response: R, is_immutable: bool) -> Cached<R> {\n        Self {\n            response,\n            is_immutable,\n            ttl: 604800, // 7 days\n        }\n    }\n\n    pub fn short(response: R, is_immutable: bool) -> Cached<R> {\n        Self {\n            response,\n            is_immutable,\n            ttl: 600, // 10 minutes\n        }\n    }\n\n    pub fn ttl(response: R, ttl: u64, is_immutable: bool) -> Cached<R> {\n        Self {\n            response,\n            is_immutable,\n            ttl,\n        }\n    }\n}\n\nimpl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cached<R> {\n    fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> {\n        let mut res = self.response.respond_to(request)?;\n\n        let cache_control_header = if self.is_immutable {\n            format!(\"public, immutable, max-age={}\", self.ttl)\n        } else {\n            format!(\"public, max-age={}\", self.ttl)\n        };\n        res.set_raw_header(\"Cache-Control\", cache_control_header);\n\n        let time_now = Local::now();\n        let expiry_time = time_now + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap();\n        res.set_raw_header(\"Expires\", format_datetime_http(&expiry_time));\n        Ok(res)\n    }\n}\n\n// Log all the routes from the main paths list, and the attachments endpoint\n// Effectively ignores, any static file route, and the alive endpoint\nconst LOGGED_ROUTES: [&str; 7] = [\"/api\", \"/admin\", \"/identity\", \"/icons\", \"/attachments\", \"/events\", \"/notifications\"];\n\n// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts\npub struct BetterLogging(pub bool);\n#[rocket::async_trait]\nimpl Fairing for BetterLogging {\n    fn info(&self) -> Info {\n        Info {\n            name: \"Better Logging\",\n            kind: Kind::Liftoff | Kind::Request | Kind::Response,\n        }\n    }\n\n    async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {\n        if self.0 {\n            info!(target: \"routes\", \"Routes loaded:\");\n            let mut routes: Vec<_> = rocket.routes().collect();\n            routes.sort_by_key(|r| r.uri.path());\n            for route in routes {\n                if route.rank < 0 {\n                    info!(target: \"routes\", \"{:<6} {}\", route.method, route.uri);\n                } else {\n                    info!(target: \"routes\", \"{:<6} {} [{}]\", route.method, route.uri, route.rank);\n                }\n            }\n        }\n\n        let config = rocket.config();\n        let scheme = if config.tls_enabled() {\n            \"https\"\n        } else {\n            \"http\"\n        };\n        let addr = format!(\"{scheme}://{}:{}\", &config.address, &config.port);\n        info!(target: \"start\", \"Rocket has launched from {addr}\");\n    }\n\n    async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {\n        let method = request.method();\n        if !self.0 && method == Method::Options {\n            return;\n        }\n        let uri = request.uri();\n        let uri_path = uri.path();\n        let uri_path_str = uri_path.url_decode_lossy();\n        let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);\n        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {\n            match uri.query() {\n                Some(q) => info!(target: \"request\", \"{method} {uri_path_str}?{}\", &q[..q.len().min(30)]),\n                None => info!(target: \"request\", \"{method} {uri_path_str}\"),\n            };\n        }\n    }\n\n    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {\n        if !self.0 && request.method() == Method::Options {\n            return;\n        }\n        let uri_path = request.uri().path();\n        let uri_path_str = uri_path.url_decode_lossy();\n        let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);\n        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {\n            let status = response.status();\n            if let Some(ref route) = request.route() {\n                info!(target: \"response\", \"{route} => {status}\")\n            } else {\n                info!(target: \"response\", \"{status}\")\n            }\n        }\n    }\n}\n\npub fn get_display_size(size: i64) -> String {\n    const UNITS: [&str; 6] = [\"bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n\n    // If we're somehow too big for a f64, just return the size in bytes\n    let Some(mut size) = size.to_f64() else {\n        return format!(\"{size} bytes\");\n    };\n\n    let mut unit_counter = 0;\n\n    loop {\n        if size > 1024. {\n            size /= 1024.;\n            unit_counter += 1;\n        } else {\n            break;\n        }\n    }\n\n    format!(\"{size:.2} {}\", UNITS[unit_counter])\n}\n\npub fn get_uuid() -> String {\n    uuid::Uuid::new_v4().to_string()\n}\n\n//\n// String util methods\n//\n\nuse std::str::FromStr;\n\n#[inline]\npub fn upcase_first(s: &str) -> String {\n    let mut c = s.chars();\n    match c.next() {\n        None => String::new(),\n        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),\n    }\n}\n\n#[inline]\npub fn lcase_first(s: &str) -> String {\n    let mut c = s.chars();\n    match c.next() {\n        None => String::new(),\n        Some(f) => f.to_lowercase().collect::<String>() + c.as_str(),\n    }\n}\n\npub fn try_parse_string<S, T>(string: Option<S>) -> Option<T>\nwhere\n    S: AsRef<str>,\n    T: FromStr,\n{\n    if let Some(Ok(value)) = string.map(|s| s.as_ref().parse::<T>()) {\n        Some(value)\n    } else {\n        None\n    }\n}\n\n//\n// Env methods\n//\n\nuse std::env;\n\npub fn get_env_str_value(key: &str) -> Option<String> {\n    let key_file = format!(\"{key}_FILE\");\n    let value_from_env = env::var(key);\n    let value_file = env::var(&key_file);\n\n    match (value_from_env, value_file) {\n        (Ok(_), Ok(_)) => panic!(\"You should not define both {key} and {key_file}!\"),\n        (Ok(v_env), Err(_)) => Some(v_env),\n        (Err(_), Ok(v_file)) => match std::fs::read_to_string(v_file) {\n            Ok(content) => Some(content.trim().to_string()),\n            Err(e) => panic!(\"Failed to load {key}: {e:?}\"),\n        },\n        _ => None,\n    }\n}\n\npub fn get_env<V>(key: &str) -> Option<V>\nwhere\n    V: FromStr,\n{\n    try_parse_string(get_env_str_value(key))\n}\n\npub fn get_env_bool(key: &str) -> Option<bool> {\n    const TRUE_VALUES: &[&str] = &[\"true\", \"t\", \"yes\", \"y\", \"1\"];\n    const FALSE_VALUES: &[&str] = &[\"false\", \"f\", \"no\", \"n\", \"0\"];\n\n    match get_env_str_value(key) {\n        Some(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),\n        Some(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),\n        _ => None,\n    }\n}\n\n//\n// Date util methods\n//\n\nuse chrono::{DateTime, Local, NaiveDateTime, TimeZone};\n\n/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API\n/// responses with \"date\" fields (`CreationDate`, `RevisionDate`, etc.).\npub fn format_date(dt: &NaiveDateTime) -> String {\n    dt.and_utc().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)\n}\n\n/// Validates and formats a RFC3339 timestamp\n/// If parsing fails it will return the start of the unix datetime\npub fn validate_and_format_date(dt: &str) -> String {\n    match DateTime::parse_from_rfc3339(dt) {\n        Ok(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true),\n        _ => String::from(\"1970-01-01T00:00:00.000000Z\"),\n    }\n}\n\n/// Formats a `DateTime<Local>` using the specified format string.\n///\n/// For a `DateTime<Local>`, the `%Z` specifier normally formats as the\n/// time zone's UTC offset (e.g., `+00:00`). In this function, if the\n/// `TZ` environment variable is set, then `%Z` instead formats as the\n/// abbreviation for that time zone (e.g., `UTC`).\npub fn format_datetime_local(dt: &DateTime<Local>, fmt: &str) -> String {\n    // Try parsing the `TZ` environment variable to enable formatting `%Z` as\n    // a time zone abbreviation.\n    if let Ok(tz) = env::var(\"TZ\") {\n        if let Ok(tz) = tz.parse::<chrono_tz::Tz>() {\n            return dt.with_timezone(&tz).format(fmt).to_string();\n        }\n    }\n\n    // Otherwise, fall back to formatting `%Z` as a UTC offset.\n    dt.format(fmt).to_string()\n}\n\n/// Formats a UTC-offset `NaiveDateTime` as a datetime in the local time zone.\n///\n/// This function basically converts the `NaiveDateTime` to a `DateTime<Local>`,\n/// and then calls [format_datetime_local](crate::util::format_datetime_local).\npub fn format_naive_datetime_local(dt: &NaiveDateTime, fmt: &str) -> String {\n    format_datetime_local(&Local.from_utc_datetime(dt), fmt)\n}\n\n/// Formats a `DateTime<Local>` as required for HTTP\n///\n/// https://httpwg.org/specs/rfc7231.html#http.date\npub fn format_datetime_http(dt: &DateTime<Local>) -> String {\n    let expiry_time = DateTime::<chrono::Utc>::from_naive_utc_and_offset(dt.naive_utc(), chrono::Utc);\n\n    // HACK: HTTP expects the date to always be GMT (UTC) rather than giving an\n    // offset (which would always be 0 in UTC anyway)\n    expiry_time.to_rfc2822().replace(\"+0000\", \"GMT\")\n}\n\npub fn parse_date(date: &str) -> NaiveDateTime {\n    DateTime::parse_from_rfc3339(date).unwrap().naive_utc()\n}\n\n/// Returns true or false if an email address is valid or not\n///\n/// Some extra checks instead of only using email_address\n/// This prevents from weird email formats still excepted but in the end invalid\npub fn is_valid_email(email: &str) -> bool {\n    let Ok(email) = email_address::EmailAddress::from_str(email) else {\n        return false;\n    };\n    let Ok(email_url) = url::Url::parse(&format!(\"https://{}\", email.domain())) else {\n        return false;\n    };\n    if email_url.path().ne(\"/\") || email_url.domain().is_none() || email_url.query().is_some() {\n        return false;\n    }\n    true\n}\n\n//\n// Deployment environment methods\n//\n\n/// Returns true if the program is running in Docker, Podman or Kubernetes.\npub fn is_running_in_container() -> bool {\n    Path::new(\"/.dockerenv\").exists()\n        || Path::new(\"/run/.containerenv\").exists()\n        || Path::new(\"/run/secrets/kubernetes.io\").exists()\n        || Path::new(\"/var/run/secrets/kubernetes.io\").exists()\n}\n\n/// Simple check to determine on which container base image vaultwarden is running.\n/// We build images based upon Debian or Alpine, so these we check here.\npub fn container_base_image() -> &'static str {\n    if Path::new(\"/etc/debian_version\").exists() {\n        \"Debian\"\n    } else if Path::new(\"/etc/alpine-release\").exists() {\n        \"Alpine\"\n    } else {\n        \"Unknown\"\n    }\n}\n\n#[derive(Deserialize)]\nstruct WebVaultVersion {\n    version: String,\n}\n\npub fn get_active_web_release() -> String {\n    let version_files = [\n        format!(\"{}/vw-version.json\", CONFIG.web_vault_folder()),\n        format!(\"{}/version.json\", CONFIG.web_vault_folder()),\n    ];\n\n    for version_file in version_files {\n        if let Ok(version_str) = std::fs::read_to_string(&version_file) {\n            if let Ok(version) = serde_json::from_str::<WebVaultVersion>(&version_str) {\n                return String::from(version.version.trim_start_matches('v'));\n            }\n        }\n    }\n\n    String::from(\"Version file missing\")\n}\n\n//\n// Deserialization methods\n//\n\nuse std::fmt;\n\nuse serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor};\nuse serde_json::Value;\n\npub type JsonMap = serde_json::Map<String, Value>;\n\n#[derive(Serialize, Deserialize)]\npub struct LowerCase<T: DeserializeOwned> {\n    #[serde(deserialize_with = \"lowercase_deserialize\")]\n    #[serde(flatten)]\n    pub data: T,\n}\n\nimpl Default for LowerCase<Value> {\n    fn default() -> Self {\n        Self {\n            data: Value::Null,\n        }\n    }\n}\n\n// https://github.com/serde-rs/serde/issues/586\npub fn lowercase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>\nwhere\n    T: DeserializeOwned,\n    D: Deserializer<'de>,\n{\n    let d = deserializer.deserialize_any(LowerCaseVisitor)?;\n    T::deserialize(d).map_err(de::Error::custom)\n}\n\nstruct LowerCaseVisitor;\n\nimpl<'de> Visitor<'de> for LowerCaseVisitor {\n    type Value = Value;\n\n    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n        formatter.write_str(\"an object or an array\")\n    }\n\n    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>\n    where\n        A: MapAccess<'de>,\n    {\n        let mut result_map = JsonMap::new();\n\n        while let Some((key, value)) = map.next_entry()? {\n            result_map.insert(_process_key(key), convert_json_key_lcase_first(value));\n        }\n\n        Ok(Value::Object(result_map))\n    }\n\n    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>\n    where\n        A: SeqAccess<'de>,\n    {\n        let mut result_seq = Vec::<Value>::new();\n\n        while let Some(value) = seq.next_element()? {\n            result_seq.push(convert_json_key_lcase_first(value));\n        }\n\n        Ok(Value::Array(result_seq))\n    }\n}\n\n// Inner function to handle a special case for the 'ssn' key.\n// This key is part of the Identity Cipher (Social Security Number)\nfn _process_key(key: &str) -> String {\n    match key.to_lowercase().as_ref() {\n        \"ssn\" => \"ssn\".into(),\n        _ => lcase_first(key),\n    }\n}\n\n#[derive(Clone, Debug, Deserialize)]\n#[serde(untagged)]\npub enum NumberOrString {\n    Number(i64),\n    String(String),\n}\n\nimpl NumberOrString {\n    pub fn into_string(self) -> String {\n        match self {\n            NumberOrString::Number(n) => n.to_string(),\n            NumberOrString::String(s) => s,\n        }\n    }\n\n    #[allow(clippy::wrong_self_convention)]\n    pub fn into_i32(&self) -> Result<i32, crate::Error> {\n        use std::num::ParseIntError as PIE;\n        match self {\n            NumberOrString::Number(n) => match n.to_i32() {\n                Some(n) => Ok(n),\n                None => err!(\"Number does not fit in i32\"),\n            },\n            NumberOrString::String(s) => {\n                s.parse().map_err(|e: PIE| crate::Error::new(\"Can't convert to number\", e.to_string()))\n            }\n        }\n    }\n\n    #[allow(clippy::wrong_self_convention)]\n    pub fn into_i64(&self) -> Result<i64, crate::Error> {\n        use std::num::ParseIntError as PIE;\n        match self {\n            NumberOrString::Number(n) => Ok(*n),\n            NumberOrString::String(s) => {\n                s.parse().map_err(|e: PIE| crate::Error::new(\"Can't convert to number\", e.to_string()))\n            }\n        }\n    }\n}\n\n//\n// Retry methods\n//\n\npub fn retry<F, T, E>(mut func: F, max_tries: u32) -> Result<T, E>\nwhere\n    F: FnMut() -> Result<T, E>,\n{\n    let mut tries = 0;\n\n    loop {\n        match func() {\n            ok @ Ok(_) => return ok,\n            err @ Err(_) => {\n                tries += 1;\n\n                if tries >= max_tries {\n                    return err;\n                }\n                Handle::current().block_on(sleep(Duration::from_millis(500)));\n            }\n        }\n    }\n}\n\npub async fn retry_db<F, T, E>(mut func: F, max_tries: u32) -> Result<T, E>\nwhere\n    F: FnMut() -> Result<T, E>,\n    E: std::error::Error,\n{\n    let mut tries = 0;\n\n    loop {\n        match func() {\n            ok @ Ok(_) => return ok,\n            Err(e) => {\n                tries += 1;\n\n                if tries >= max_tries && max_tries > 0 {\n                    return Err(e);\n                }\n\n                warn!(\"Can't connect to database, retrying: {e:?}\");\n\n                sleep(Duration::from_millis(1_000)).await;\n            }\n        }\n    }\n}\n\npub fn convert_json_key_lcase_first(src_json: Value) -> Value {\n    match src_json {\n        Value::Array(elm) => {\n            let mut new_array: Vec<Value> = Vec::with_capacity(elm.len());\n\n            for obj in elm {\n                new_array.push(convert_json_key_lcase_first(obj));\n            }\n            Value::Array(new_array)\n        }\n\n        Value::Object(obj) => {\n            let mut json_map = JsonMap::new();\n            for (key, value) in obj.into_iter() {\n                match (key, value) {\n                    (key, Value::Object(elm)) => {\n                        let inner_value = convert_json_key_lcase_first(Value::Object(elm));\n                        json_map.insert(_process_key(&key), inner_value);\n                    }\n\n                    (key, Value::Array(elm)) => {\n                        let mut inner_array: Vec<Value> = Vec::with_capacity(elm.len());\n\n                        for inner_obj in elm {\n                            inner_array.push(convert_json_key_lcase_first(inner_obj));\n                        }\n\n                        json_map.insert(_process_key(&key), Value::Array(inner_array));\n                    }\n\n                    (key, value) => {\n                        json_map.insert(_process_key(&key), value);\n                    }\n                }\n            }\n\n            Value::Object(json_map)\n        }\n\n        value => value,\n    }\n}\n\n/// Parses the experimental client feature flags string into a HashMap.\npub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {\n    // These flags could still be configured, but are deprecated and not used anymore\n    // To prevent old installations from starting filter these out and not error out\n    const DEPRECATED_FLAGS: &[&str] =\n        &[\"autofill-overlay\", \"autofill-v2\", \"browser-fileless-import\", \"extension-refresh\", \"fido2-vault-credentials\"];\n    experimental_client_feature_flags\n        .split(',')\n        .filter_map(|f| {\n            let flag = f.trim();\n            if !flag.is_empty() && !DEPRECATED_FLAGS.contains(&flag) {\n                return Some((flag.to_owned(), true));\n            }\n            None\n        })\n        .collect()\n}\n\n/// TODO: This is extracted from IpAddr::is_global, which is unstable:\n/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global\n/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged\n#[allow(clippy::nonminimal_bool)]\n#[cfg(any(not(feature = \"unstable\"), test))]\npub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool {\n    match ip {\n        std::net::IpAddr::V4(ip) => {\n            !(ip.octets()[0] == 0 // \"This network\"\n            || ip.is_private()\n            || (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) //ip.is_shared()\n            || ip.is_loopback()\n            || ip.is_link_local()\n            // addresses reserved for future protocols (`192.0.0.0/24`)\n            ||(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0)\n            || ip.is_documentation()\n            || (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking()\n            || (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) //ip.is_reserved()\n            || ip.is_broadcast())\n        }\n        std::net::IpAddr::V6(ip) => {\n            !(ip.is_unspecified()\n            || ip.is_loopback()\n            // IPv4-mapped Address (`::ffff:0:0/96`)\n            || matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])\n            // IPv4-IPv6 Translat. (`64:ff9b:1::/48`)\n            || matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])\n            // Discard-Only Address Block (`100::/64`)\n            || matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _])\n            // IETF Protocol Assignments (`2001::/23`)\n            || (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)\n                && !(\n                    // Port Control Protocol Anycast (`2001:1::1`)\n                    u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001\n                    // Traversal Using Relays around NAT Anycast (`2001:1::2`)\n                    || u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002\n                    // AMT (`2001:3::/32`)\n                    || matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _])\n                    // AS112-v6 (`2001:4:112::/48`)\n                    || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])\n                    // ORCHIDv2 (`2001:20::/28`)\n                    || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b))\n                ))\n            || ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // ip.is_documentation()\n            || ((ip.segments()[0] & 0xfe00) == 0xfc00) //ip.is_unique_local()\n            || ((ip.segments()[0] & 0xffc0) == 0xfe80)) //ip.is_unicast_link_local()\n        }\n    }\n}\n\n#[cfg(not(feature = \"unstable\"))]\npub use is_global_hardcoded as is_global;\n\n#[cfg(feature = \"unstable\")]\n#[inline(always)]\npub fn is_global(ip: std::net::IpAddr) -> bool {\n    ip.is_global()\n}\n\n/// Saves a Rocket temporary file to the OpenDAL Operator at the given path.\npub async fn save_temp_file(\n    path_type: &PathType,\n    path: &str,\n    temp_file: rocket::fs::TempFile<'_>,\n    overwrite: bool,\n) -> Result<(), crate::Error> {\n    use futures::AsyncWriteExt as _;\n    use tokio_util::compat::TokioAsyncReadCompatExt as _;\n\n    let operator = CONFIG.opendal_operator_for_path_type(path_type)?;\n\n    let mut read_stream = temp_file.open().await?.compat();\n    let mut writer = operator.writer_with(path).if_not_exists(!overwrite).await?.into_futures_async_write();\n    futures::io::copy(&mut read_stream, &mut writer).await?;\n    writer.close().await?;\n\n    Ok(())\n}\n\n/// These are some tests to check that the implementations match\n/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17\n/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct\n/// Note that the is_global implementation is subject to change as new IP RFCs are created\n///\n/// To run while showing progress output:\n/// cargo +nightly test --release --features sqlite,unstable -- --nocapture --ignored\n#[cfg(test)]\n#[cfg(feature = \"unstable\")]\nmod tests {\n    use super::*;\n    use std::net::IpAddr;\n\n    #[test]\n    #[ignore]\n    fn test_ipv4_global() {\n        for a in 0..u8::MAX {\n            println!(\"Iter: {}/255\", a);\n            for b in 0..u8::MAX {\n                for c in 0..u8::MAX {\n                    for d in 0..u8::MAX {\n                        let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d));\n                        assert_eq!(ip.is_global(), is_global_hardcoded(ip), \"IP mismatch: {}\", ip)\n                    }\n                }\n            }\n        }\n    }\n\n    #[test]\n    #[ignore]\n    fn test_ipv6_global() {\n        use rand::Rng;\n\n        std::thread::scope(|s| {\n            for t in 0..16 {\n                let handle = s.spawn(move || {\n                    let mut v = [0u8; 16];\n                    let mut rng = rand::thread_rng();\n\n                    for i in 0..20 {\n                        println!(\"Thread {t} Iter: {i}/50\");\n                        for _ in 0..500_000_000 {\n                            rng.fill(&mut v);\n                            let ip = IpAddr::V6(std::net::Ipv6Addr::from(v));\n                            assert_eq!(ip.is_global(), is_global_hardcoded(ip), \"IP mismatch: {ip}\");\n                        }\n                    }\n                });\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "tools/global_domains.py",
    "content": "#!/usr/bin/env python3\n#\n# This script generates a global equivalent domains JSON file from\n# the upstream Bitwarden source repo.\n#\nimport json\nimport re\nimport sys\nimport urllib.request\n\nfrom collections import OrderedDict\n\nif not 2 <= len(sys.argv) <= 3:\n    print(f\"usage: {sys.argv[0]} <OUTPUT-FILE> [GIT-REF]\")\n    print()\n    print(\"This script generates a global equivalent domains JSON file from\")\n    print(\"the upstream Bitwarden source repo.\")\n    sys.exit(1)\n\nOUTPUT_FILE = sys.argv[1]\nGIT_REF = 'main' if len(sys.argv) == 2 else sys.argv[2]\n\nBASE_URL = f'https://github.com/bitwarden/server/raw/{GIT_REF}'\nENUMS_URL = f'{BASE_URL}/src/Core/Enums/GlobalEquivalentDomainsType.cs'\nDOMAIN_LISTS_URL = f'{BASE_URL}/src/Core/Utilities/StaticStore.cs'\n\n# Enum lines look like:\n#\n#   EnumName0 = 0,\n#   EnumName1 = 1,\n#\nENUM_RE = re.compile(\n    r'\\s*'             # Leading whitespace (optional).\n    r'([_0-9a-zA-Z]+)' # Enum name (capture group 1).\n    r'\\s*=\\s*'         # '=' with optional surrounding whitespace.\n    r'([0-9]+)'        # Enum value (capture group 2).\n)\n\n# Global domains lines look like:\n#\n#   GlobalDomains.Add(GlobalEquivalentDomainsType.EnumName, new List<string> { \"x.com\", \"y.com\" });\n#\nDOMAIN_LIST_RE = re.compile(\n    r'\\s*'             # Leading whitespace (optional).\n    r'GlobalDomains\\.Add\\(GlobalEquivalentDomainsType\\.'\n    r'([_0-9a-zA-Z]+)' # Enum name (capture group 1).\n    r'\\s*,\\s*new List<string>\\s*{'\n    r'([^}]+)'         # Domain list (capture group 2).\n    r'}\\);'\n)\n\nenums = dict()\ndomain_lists = OrderedDict()\n\n# Read in the enum names and values.\nwith urllib.request.urlopen(ENUMS_URL) as response:\n    for ln in response.read().decode('utf-8').split('\\n'):\n        m = ENUM_RE.match(ln)\n        if m:\n            enums[m.group(1)] = int(m.group(2))\n\n# Read in the domain lists.\nwith urllib.request.urlopen(DOMAIN_LISTS_URL) as response:\n    for ln in response.read().decode('utf-8').split('\\n'):\n        m = DOMAIN_LIST_RE.match(ln)\n        if m:\n            # Strip double quotes and extraneous spaces in each domain.\n            domain_lists[m.group(1)] = [d.strip(' \"') for d in m.group(2).split(\",\")]\n\n# Build the global domains data structure.\nglobal_domains = []\nfor name, domain_list in domain_lists.items():\n    entry = OrderedDict()\n    entry[\"type\"] = enums[name]\n    entry[\"domains\"] = domain_list\n    entry[\"excluded\"] = False\n    global_domains.append(entry)\n\n# Write out the global domains JSON file.\nwith open(file=OUTPUT_FILE, mode='w', encoding='utf-8') as f:\n    json.dump(global_domains, f, indent=2)\n"
  }
]